When to Use Value vs Pointer Receivers and Other Go Pitfalls You Must Avoid
This article examines common Go mistakes—including choosing between value and pointer receivers, misusing unnamed or named return values, returning nil interfaces, passing filenames instead of readers, and defer parameter evaluation—provides clear explanations, real‑world examples, and best‑practice recommendations to write more reliable Go code.
Error #42: Unsure Which Receiver Type to Use
Methods in Go can have either a value or a pointer receiver. Using a value receiver means modifications inside the method do not affect the original object, which can lead to unexpected behavior.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Count int
}
// Value receiver
func (ft FunTester) Increment() {
ft.Count += 1
}
// Pointer receiver
func (ft *FunTester) IncrementPointer() {
ft.Count += 1
}
func main() {
ft := FunTester{Name: "FunTester", Count: 0}
ft.Increment()
fmt.Printf("FunTester after value receiver = %+v
", ft) // Count still 0
ft.IncrementPointer()
fmt.Printf("FunTester after pointer receiver = %+v
", ft) // Count becomes 1
}Impact: A value receiver cannot modify the original struct, which is problematic when the method is expected to change state.
Best Practices:
Use a pointer receiver if the method needs to modify the receiver's state.
Prefer a pointer receiver for large structs to avoid copying overhead.
If the struct contains non‑copyable fields (e.g., sync.Mutex), a pointer receiver is required.
Keep receiver types consistent across methods of the same type for readability and maintainability.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Count int
}
// Pointer receiver ensures modifications persist
func (ft *FunTester) Increment() {
ft.Count += 1
}
func main() {
ft := &FunTester{Name: "FunTester", Count: 0}
ft.Increment()
fmt.Printf("FunTester after pointer receiver = %+v
", ft) // Count becomes 1
}Output:
FunTester: after pointer receiver = &{Name:FunTester Count:1}Error #43: Not Using Named Return Values
Named return values improve readability, especially when a function returns multiple values of the same type. However, misuse can cause unintended early returns or missed assignments.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Age int
}
// No named return values
func NewFunTester(name string, age int) FunTester {
return FunTester{Name: name, Age: age}
}
func main() {
tester := NewFunTester("FunTester1", 25)
fmt.Printf("FunTester: created = %+v
", tester)
}Impact: When the function should modify several results, omitting names can make the code harder to understand and maintain.
Best Practices:
Use named returns when the function returns many values or when you want to document each result.
Avoid side effects by explicitly assigning to the named return variables inside the function.
Give descriptive names to aid readability.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Age int
}
// Using named return values
func NewFunTester(name string, age int) (tester FunTester, err error) {
if age < 0 {
err = fmt.Errorf("FunTester: age cannot be negative")
return
}
tester = FunTester{Name: name, Age: age}
return
}
func main() {
tester, err := NewFunTester("FunTester1", 25)
if err != nil {
fmt.Println("FunTester: creation error:", err)
return
}
fmt.Printf("FunTester: created = %+v
", tester)
}Output:
FunTester: created = {Name:FunTester1 Age:25}Error #44: Unexpected Side Effects with Named Returns
If a named return variable is not explicitly assigned, the function may return a zero‑value unintentionally.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Age int
}
func UpdateFunTester(t FunTester) (updated FunTester, err error) {
if t.Age < 0 {
err = fmt.Errorf("FunTester: age cannot be negative")
return
}
t.Age += 1
// Forgot to assign to 'updated'
return
}
func main() {
tester := FunTester{Name: "FunTester1", Age: 25}
updatedTester, err := UpdateFunTester(tester)
if err != nil {
fmt.Println("FunTester: update error:", err)
return
}
fmt.Printf("FunTester: updated = %+v
", updatedTester) // Age unchanged
}Impact: Callers may receive an object that has not been updated, causing logic errors.
Best Practices:
Assign to all named return variables on every code path.
Use code reviews or tests to catch missing assignments.
Explicitly set the named return before returning.
package main
import (
"fmt"
)
type FunTester struct {
Name string
Age int
}
func UpdateFunTester(t FunTester) (updated FunTester, err error) {
if t.Age < 0 {
err = fmt.Errorf("FunTester: age cannot be negative")
return
}
t.Age += 1
updated = t
return
}
func main() {
tester := FunTester{Name: "FunTester1", Age: 25}
updatedTester, err := UpdateFunTester(tester)
if err != nil {
fmt.Println("FunTester: update error:", err)
return
}
fmt.Printf("FunTester: updated = %+v
", updatedTester) // Age becomes 26
}Output:
FunTester: updated = {Name:FunTester1 Age:26}Error #45: Returning a Nil Receiver
When a concrete nil pointer is returned as an interface value, the interface itself is non‑nil because it still holds type information, which can mislead callers.
package main
import (
"fmt"
)
type FunTester interface {
Run()
}
type FunTesterImpl struct { Name string }
func (ft *FunTesterImpl) Run() { fmt.Printf("FunTester: %s running
", ft.Name) }
func GetFunTester(condition bool) FunTester {
if condition {
return &FunTesterImpl{Name: "FunTester1"}
}
var ft *FunTesterImpl = nil
return ft // Interface is non‑nil
}
func main() {
tester := GetFunTester(false)
if tester == nil {
fmt.Println("FunTester: tester is nil")
} else {
fmt.Println("FunTester: tester is not nil")
tester.Run()
}
}Impact: The caller may think the interface is usable and invoke methods, leading to a runtime panic.
Best Practices:
Return a plain nil when the function should indicate the absence of an implementation.
Check both the interface and its underlying concrete value before calling methods.
Prefer factory functions that hide the nil‑pointer details.
package main
import (
"fmt"
)
type FunTester interface { Run() }
type FunTesterImpl struct { Name string }
func (ft *FunTesterImpl) Run() { fmt.Printf("FunTester: %s running
", ft.Name) }
func GetFunTester(condition bool) FunTester {
if condition {
return &FunTesterImpl{Name: "FunTester1"}
}
return nil // Explicit nil interface
}
func main() {
tester := GetFunTester(false)
if tester == nil {
fmt.Println("FunTester: tester is nil")
} else {
fmt.Println("FunTester: tester is not nil")
tester.Run()
}
}Output:
FunTester: tester is nilError #46: Using a Filename as a Function Parameter
Hard‑coding a filename limits a function to file‑based input, making testing difficult and reducing reusability.
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
)
// Original version – accepts a filename
func ReadFunTesterFile(filename string) (string, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return "", err
}
return string(data), nil
}Best Practice: Accept an io.Reader so the function can work with files, strings, network streams, etc.
package main
import (
"fmt"
"io"
"io/ioutil"
"os"
)
func ReadFunTester(r io.Reader) (string, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return "", err
}
return string(data), nil
}
func main() {
file, err := os.Open("FunTester.txt")
if err != nil {
fmt.Println("FunTester: open file error:", err)
os.Exit(1)
}
defer file.Close()
content, err := ReadFunTester(file)
if err != nil {
fmt.Println("FunTester: read error:", err)
os.Exit(1)
}
fmt.Println("FunTester: file content =", content)
}Output:
FunTester: file content = FunTester演示内容Unit tests can now use strings.NewReader without touching the filesystem.
Error #47: Ignoring Defer Parameter Evaluation
Arguments to a defer statement are evaluated when the defer is declared, not when it runs. In loops this often leads to all deferred calls using the final loop variable value.
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: cannot create file")
return
}
defer file.Close()
for i := 0; i < 3; i++ {
defer fmt.Fprintf(file, "FunTester: record %d
", i) // i evaluated now
}
fmt.Println("FunTester: loop finished")
}Impact: The deferred writes all record the same value (the final i), producing incorrect output.
Best Practices:
Capture the current loop variable in a closure before deferring.
Avoid using defer inside tight loops unless necessary.
Pass explicit values to the deferred function to control evaluation timing.
package main
import (
"fmt"
"os"
)
func main() {
file, err := os.Create("FunTester_output.txt")
if err != nil {
fmt.Println("FunTester: cannot create file")
return
}
defer file.Close()
for i := 0; i < 3; i++ {
func(n int) {
defer func() {
fmt.Fprintf(file, "FunTester: record %d
", n)
}()
}(i)
}
fmt.Println("FunTester: loop finished")
}Resulting file content:
FunTester: record 0
FunTester: record 1
FunTester: record 2These examples are part of a larger series on Go best practices and common pitfalls.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
