Go Pointers vs C++ Pointers: A Comprehensive Comparison

If you're coming from C++ to Go (or vice versa), one of the first things you'll notice is how differently these languages handle pointers. While both languages have pointers, they differ significantly in philosophy, safety, and capabilities.
This guide provides a comprehensive comparison to help you understand the key differences and write better code in both languages.
What You'll Learn
✅ Core syntax differences between Go and C++ pointers
✅ Why Go removes pointer arithmetic (and what you gain)
✅ Memory management: manual vs garbage collection
✅ Null safety and nil handling
✅ References in C++ vs pointers in Go
✅ Smart pointers in C++ and their Go equivalents
✅ Best practices for both languages
Basic Pointer Syntax
Let's start with the fundamentals. Both languages use * for dereferencing and & for getting addresses, but with subtle differences.
Go Pointer Basics
package main
import "fmt"
func main() {
// Declare and initialize
x := 42
var p *int = &x // p holds address of x
fmt.Println("Value:", x) // 42
fmt.Println("Address:", &x) // 0xc000018090 (example)
fmt.Println("Pointer:", p) // 0xc000018090
fmt.Println("Deref:", *p) // 42
// Modify through pointer
*p = 100
fmt.Println("New x:", x) // 100
// Zero value is nil
var nilPtr *int
fmt.Println("nil:", nilPtr) // <nil>
// new() allocates and returns pointer
ptr := new(int)
fmt.Println("new:", *ptr) // 0 (zero value)
}C++ Pointer Basics
#include <iostream>
int main() {
// Declare and initialize
int x = 42;
int* p = &x; // p holds address of x
std::cout << "Value: " << x << std::endl; // 42
std::cout << "Address: " << &x << std::endl; // 0x7ffee...
std::cout << "Pointer: " << p << std::endl; // 0x7ffee...
std::cout << "Deref: " << *p << std::endl; // 42
// Modify through pointer
*p = 100;
std::cout << "New x: " << x << std::endl; // 100
// nullptr (C++11+) or NULL
int* nullPtr = nullptr;
std::cout << "nullptr: " << nullPtr << std::endl; // 0
// new allocates on heap (must delete!)
int* ptr = new int(0);
std::cout << "new: " << *ptr << std::endl; // 0
delete ptr; // Required!
return 0;
}Key Syntax Differences:
| Aspect | Go | C++ |
|---|---|---|
| Declaration | var p *int | int* p or int *p |
| Null value | nil | nullptr (C++11) or NULL |
| Heap allocation | new(int) | new int |
| Deallocation | Automatic (GC) | Manual (delete) |
| Type position | After * | Before * |
Pointer Arithmetic: The Biggest Difference
This is the most significant difference between Go and C++ pointers.
C++ Allows Pointer Arithmetic
#include <iostream>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* p = arr; // Points to first element
std::cout << "p[0]: " << *p << std::endl; // 10
std::cout << "p[1]: " << *(p + 1) << std::endl; // 20
std::cout << "p[2]: " << *(p + 2) << std::endl; // 30
// Increment pointer
p++;
std::cout << "*p after p++: " << *p << std::endl; // 20
// Pointer subtraction
int* end = arr + 5;
std::cout << "Array size: " << (end - arr) << std::endl; // 5
// Dangerous: No bounds checking!
int* danger = arr + 100; // Compiles fine, undefined behavior
// std::cout << *danger; // Could crash or return garbage
return 0;
}Go Explicitly Forbids Pointer Arithmetic
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
p := &arr[0]
fmt.Println("p[0]:", *p) // 10
// These would NOT compile:
// p++ // Error: invalid operation
// p = p + 1 // Error: invalid operation
// q := p + 2 // Error: invalid operation
// Instead, use slices and indices
slice := arr[:]
for i, v := range slice {
fmt.Printf("slice[%d] = %d\n", i, v)
}
// Access specific elements
fmt.Println("Element 2:", slice[2]) // 30
}Why Go Made This Choice:
| C++ Pointer Arithmetic | Go's Alternative |
|---|---|
| Memory bugs (buffer overflows) | Compile-time prevention |
| Undefined behavior | Bounds checking with slices |
| Manual array traversal | Range-based iteration |
| Complex pointer expressions | Simpler, safer code |
Historical Context: Pointer arithmetic is the source of countless security vulnerabilities (buffer overflows, use-after-free, etc.). Go's designers chose to eliminate this entire class of bugs by disallowing pointer arithmetic entirely.
If you absolutely need low-level memory manipulation in Go, use the unsafe package:
import "unsafe"
// This is discouraged but possible
func unsafeIncrement(p *int) *int {
return (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p)))
}Memory Management
This is where Go and C++ take completely different approaches.
C++: Manual Memory Management
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Created" << std::endl; }
~Resource() { std::cout << "Destroyed" << std::endl; }
};
void demonstrateManualMemory() {
// Stack allocation (automatic cleanup)
Resource stackObj;
// Heap allocation (manual cleanup required)
Resource* heapObj = new Resource();
// If you forget this, you have a memory leak!
delete heapObj;
// Array allocation
int* arr = new int[100];
// Must use delete[] for arrays!
delete[] arr;
}
// Common memory issues in C++
void memoryProblems() {
int* p = new int(42);
// Problem 1: Forget to delete (memory leak)
// delete p; // Oops, forgot!
// Problem 2: Double delete
// delete p;
// delete p; // Crash or undefined behavior!
// Problem 3: Use after delete
delete p;
// *p = 10; // Undefined behavior!
// Problem 4: Dangling pointer
int* dangling;
{
int local = 42;
dangling = &local;
}
// *dangling is now garbage!
}Go: Garbage Collection
package main
import "fmt"
type Resource struct {
data string
}
func demonstrateGC() {
// All allocations are tracked by the garbage collector
// Stack allocation (compiler decides)
local := Resource{data: "stack"}
// Heap allocation (compiler decides via escape analysis)
heapPtr := &Resource{data: "heap"}
// No delete needed - GC handles cleanup
_ = local
_ = heapPtr
// Returning local pointer is SAFE in Go!
ptr := createResource()
fmt.Println(ptr.data) // Works fine
}
func createResource() *Resource {
local := Resource{data: "escaped to heap"}
return &local // Safe! Go's escape analysis moves this to heap
}
func main() {
demonstrateGC()
// No memory leaks, no double-free, no use-after-free
// GC handles everything automatically
}Comparison:
| Aspect | C++ | Go |
|---|---|---|
| Allocation | new/malloc | new/make/literals |
| Deallocation | delete/free (manual) | Automatic (GC) |
| Memory leaks | Possible | Rare (only reference cycles) |
| Double-free | Possible | Impossible |
| Use-after-free | Possible | Impossible |
| Dangling pointers | Possible | Impossible |
| Performance | Deterministic | GC pauses (usually <1ms) |
| Control | Full | Limited |
Null Safety
Both languages have null/nil pointers, but handle them differently.
C++ Null Handling
#include <iostream>
#include <optional> // C++17
void processPointer(int* p) {
// Must manually check for null
if (p != nullptr) {
std::cout << "Value: " << *p << std::endl;
} else {
std::cout << "Null pointer!" << std::endl;
}
}
// C++17: std::optional for nullable values
std::optional<int> findValue(int id) {
if (id > 0) {
return id * 10;
}
return std::nullopt;
}
int main() {
int* null = nullptr;
// Dereferencing null = undefined behavior!
// std::cout << *null; // Crash or worse
processPointer(null);
// Using optional
auto result = findValue(5);
if (result.has_value()) {
std::cout << "Found: " << result.value() << std::endl;
}
return 0;
}Go Nil Handling
package main
import "fmt"
func processPointer(p *int) {
// Must manually check for nil
if p != nil {
fmt.Println("Value:", *p)
} else {
fmt.Println("Nil pointer!")
}
}
// Idiomatic Go: return multiple values
func findValue(id int) (int, bool) {
if id > 0 {
return id * 10, true
}
return 0, false
}
// Or return pointer (nil = not found)
func findValuePtr(id int) *int {
if id > 0 {
result := id * 10
return &result
}
return nil
}
func main() {
var null *int // nil by default
// Dereferencing nil = panic!
// fmt.Println(*null) // panic: runtime error
processPointer(null)
// Using multiple return values
if value, ok := findValue(5); ok {
fmt.Println("Found:", value)
}
// Using pointer
if ptr := findValuePtr(5); ptr != nil {
fmt.Println("Found:", *ptr)
}
}Key Difference:
- C++: Dereferencing null is undefined behavior (anything can happen)
- Go: Dereferencing nil causes a panic (controlled crash with stack trace)
References: C++ Feature, No Go Equivalent
C++ has references, which are like auto-dereferencing pointers. Go does not have this feature.
C++ References
#include <iostream>
void modifyByReference(int& ref) {
ref = 100; // No * needed, modifies original
}
void modifyByPointer(int* ptr) {
*ptr = 200; // Must use * to dereference
}
int main() {
int x = 42;
// Reference: must be initialized, can't be null
int& ref = x;
ref = 50;
std::cout << "x after ref: " << x << std::endl; // 50
// Reference as function parameter
modifyByReference(x);
std::cout << "x after modifyByReference: " << x << std::endl; // 100
// Pointer as function parameter
modifyByPointer(&x);
std::cout << "x after modifyByPointer: " << x << std::endl; // 200
// const reference: can bind to rvalue
const int& constRef = 42; // Works!
// int& badRef = 42; // Error: can't bind to rvalue
return 0;
}Go: Only Pointers
package main
import "fmt"
// Go uses pointers for pass-by-reference semantics
func modifyByPointer(ptr *int) {
*ptr = 100 // Must use * to dereference
}
// No reference syntax - this copies
func modifyByCopy(val int) {
val = 200 // Only modifies local copy
}
func main() {
x := 42
// Go has no references, only pointers
ptr := &x
*ptr = 50
fmt.Println("x after pointer:", x) // 50
// Pointer as function parameter
modifyByPointer(&x)
fmt.Println("x after modifyByPointer:", x) // 100
// Value is copied, original unchanged
modifyByCopy(x)
fmt.Println("x after modifyByCopy:", x) // Still 100
}Why Go Doesn't Have References:
- Explicitness: Pointers make it clear when mutation happens
- Simplicity: One way to do things, not two
- Consistency: Always use
*to dereference
Smart Pointers (C++) vs Go's Approach
C++ introduced smart pointers to handle memory automatically. Go's garbage collector makes these unnecessary.
C++ Smart Pointers
#include <iostream>
#include <memory>
class Resource {
public:
std::string name;
Resource(std::string n) : name(n) {
std::cout << "Created: " << name << std::endl;
}
~Resource() {
std::cout << "Destroyed: " << name << std::endl;
}
};
int main() {
// unique_ptr: exclusive ownership
{
std::unique_ptr<Resource> unique =
std::make_unique<Resource>("unique");
// Automatically deleted when scope ends
} // "Destroyed: unique" printed here
// shared_ptr: shared ownership with reference counting
std::shared_ptr<Resource> shared1 =
std::make_shared<Resource>("shared");
{
std::shared_ptr<Resource> shared2 = shared1;
std::cout << "Count: " << shared1.use_count() << std::endl; // 2
}
std::cout << "Count: " << shared1.use_count() << std::endl; // 1
// Deleted when last shared_ptr goes out of scope
// weak_ptr: non-owning reference
std::weak_ptr<Resource> weak = shared1;
if (auto locked = weak.lock()) {
std::cout << "Still alive: " << locked->name << std::endl;
}
return 0;
}Go: Garbage Collection Handles Everything
package main
import "fmt"
type Resource struct {
name string
}
func main() {
// Go has no smart pointers - GC handles memory
// Equivalent to unique_ptr: just use pointer
{
unique := &Resource{name: "unique"}
fmt.Println("Using:", unique.name)
}
// GC will clean up when no references remain
// Equivalent to shared_ptr: all pointers are "shared"
shared1 := &Resource{name: "shared"}
shared2 := shared1 // Both point to same object
fmt.Println("shared1:", shared1.name)
fmt.Println("shared2:", shared2.name)
// GC tracks all references automatically
// No weak_ptr equivalent - Go tracks all references
// If you need weak references, use a map with explicit cleanup
}Comparison:
| C++ Smart Pointer | Go Equivalent |
|---|---|
std::unique_ptr | Regular pointer (GC handles cleanup) |
std::shared_ptr | Regular pointer (GC does ref counting) |
std::weak_ptr | No direct equivalent; use maps/callbacks |
| Custom deleters | runtime.SetFinalizer (discouraged) |
Struct Pointers
Both languages use pointers to structs, but with different syntax.
C++ Struct/Class Pointers
#include <iostream>
struct Person {
std::string name;
int age;
};
int main() {
Person person{"Alice", 30};
Person* ptr = &person;
// Two ways to access members through pointer:
std::cout << (*ptr).name << std::endl; // Dereference then access
std::cout << ptr->name << std::endl; // Arrow operator (preferred)
// Modify through pointer
ptr->age = 31;
std::cout << person.age << std::endl; // 31
// Heap allocation
Person* heapPerson = new Person{"Bob", 25};
std::cout << heapPerson->name << std::endl;
delete heapPerson;
return 0;
}Go Struct Pointers
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
person := Person{Name: "Alice", Age: 30}
ptr := &person
// Go automatically dereferences for struct access
fmt.Println(ptr.Name) // No -> needed! Auto-dereference
fmt.Println((*ptr).Name) // Also works, but not idiomatic
// Modify through pointer
ptr.Age = 31
fmt.Println(person.Age) // 31
// Create pointer directly
heapPerson := &Person{Name: "Bob", Age: 25}
fmt.Println(heapPerson.Name)
// No delete needed - GC handles it
}Key Difference:
- C++: Use
->for pointer member access,.for value member access - Go: Always use
.- Go auto-dereferences for you
Pass by Pointer in Functions
C++ Function Parameters
#include <iostream>
#include <vector>
// Pass by value (copies)
void passByValue(std::vector<int> vec) {
vec.push_back(100); // Only modifies copy
}
// Pass by pointer
void passByPointer(std::vector<int>* vec) {
vec->push_back(100); // Modifies original
}
// Pass by reference (C++ only)
void passByReference(std::vector<int>& vec) {
vec.push_back(100); // Modifies original, cleaner syntax
}
// Pass by const reference (best for read-only)
void passByConstRef(const std::vector<int>& vec) {
// vec.push_back(100); // Error: can't modify
std::cout << "Size: " << vec.size() << std::endl;
}
int main() {
std::vector<int> vec = {1, 2, 3};
passByValue(vec);
std::cout << "After value: " << vec.size() << std::endl; // 3
passByPointer(&vec);
std::cout << "After pointer: " << vec.size() << std::endl; // 4
passByReference(vec);
std::cout << "After reference: " << vec.size() << std::endl; // 5
passByConstRef(vec);
return 0;
}Go Function Parameters
package main
import "fmt"
// Pass by value (copies the slice header, not underlying array)
func passByValue(slice []int) {
slice = append(slice, 100) // May or may not affect original
// Note: slices are tricky - the header is copied, not the array
}
// Pass by pointer (explicit modification)
func passByPointer(slice *[]int) {
*slice = append(*slice, 100) // Definitely modifies original
}
// For structs - pass by value (copies)
func updatePersonValue(p Person) {
p.Age = 100 // Only modifies copy
}
// For structs - pass by pointer (modifies original)
func updatePersonPointer(p *Person) {
p.Age = 100 // Modifies original
}
type Person struct {
Name string
Age int
}
func main() {
// Slices are reference-like but have nuances
slice := []int{1, 2, 3}
passByValue(slice)
fmt.Println("After value:", len(slice)) // 3 (unchanged)
passByPointer(&slice)
fmt.Println("After pointer:", len(slice)) // 4
// Structs
person := Person{Name: "Alice", Age: 30}
updatePersonValue(person)
fmt.Println("After value:", person.Age) // 30 (unchanged)
updatePersonPointer(&person)
fmt.Println("After pointer:", person.Age) // 100
}Best Practices Comparison
C++ Best Practices
// 1. Prefer smart pointers over raw pointers
std::unique_ptr<Resource> ptr = std::make_unique<Resource>();
// 2. Use references for function parameters when possible
void process(const std::vector<int>& data);
// 3. Use nullptr, not NULL or 0
int* ptr = nullptr;
// 4. Check pointers before dereferencing
if (ptr != nullptr) {
process(*ptr);
}
// 5. Follow RAII - resources should be owned by objects
class FileHandler {
std::unique_ptr<FILE, decltype(&fclose)> file;
public:
FileHandler(const char* path) : file(fopen(path, "r"), &fclose) {}
};
// 6. Avoid pointer arithmetic when possible
// Use iterators and ranges instead
for (const auto& item : container) {
process(item);
}Go Best Practices
// 1. Use pointers when you need to modify
func (p *Person) SetAge(age int) {
p.Age = age
}
// 2. Use values for small, immutable structs
func (p Point) Distance() float64 {
return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
// 3. Check for nil before dereferencing
if ptr != nil {
process(*ptr)
}
// 4. Don't return pointer to local variable just to avoid copy
// (unless the struct is large)
// OK: func getPoint() Point { return Point{X: 1, Y: 2} }
// Only if large: func getLargeStruct() *LargeStruct { ... }
// 5. Use slices and maps (reference types) naturally
func process(data []int) { // No pointer needed
// Modifications to data[i] affect caller
}
// 6. Be consistent with receiver types
type Counter struct{ count int }
func (c *Counter) Increment() { c.count++ } // All methods use pointer
func (c *Counter) Value() int { return c.count }Summary: When to Use What
Use Pointers in Both Languages When:
| Scenario | Go | C++ |
|---|---|---|
| Modifying the original value | *Type | Type* or Type& |
| Large struct (avoid copy) | *Type | const Type& or Type* |
| Optional/nullable value | *Type (nil) | Type* or std::optional |
| Polymorphism | Interface + pointer | Base* or Base& |
Key Differences Summary
| Feature | Go | C++ |
|---|---|---|
| Pointer arithmetic | ❌ Not allowed | ✅ Full support |
| Memory management | Garbage collected | Manual (or smart pointers) |
| References | ❌ Only pointers | ✅ & syntax |
| Null value | nil | nullptr |
| Struct access | Always . | . for values, -> for pointers |
| Smart pointers | Not needed | unique_ptr, shared_ptr, etc. |
| Escape analysis | ✅ Automatic | ❌ Manual heap/stack decision |
Related Posts
- Go Phase 1: Fundamentals - Complete Go basics including pointers
- C++ Phase 1: Fundamentals - Modern C++ basics
- Pointer Receiver vs Value Receiver in Go - Method receivers
- Go Learning Roadmap - Complete Go learning path
- C++ Learning Roadmap - Complete C++ learning path
Conclusion
While both Go and C++ use pointers, they represent fundamentally different philosophies:
C++ gives you complete control over memory, including pointer arithmetic, manual memory management, and fine-grained optimization. This power comes with responsibility - you must handle memory correctly or face bugs.
Go trades some control for safety. By removing pointer arithmetic and adding garbage collection, Go eliminates entire categories of bugs while remaining efficient enough for most applications.
Neither approach is universally "better" - choose based on your requirements:
- Choose C++ when you need maximum performance, hardware control, or work in constrained environments (embedded, games, OS kernels)
- Choose Go when you want productive development, safety, and "good enough" performance (servers, CLI tools, cloud applications)
Understanding both approaches makes you a better programmer in either language!
📬 Subscribe to Newsletter
Get the latest blog posts delivered to your inbox every week. No spam, unsubscribe anytime.
We respect your privacy. Unsubscribe at any time.
💬 Comments
Sign in to leave a comment
We'll never post without your permission.