In Go there are various ways to return a struct
value or slice thereof. For individual ones I've seen:
type MyStruct struct {
Val int
}
func myfunc() MyStruct {
return MyStruct{Val: 1}
}
func myfunc() *MyStruct {
return &MyStruct{}
}
func myfunc(s *MyStruct) {
s.Val = 1
}
I understand the differences between these. The first returns a copy of the struct, the second a pointer to the struct value created within the function, the third expects an existing struct to be passed in and overrides the value.
I've seen all of these patterns be used in various contexts, I'm wondering what the best practices are regarding these. When would you use which? For instance, the first one could be ok for small structs (because the overhead is minimal), the second for bigger ones. And the third if you want to be extremely memory efficient, because you can easily reuse a single struct instance between calls. Are there any best practices for when to use which?
Similarly, the same question regarding slices:
func myfunc() []MyStruct {
return []MyStruct{ MyStruct{Val: 1} }
}
func myfunc() []*MyStruct {
return []MyStruct{ &MyStruct{Val: 1} }
}
func myfunc(s *[]MyStruct) {
*s = []MyStruct{ MyStruct{Val: 1} }
}
func myfunc(s *[]*MyStruct) {
*s = []MyStruct{ &MyStruct{Val: 1} }
}
Again: what are best practices here. I know slices are always pointers, so returning a pointer to a slice isn't useful. However, should I return a slice of struct values, a slice of pointers to structs, should I pass in a pointer to a slice as argument (a pattern used in the Go App Engine API)?
Answers
In Go, the choice between returning a struct value, a pointer to a struct, or using a function that modifies an existing struct depends on several factors including the size of the struct, the intended usage, and performance considerations. Let's discuss the best practices for each scenario:
Returning a Struct Value vs. Returning a Pointer to a Struct:
-
Returning a Struct Value:
- Suitable for small structs or when the struct is used in a read-only context.
- Avoids the overhead of memory allocation for a pointer.
- Creates a new copy of the struct, which can be expensive for large structs or when frequently copying.
-
Returning a Pointer to a Struct:
- Suitable for large structs or when modifications need to be made to the original struct.
- Avoids copying the entire struct, which can improve performance.
- Requires careful handling to avoid nil pointer dereference.
-
Modifying an Existing Struct:
- Suitable when reusing an existing struct instance is beneficial for memory efficiency.
- Avoids the overhead of memory allocation and copying.
- May lead to unexpected behavior if the same struct instance is modified concurrently in different goroutines.
Returning a Slice of Struct Values vs. Returning a Slice of Pointers to Structs:
-
Returning a Slice of Struct Values:
- Suitable when the struct instances are independent and don't need to be modified concurrently.
- Avoids the need for manual memory management.
- Provides better encapsulation as each caller receives a separate copy of the slice.
-
Returning a Slice of Pointers to Structs:
- Suitable when the struct instances need to be modified or shared among different parts of the program.
- Avoids copying the entire structs, improving performance and memory usage.
- Requires careful handling to prevent concurrent modifications from different parts of the program.
Passing a Pointer to a Slice as an Argument:
- Passing a Pointer to a Slice:
- Suitable when the function needs to modify the slice and those modifications need to be visible to the caller.
- Avoids copying the entire slice, improving performance.
- Allows the function to allocate memory for the slice if needed.
In summary, the choice between returning a struct value, a pointer to a struct, or modifying an existing struct depends on factors such as the size of the struct, the need for modifications, and memory efficiency considerations. Similarly, the choice between returning a slice of struct values and a slice of pointers to structs depends on whether the structs need to be modified or shared among different parts of the program. Use the approach that best fits your specific use case and performance requirements.