Learning Go: Notes from a Beginner — Structs, Pointers & Methods
Table of Contents
- Why This Matters
- 1. Structs — Go's version of a class
- 2. Pointers — What They Are and Why They Matter
- 3. Pointer Receiver vs. Value Receiver
- 4. Methods & Method Receivers — The Bigger Picture
- 5. Go's Object-Oriented Style
- 6. Common Pitfalls for Beginners
- Resources
Why This Matters
Coming from languages like Python or Java, Go's approach to OOP can feel unusual at first. There's no class keyword. Instead, Go uses structs and methods to achieve encapsulation and object-oriented design.
Understanding the difference between a pointer receiver and a value receiver is probably the single most important thing to get right early on — and it's exactly where most beginners get tripped up.
(對於有 Python / Java 背景的開發者來說,Go 的物件導向方式會感覺很不一樣。沒有 class,只有 struct + method。而 pointer receiver 和 value receiver 的差別,是新手最常踩坑的地方。)
1. Structs — Go's version of a class
A struct is a collection of named fields grouped together under one type. Think of it as a class without methods — it holds the data, but no behavior yet.
(struct 就像一個只有資料、沒有方法的 class。)
type Person struct {
Name string
Age int
}
p := Person{Name: "Alice", Age: 30}
fmt.Println(p.Name) // Alice
fmt.Println(p.Age) // 30You can also create a pointer to a struct:
p := &Person{Name: "Alice", Age: 30}
fmt.Println(p.Name) // Alice — Go auto-dereferences for you2. Pointers — What They Are and Why They Matter
A pointer stores the memory address of a value, rather than the value itself.
(指標存的是記憶體位址,而不是值本身。)
x := 42
p := &x // & gets the address of x (取得 x 的記憶體位址)
fmt.Println(*p) // 42 — * dereferences: gives you the value at that address (取得該位址的值)
*p = 100 // modifies x through the pointer (透過指標修改原始值)
fmt.Println(x) // 100Two key operators:
| Operator | Name | What it does |
|---|---|---|
& |
Address-of | Returns the memory address of a variable |
* |
Dereference | Returns the value stored at a pointer address |
The reason pointers matter so much in Go: they determine whether a function or method can modify the original value — which leads us to method receivers.
3. Pointer Receiver vs. Value Receiver
In Go, you can attach behavior to a type using a method — a function with a special receiver parameter placed before the function name. The receiver comes in two flavors, and this is the most important distinction to understand. (這是最關鍵的概念。)
Value Receiver — Gets a copy
A value receiver receives a copy of the struct. Any changes made inside the method do not affect the original.
(Value receiver 收到的是 struct 的副本。在方法裡做的修改不會影響原始值。)
func (p Person) HaveBirthday() {
p.Age++ // modifies the copy, NOT the original
}
alice := Person{Name: "Alice", Age: 30}
alice.HaveBirthday()
fmt.Println(alice.Age) // Still 30 — original is unchangedPointer Receiver — Gets the address
A pointer receiver receives a pointer to the struct. Changes made inside the method do affect the original.
(Pointer receiver 收到的是 struct 的指標(位址)。修改會直接影響原始資料。)
func (p *Person) HaveBirthday() {
p.Age++ // modifies the original struct directly
}
alice := Person{Name: "Alice", Age: 30}
alice.HaveBirthday()
fmt.Println(alice.Age) // 31 — original was modifiedNotice the only difference is (p Person) vs (p *Person) — the * makes all the difference.
Side-by-side comparison & when to use each
| Feature | Value Receiver (p Person) | Pointer Receiver (p *Person) |
|---|---|---|
| Receives | A copy of the struct | A pointer to the struct |
| Can modify original? | ✗ No | ✓ Yes |
| Memory | Copies the entire struct | Copies only a pointer (8 bytes) |
| Use when |
|
|
Rule of thumb (經驗法則): When in doubt, use a pointer receiver. It's far more common in real-world Go code, and it's always safe.
4. Methods & Method Receivers — The Bigger Picture
Now that you've seen how value and pointer receivers differ, let's zoom out: why does Go have receivers at all? What makes them more powerful than plain functions?
4.1 Why Not Just Use Functions? (為什麼需要 Receiver?)
Go does not support function overloading — you cannot define two functions with the same name in the same package.
(Go 不支援 Function Overloading,同一個 package 裡不能有兩個同名的 function。)
Without receivers, you'd be forced to do this:
type Square struct{ Side float64 }
type Circle struct{ Radius float64 }
// ❌ This won't compile — two functions both named Area is illegal
// func Area(s Square) float64 { return s.Side * s.Side }
// func Area(c Circle) float64 { return c.Radius * c.Radius * math.Pi }
// ✅ Workaround: prefix every function name with its type
func SquareArea(s Square) float64 { return s.Side * s.Side }
func CircleArea(c Circle) float64 { return c.Radius * c.Radius * math.Pi }
fmt.Println(SquareArea(s)) // verbose
fmt.Println(CircleArea(c)) // verboseWith receivers, you bind Area to each type — no collision:
func (s Square) Area() float64 { return s.Side * s.Side }
func (c Circle) Area() float64 { return c.Radius * c.Radius * math.Pi }
fmt.Println(s.Area()) // clean: noun.verb()
fmt.Println(c.Area())Bonus: WhenSquareandCircleboth have.Area()with the same signature, you unlock Interfaces — one function can handle all shapes at once. (當兩個 struct 都有相同簽名的 method,就能套用 Interface,讓同一段程式碼同時處理所有形狀。)
4.2 Compiler Magic: Automatic Pointer/Value Conversion (編譯器自動轉型)
Methods are flexible — Go's compiler automatically handles pointer/value conversion for you in both directions, unlike plain functions which are strict.
// Regular functions: type must match exactly (普通函式:型別必須完全吻合)
ScaleFunc(v, 5) // ❌ need pointer, got value
ScaleFunc(&v, 5) // ✅ must add & manually
AbsFunc(p) // ❌ need value, got pointer
AbsFunc(*p) // ✅ must dereference manually
// Methods: compiler converts automatically (Method:編譯器自動轉換)
v.Scale(5) // ✅ Go rewrites as (&v).Scale(5) behind the scenes
p.Abs() // ✅ Go rewrites as (*p).Abs() behind the scenes
| Situation | Regular Function | Method (Receiver) |
|---|---|---|
| Needs pointer, got value |
❌ error — write Func(&v)
|
✅ auto-converts to (&v).Method()
|
| Needs value, got pointer |
❌ error — write Func(*p)
|
✅ auto-converts to (*p).Method()
|
4.3 Real-World Example: From ByteSlice to io.Writer
This example from Effective Go shows the full receiver design journey in one place — and also reveals that receivers aren't limited to structs. Any named type qualifies.
(Receiver 不只能綁在 struct 上,任何你自定義的型別都可以。下面這個例子把整個 receiver 設計思路完整走一遍。)
Stage 1 — Value Receiver: works, but clunky
type ByteSlice []byte // a named type based on []byte — not a struct!
func (slice ByteSlice) Append(data []byte) ByteSlice {
return append(slice, data...)
// operates on a copy — must return the result
}
b = b.Append(data) // must reassign every time — clunky (每次都要重新賦值,很笨重)Stage 2 — Pointer Receiver: clean, modifies in place
func (p *ByteSlice) Append(data []byte) {
*p = append(*p, data...)
// modifies original directly — no return needed
}
b.Append(data) // clean — Go auto-converts to (&b).Append(data)Stage 3 — Satisfy io.Writer: plug into the standard library
// Rename to Write + add (n int, err error) return values
// → *ByteSlice now satisfies io.Writer
func (p *ByteSlice) Write(data []byte) (n int, err error) {
*p = append(*p, data...)
return len(data), nil
}
var b ByteSlice
fmt.Fprintf(&b, "This hour has %d days\n", 7)
// fmt.Fprintf accepts any io.Writer — and *ByteSlice now qualifies!Why rename to Write and add return values? Because io.Writer requires exactly Write(p []byte) (n int, err error). Match that signature and your type plugs into the entire Go standard library. (Interfaces are a topic for a future post — the key takeaway here is that pointer receivers are what make this possible.)
(為什麼要改名叫 Write 並加回傳值?因為符合 io.Writer 的簽名,就能讓這個 type 跟 Go 標準庫無縫接軌。Interface 的細節留待後續篇章,這裡只要記住:pointer receiver 是達成這件事的關鍵。)
Why &b is required — no magic when passing as interface
| Direct method call | Passing as interface | |
|---|---|---|
| Example |
b.Write(data)
|
fmt.Fprintf(b, ...)
|
| b is value, Write needs pointer |
✅ auto-adds & → (&b).Write(data)
|
❌ compile error — must write &b explicitly
|
| Why | b is addressable — safe to auto-convert | Interface would receive a copy; modifications would be silently discarded |
The official explanation from Effective Go:
"Pointer methods can modify the receiver; invoking them on a value would cause the method to receive a copy of the value, so any modifications would be discarded. The language therefore disallows this mistake."
If Go silently passed a copy of b to io.Writer, the Write call would modify that copy — then throw it away. Your original b would never change. This silent bug is nearly impossible to debug, so Go forces a compile error instead.
(如果 Go 允許把值傳給需要指標的 interface,底層呼叫 Write 時修改的只是那份複製品,原始資料不會有任何變化。這種無聲的 bug 極難追蹤,所以 Go 選擇直接讓它編譯失敗。)
Rule of thumb: When calling a method directly, Go is helpful — it adds&for you. When satisfying an interface, Go is strict — you must get the type right yourself.
(直接呼叫 method,Go 幫你加&;傳入 interface,Go 要你自己負責。)
5. Go's Object-Oriented Style
Go achieves OOP without class, extends, or implements. The toolkit is:
- Structs — group related data (組合資料)
- Methods with receivers — attach behavior to a type (附加行為)
- Interfaces — define behavior contracts for polymorphism (多型,後續篇章再展開)
(Go 沒有傳統 OOP 的 class 和繼承,但透過 struct + method + interface 實現了物件導向的核心概念。)
type Animal struct{ Name string }
func (a *Animal) Speak() string {
return a.Name + " makes a sound"
}
// Embedding — Go's alternative to inheritance (struct 嵌入,Go 版的繼承替代方案)
type Dog struct {
Animal
Breed string
}
fido := Dog{Animal: Animal{Name: "Fido"}, Breed: "Labrador"}
fmt.Println(fido.Speak()) // Fido makes a soundGo favors composition over inheritance — you embed types instead of extending them. (Go 偏好組合而非繼承。)
6. Common Pitfalls for Beginners (常見坑)
1. Using a value receiver when you meant to modify the struct
// BUG: value receiver — won't modify the original
func (p Person) SetName(name string) { p.Name = name }
// FIX: pointer receiver
func (p *Person) SetName(name string) { p.Name = name }2. Mixing pointer and value receivers on the same type
Go gets confused when you try to implement an interface with inconsistent receiver types. Pick one style and stick with it across all methods on a type.
3. Dereferencing a nil pointer
var p *Person // p is nil
if p != nil {
fmt.Println(p.Name) // safe
}
// Without the check: runtime panic!Resources
- A Tour of Go — Methods and Interfaces — interactive sandbox, perfect for testing receiver behavior hands-on
- Go by Example — Pointers | Structs | Methods — code-first, no fluff
- Effective Go — Pointers vs. Values — the official design philosophy, including the ByteSlice → io.Writer evolution