Keywords: Go language | pointer operations | literals | address operator | generics
Abstract: This article provides a comprehensive exploration of the challenges in creating *int64 pointer literals in Go, explaining from the language specification perspective why constants cannot be directly addressed. It systematically presents seven solutions including traditional methods like using the new() function, helper variables, helper functions, anonymous functions, slice literals, helper struct literals, and specifically introduces the generic solution introduced in Go 1.18. Through detailed code examples and principle analysis, it helps developers fully understand the underlying mechanisms and best practices of pointer operations in Go.
Introduction
In Go programming, handling pointer types is a common task. When assigning values to *int64 fields in structs, developers may encounter a seemingly simple yet challenging problem: how to directly create a *int64 literal pointing to a specific integer value? This article starts from the language specification, deeply analyzes the root cause of this problem, and systematically introduces multiple solutions.
Problem Context and Language Specification Limitations
Consider the following struct definition:
type SomeType struct {
SomeField *int64
}When attempting to create a literal directly, developers might write:
instance := SomeType{
SomeField: &0,
}This code causes a compilation error: cannot use &0 (type *int) as type *int64 in field value. Even with type conversion:
instance := SomeType{
SomeField: &int64(0),
}The error persists: cannot take the address of int64(0).
The fundamental reason for these errors lies in Go's language specification restrictions on the address operator. According to the Go Language Specification, the operand of the address operator & must be addressable. Specifically, the operand must be one of:
- A variable
- A pointer indirection
- A slice indexing operation
- A field selector of an addressable struct operand
- An array indexing operation of an addressable array
The specification specifically notes that as an exception to the addressability requirement, x (in the expression &x) may also be a (possibly parenthesized) composite literal. However, numeric constants (whether untyped or typed) do not satisfy the addressability requirement and therefore cannot be directly addressed.
Traditional Solutions
1. Using the new() Function
For cases requiring zero-value pointers, the simplest approach is using the built-in new() function:
instance := SomeType{
SomeField: new(int64),
}new(int64) allocates a zero-value int64 and returns its pointer. This method is concise and efficient but limited to obtaining zero-value pointers.
2. Using Helper Variables
For non-zero values, the most straightforward and recommended approach is using helper variables:
helper := int64(2)
instance := SomeType{
SomeField: &helper,
}This method is clear, understandable, and follows Go idioms. Helper variables are allocated on the stack, with compiler optimizations applied.
3. Using Helper Functions
If *int64 pointers need to be created in multiple places, helper functions can be defined:
func Int64Ptr(x int64) *int64 {
return &x
}
instance := SomeType{
SomeField: Int64Ptr(3),
}The Go compiler performs escape analysis, allocating function parameters on the heap to ensure returned pointers remain valid. This approach improves code reusability.
4. Using Anonymous Functions
For one-time use cases, anonymous functions can be employed:
instance := SomeType{
SomeField: func() *int64 { i := int64(4); return &i }(),
}Or a more concise version:
instance := SomeType{
SomeField: func(i int64) *int64 { return &i }(4),
}While the syntax is somewhat complex, this method avoids introducing additional named entities.
5. Using Slice Literals
A less intuitive but effective approach leverages the addressability of slice literals:
instance := SomeType{
SomeField: &[]int64{5}[0],
}This method creates a slice with a single element, then takes the address of that element. Under the hood, the compiler allocates a [1]int64 array as the slice's backing array. While functionally viable, this approach has poor readability and some performance overhead.
6. Using Helper Struct Literals
Leveraging the composite literal addressability exception, helper structs can be created:
type intWrapper struct {
x int64
}
instance := SomeType{
SomeField: &(&intWrapper{6}).x,
}The expression &(&intWrapper{6}).x is parsed as follows: first create an intWrapper struct literal and take its address, then select the x field, and finally take the address of that field. This method utilizes two language specification features: the composite literal addressability exception and the addressability of field selectors of addressable struct operands.
7. Using Anonymous Struct Literals
To avoid defining additional types, anonymous structs can be used:
instance := SomeType{
SomeField: &(&struct{ x int64 }{7}).x,
}This method shares the same principle as solution 6 but is more self-contained, requiring no pre-defined struct type.
Go 1.18 Generic Solution
Go 1.18 introduced generics, providing a more elegant solution to this problem. A generic Ptr() function can be defined:
func Ptr[T any](v T) *T {
return &v
}Usage example:
instance := SomeType{
SomeField: Ptr(int64(8)),
}This generic function works with any type, greatly simplifying pointer creation. While such a function is not yet included in the standard library, community implementations exist, such as the gog.Ptr() function in the github.com/icza/gog library.
Performance and Memory Considerations
When choosing a solution, performance and memory impacts should be considered:
- Helper variables and functions: The compiler performs escape analysis to ensure proper memory allocation. For local use, variables are typically stack-allocated; when addresses escape the scope, they are heap-allocated.
- new() function: Always allocates zero values with good efficiency, but limited to zero-value scenarios.
- Slice and struct literal methods: Introduce additional memory allocations that may impact performance, especially in hot paths.
- Generic functions: Type instantiation occurs at compile time, with runtime overhead equivalent to regular functions.
In most cases, helper variables or functions represent the best choice, balancing readability, performance, and clarity.
Practical Application Recommendations
Based on different usage scenarios, the following recommendations are suggested:
- Single use, zero value needed: Use
new(int64) - Single use, non-zero value needed: Use helper variables
- Multiple uses, frequent pointer needs in codebase: Define helper functions or use generic functions
- Minimal dependencies desired: Use anonymous functions or anonymous struct literals
- Using Go 1.18 or later: Consider implementing or using generic
Ptr()functions
Conclusion
The problem of creating *int64 literals in Go appears simple but involves deep considerations in language design. By understanding the limitations of the address operator and addressability requirements, developers can choose the most appropriate solution for their context. From traditional helper variable approaches to modern generic solutions, each method has its applicable scenarios. As Go evolves, particularly with the introduction of generics, solutions to such problems become more elegant and unified. In practical development, the choice of implementation should balance code readability, maintainability, and performance requirements.