Back to blog

Go Pointers vs C++ Pointers: A Comprehensive Comparison

gogolangcppc++pointersmemory-management
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:

AspectGoC++
Declarationvar p *intint* p or int *p
Null valuenilnullptr (C++11) or NULL
Heap allocationnew(int)new int
DeallocationAutomatic (GC)Manual (delete)
Type positionAfter *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 ArithmeticGo's Alternative
Memory bugs (buffer overflows)Compile-time prevention
Undefined behaviorBounds checking with slices
Manual array traversalRange-based iteration
Complex pointer expressionsSimpler, 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:

AspectC++Go
Allocationnew/mallocnew/make/literals
Deallocationdelete/free (manual)Automatic (GC)
Memory leaksPossibleRare (only reference cycles)
Double-freePossibleImpossible
Use-after-freePossibleImpossible
Dangling pointersPossibleImpossible
PerformanceDeterministicGC pauses (usually <1ms)
ControlFullLimited

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:

  1. Explicitness: Pointers make it clear when mutation happens
  2. Simplicity: One way to do things, not two
  3. 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 PointerGo Equivalent
std::unique_ptrRegular pointer (GC handles cleanup)
std::shared_ptrRegular pointer (GC does ref counting)
std::weak_ptrNo direct equivalent; use maps/callbacks
Custom deletersruntime.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:

ScenarioGoC++
Modifying the original value*TypeType* or Type&
Large struct (avoid copy)*Typeconst Type& or Type*
Optional/nullable value*Type (nil)Type* or std::optional
PolymorphismInterface + pointerBase* or Base&

Key Differences Summary

FeatureGoC++
Pointer arithmetic❌ Not allowed✅ Full support
Memory managementGarbage collectedManual (or smart pointers)
References❌ Only pointers& syntax
Null valuenilnullptr
Struct accessAlways .. for values, -> for pointers
Smart pointersNot neededunique_ptr, shared_ptr, etc.
Escape analysis✅ Automatic❌ Manual heap/stack decision


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.