Every time you ask AI to "add behavior to this struct," it generates methods. Methods are how Go attaches functions to types -- they're the closest thing Go has to class methods, but without classes. Your job is knowing when AI picked the right receiver type, because it frequently doesn't.
What makes a method different from a function
A regular function stands alone. A method has a receiver that ties it to a specific type.
When you ask AI to "create a User type with a full name method," it generates something like:
type User struct {
FirstName string
LastName string
}
func (u User) FullName() string {
return u.FirstName + " " + u.LastName
}
// Usage
user := User{FirstName: "Alice", LastName: "Chen"}
fmt.Println(user.FullName()) // Output: Alice ChenThe receiver (u User) sits between func and the method name. It says: "This method belongs to the User type, and u holds the specific instance when called."
Value receivers vs pointer receivers
This is where AI makes the most mistakes. Go gives you two receiver types, and picking wrong creates silent bugs.
Value receivers: the copy trap
A value receiver makes a copy of the struct when the method is called:
func (u User) SetName(newName string) {
u.FirstName = newName // Modifies the copy, not the original
}
user := User{FirstName: "Alice"}
user.SetName("Bob")
fmt.Println(user.FirstName) // Still "Alice" -- the change vanishedPointer receivers: mutations that stick
A pointer receiver gets the address of the struct, letting you modify the original:
func (u *User) SetName(newName string) {
u.FirstName = newName // Modifies the original struct
}
user := User{FirstName: "Alice"}
user.SetName("Bob")
fmt.Println(user.FirstName) // "Bob" -- change persistsDeciding which receiver to use
When reviewing AI-generated methods, use this decision table:
| Question | Value receiver | Pointer receiver |
|---|---|---|
| Does the method modify the struct? | Never correct | Required |
| Is the struct large (many fields)? | Wasteful (copies data) | Efficient (copies pointer) |
| Does any other method on this type use a pointer? | Inconsistent | Use pointer for all |
| Is the struct small and read-only? | Fine | Unnecessary |
| Does it need to be called on a nil receiver? | Panics | Can check for nil |
The consistency rule matters most in practice: if any method on a type needs a pointer receiver, all methods on that type should use pointer receivers. AI almost never follows this convention on its own.
// AI often generates this mixed style -- it's wrong
type Counter struct {
value int
}
func (c Counter) Get() int { // Value receiver
return c.value
}
func (c *Counter) Increment() { // Pointer receiver
c.value++
}The fix: make Get use *Counter too. Mixed receivers create confusing behavior when values are copied.
Methods can be called on nil pointers without panicking, as long as the method checks:
> func (u *User) Name() string {
> if u == nil {
> return ""
> }
> return u.FirstName
> }
>This pattern appears in the standard library (like
error implementations). AI sometimes generates nil-safe methods when it shouldn't, or omits nil checks when it should. Evaluate based on whether "no value" is a valid state for your type.Methods on non-struct types
AI will occasionally generate methods on custom types based on primitives. This is a legitimate and powerful pattern:
type Celsius float64
func (c Celsius) ToFahrenheit() float64 {
return float64(c)*9/5 + 32
}
func (c *Celsius) Add(delta float64) {
*c += Celsius(delta)
}
temp := Celsius(25)
fmt.Println(temp.ToFahrenheit()) // 77
temp.Add(5)
fmt.Println(temp) // 30This is powerful for domain modeling. Instead of passing around raw float64, a Celsius type with methods makes the APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. self-documenting.
> type ID int // Valid -- new named type
> // func (i int) String() string {} // Invalid -- cannot extend built-in types
>Common patterns AI generates
The String() method
When you ask AI to "make this type printable," it implements fmt.Stringer:
type Point struct {
X, Y int
}
func (p Point) String() string {
return fmt.Sprintf("Point(%d, %d)", p.X, p.Y)
}
pt := Point{X: 3, Y: 4}
fmt.Println(pt) // Output: Point(3, 4)String() on a pointer receiver (func (p *Point) String() string). This means fmt.Println(point) won't use your custom format unless point is a pointer. For String(), value receivers are almost always correct.Builder pattern
When you ask AI to "make a fluent APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. for configuration," expect:
type Config struct {
Host string
Port int
}
func (c *Config) WithHost(host string) *Config {
c.Host = host
return c
}
func (c *Config) WithPort(port int) *Config {
c.Port = port
return c
}
config := &Config{}
config.WithHost("localhost").WithPort(8080)Pointer receivers are correct here because each call mutates and returns the same instance.
Methods vs functions: when to use each
| Use case | Function | Method |
|---|---|---|
| Works with a specific type's data | No | Yes |
| Generic utility (works with any string, int, etc.) | Yes | No |
| Needs to modify the original struct | Only with pointer param | Yes, with pointer receiver |
| Part of a type's core behavior | No | Yes |
| Standalone logic unrelated to a type | Yes | No |
Quick reference
| Syntax | What it does | When to use |
|---|---|---|
func (t Type) Name() {} | Value receiver -- gets a copy | Read-only operations, small structs |
func (t *Type) Name() {} | Pointer receiver -- gets address | Modifying data, large structs, consistency |
func (t Type) String() string | String representation | For fmt.Println, debugging |