Go Pointers: Best Practices and the New Initialization Proposal

Pointer Initialization Proposal: new(v) Syntax

image.png|300

Note: The core content was generated by an LLM, with human fact-checking and structural refinement.

Core Concepts: Go’s Nature

Go is fundamentally a pass-by-value language. This means that when a variable, such as a struct, is passed to a function, a copy of the entire struct is made. Unlike languages like C, where memory is explicitly allocated on the heap using malloc() and pointers to this memory are passed around, Go manages memory through a garbage collector.

The Go compiler is remarkably intelligent, capable of inlining functions to prevent unnecessary copies. It also plays a crucial role in memory management, determining whether data should reside on the stack or the heap. The compiler attempts to allocate local variables on the stack, which is faster and reduces the workload for the garbage collector, unless the variable might be referenced outside the function’s scope or is too large. Programmers in Go typically do not need to concern themselves with whether a variable is on the stack or heap, nor do they have direct control over this allocation, as the compiler handles it efficiently.

It’s important to understand that Go does not have “pass-by-reference” in the traditional sense; instead, all variables possess their own unique memory location, even when they are copies of one another. When a pointer is passed, what is actually copied is the address value of the original variable, not the variable itself. This distinction is key to understanding how modifications work.

When to Use Pointers

While Go’s pass-by-value nature is foundational, pointers become essential in several scenarios, primarily concerning mutability, performance, and shared state:

  1. To Mutate State: Pointers are necessary when you intend for operations to update the state of an “object” (struct) and for those changes to propagate outside the current function’s scope. This is also the most compelling reason to use pointer receivers for methods, allowing them to operate on the actual struct rather than a copy. If you are uncertain about a struct’s immutability, passing a pointer is generally safer to prevent hard-to-track side effects.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    type Book struct {
    Name string
    Contents []byte
    }

    // Using a pointer receiver to mutate the original struct
    func (b *Book) ChangeName(newName string) {
    b.Name = newName
    }

    // Example of a function that modifies contents in place using a pointer
    func FixContents(b *Book) {
    // b has a pointer to the original object, so it can modify b.Contents in place
    // and change the name in place, etc.
    b.Contents = []byte("fixed content")
    }

    This approach clearly signifies the intent to mutate the underlying data.

  2. For Large Structs and Performance Optimization: If you are dealing with exceptionally large chunks of memory that are demonstrably expensive to copy, using pointers for optimization might be justified. The “tipping point” for passing a struct versus a pointer can be around 80-100 bytes, but significant optimization benefits typically manifest with structs around 500 bytes or larger, especially during frequent, cheap operations. Heap allocation, accessed via pointers, means the data is not copied when passed through multiple functions, potentially resulting in significant overhead reduction for large values.

  3. To Share the Same Instance (Singleton-like behavior): Pointers are crucial when you want to share a single instance of a struct or value across multiple parts of your program to conserve memory and ensure consistency. This is common for dependency injection patterns, where a single configuration or database client object should be used everywhere. Objects like mutexes, which are designed to be single entry points for resource access, must always be passed by pointer to prevent unintended duplication and ensure proper synchronization.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type PostStore struct {
    db *sql.DB
    }
    type UserStore struct {
    db *sql.DB
    }
    type Storage struct {
    Posts *PostStore
    Users *UserStore
    }

    func NewStorage(db *sql.DB) Storage {
    return Storage{
    Posts: &PostStore{db}, // PostStore and UserStore are created as pointers
    Users: &UserStore{db}, // to ensure the same instances are used throughout the app.
    }
    }
  4. For Complex Data Structures: Data structures that intrinsically rely on interconnected elements, such as linked lists, trees, or graphs, inherently require pointers to link their components together.

  5. Distinguishing Zero Values from “Not Set” (Optional Values): A very common use case for pointers, especially with non-struct types, is to differentiate between a field explicitly set to its zero value (e.g., 0 for an int, “” for a string) and a field that was never provided or set. This scenario frequently arises in APIs, CLI flags, or optional fields in configuration structs. Libraries like AWS SDK and Stripe APIs use pointers extensively for this purpose, providing helper functions (e.g., aws.String(), aws.Int()) to create pointers to primitive types. This also helps with interoperability with other languages that interpret zero values differently than Go, which prefers explicit nil for optional fields.

  6. Working with Interfaces and Method Sets: If a type T has methods with pointer receivers (e.g., func (t *T) M()), and you want to use that type with an interface I that defines M, you must use a pointer to T (i.e., *T) when assigning it to I. This is because the method set of T does not include methods defined on *T. Consistently using pointer receivers for structs can simplify reasoning about method sets.

  7. Subtle Aliasing (Visibility): While aliasing can lead to bugs, when you are using explicit pointers, it’s generally easier to trace where the alias happens compared to subtle aliasing that can occur when value structs contain internal references like maps or slices. Copying a struct containing slices, maps, or pointers results in a shallow copy, where the internal references still point to the same underlying memory, potentially leading to bugs and data races if not handled carefully.

    1
    2
    3
    4
    5
    6
    7
    type MyType struct {
    m map[string]interface{} // map itself is an internal pointer
    }

    // If you copy a MyType struct, both the original and the copy will refer to the *same* map,
    // even though 'm' is not explicitly declared as *map[string]interface{}.
    // This is because map[string]interface{} is internally a *runtime.hmap (a pointer).

Go 1.25 Pointer Initialization Proposal (new(v))

A long-standing “ergonomics problem” in Go has been the asymmetry in creating pointers to basic types compared to structs. While creating a pointer to an initialized struct is concise (e.g., p := &S{a: 3}), doing the same for a primitive like an int requires two lines:

1
2
a := 3
p := &a

This verbosity becomes a significant pain point when working with APIs (like JSON, Protobuf, or AWS SDK) that extensively use pointers to represent optional fields. The result is a proliferation of thousands of repetitive helper functions like StringPtr and Int64Ptr across the Go ecosystem.

Rob Pike, a co-creator of Go, highlighted this issue in proposal #45624, sparking extensive community debate.

Rejected Alternative Solutions:

  • Extending the & Operator:
    • &T(v) (e.g., p := &int(3)) was an initial idea, logically extending type conversions.
    • &v (e.g., p := &3 or p := &time.Now()) was more general but deeply problematic. It introduced severe ambiguity; for instance, &m[k] would behave differently for slices (taking an address) versus maps (copying the value then taking its address), leading to hard-to-detect bugs. This proposal was rejected due to violating the “least astonishment principle”.
  • Introducing a New Generic Built-in Function: With Go 1.18’s generics, a generic helper like func ptr[T any](v T) *T { return &v } (used as p := ptr(3)) seemed like a natural fit. However, this faced challenges regarding naming (e.g., ptr, ref, addr) and its placement in the standard library.

The new(v) Consensus:

The Go proposal committee has gravitated towards extending the new built-in function with the new(v) syntax. Earlier variations like new(T, v) (e.g., new(int, 3)) were considered too verbose due to redundant type specification.

The new(v) syntax (e.g., new(3)) is the most concise solution, allowing new to directly accept a value and infer the pointer type. The primary concern was syntax ambiguity – differentiating new(pkg.X) where pkg.X is a type (current new(T)) from new(v) where v is a constant value.

However, the committee concluded that this ambiguity would be minor in practice, as context typically clarifies the intent. They also found new(v) to be a safer choice than &v because new clearly conveys the intent of “creating something new,” avoiding the “copy or reference” confusion associated with &. Furthermore, repurposing the less frequently used new(T) to provide more powerful functionality is seen as a beneficial “cleanup” of the language.

How new(expr) Will Work:

Based on the consensus, the new(expr) syntax will operate as follows:

  • Basic Usage: p := new(3) will create a *int with the value 3. Similarly, s := new("hello") will create a *string with the value “hello”.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Before:
    i := 42
    ptrToInt := &i

    s := "Go rocks!"
    ptrToString := &s

    // After (with new(expr) proposal):
    ptrToInt := new(42) // Creates a *int with value 42
    ptrToString := new("Go rocks!") // Creates a *string with value "Go rocks!"
  • Type Inference: For untyped constants, Go’s default type rules will apply (e.g., integers default to int, floating-point numbers to float64).

  • Explicit Type Specification: If a type other than the default is required, an explicit type conversion can be used: p64 := new(int64(3)) would create a *int64.

    1
    ptrToInt64 := new(int64(100)) // Creates a *int64
  • No Contextual Type Inference: Crucially, new(v) will not infer its type based on the assignment context. For example, var p *int64 = new(3) would result in a compilation error because new(3) explicitly creates a *int, which cannot be assigned to a *int64.

This proposed change, while seemingly minor, represents a thoughtful evolution in Go’s design, aiming to solve a common developer pain point and enhance the language’s ergonomics without compromising consistency or introducing new ambiguities.

General Advice

Adhering to general best practices ensures robust and efficient Go code:

  • Prioritize Semantics Over Expediency: Focus on the logical intent of your code. Avoid using pointers simply for convenience if there isn’t a genuine need for shared memory or mutation.
  • Measure Before Optimizing: Performance optimizations, especially those involving pointers, should always be guided by benchmarks and measurements. “Premature optimization is the root of all evil”. Only optimize if you can empirically demonstrate a meaningful improvement for your specific use case.
  • Minimize Allocations and Pointer Use: Go’s runtime performance is heavily influenced by memory allocations and subsequent garbage collection. Write your code as simply as possible and minimize unnecessary allocations and pointer usage where values would suffice. The compiler’s ability to keep data on the stack helps avoid GC overhead.
  • Consistency for Method Receivers: For struct methods, some developers and projects opt for pointer receivers everywhere, even for non-mutating methods, for the sake of consistency. This approach can simplify understanding method sets and avoid common pitfalls for newer Go developers.
  • Consider Nil-ability for Basic Types: For basic types, the choice between a pointer and a value can depend on whether you need the variable to be nil-able. Pointers can be nil, allowing you to represent the absence of a value, but this also introduces the need to handle nil checks.

References:

More

Recent Articles:

Random Article:


More Series Articles about You Should Know In Golang:

https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

And I’m Wesley, delighted to share knowledge from the world of programming. 

Don’t forget to follow me for more informative content, or feel free to share this with others who may also find it beneficial. It would be a great help to me.

Give me some free applauds, highlights, or replies, and I’ll pay attention to those reactions, which will determine whether I continue to post this type of article.

See you in the next article. 👋

中文文章: https://programmerscareer.com/zh-cn/go-newv-rob-pike/
Author: Medium,LinkedIn,Twitter
Note: Originally written at https://programmerscareer.com/go-newv-rob-pike/ at 2025-09-07 16:33.
Copyright: BY-NC-ND 3.0

Go Error Handling Evolution and AsA Proposals Go's Journey to Official HTTP/3 and QUIC Implementation

Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×