Methods
Method คืออะไร?
หัวข้อที่มีชื่อว่า “Method คืออะไร?”Method คือ function ที่มี receiver — parameter พิเศษที่ปรากฏระหว่าง keyword func กับชื่อ function receiver ผูก function นั้นเข้ากับ named type
func (r Rectangle) Area() float64 { return r.Width * r.Height}ในที่นี้ r Rectangle คือ receiver Area กลายเป็น method ของ Rectangle เรียกได้ว่า rect.Area()
Value receiver vs Pointer receiver
หัวข้อที่มีชื่อว่า “Value receiver vs Pointer receiver”Receiver สามารถเป็น value หรือ pointer ก็ได้:
func (c Counter) Value() int { return c.count } // value receiverfunc (c *Counter) Inc() { c.count++ } // pointer receiverValue receiver (t T) | Pointer receiver (t *T) | |
|---|---|---|
| เห็น | สำเนาของค่า | ค่าต้นฉบับ |
| แก้ไขได้? | ไม่ได้ — แก้เฉพาะสำเนา | ได้ |
| เรียกได้จาก | ทั้ง T และ *T | ทั้ง T (addressable) และ *T |
| ใช้เมื่อ | อ่านอย่างเดียว; ขนาดเล็ก, copy ถูก | ต้องแก้ไข; struct ใหญ่; method set สม่ำเสมอ |
ความสม่ำเสมอ: ถ้า method ใด ๆ บน type ต้องการ pointer receiver ให้ใช้ pointer receiver กับ ทุก method ของ type นั้น การผสมกันทำให้เกิดปัญหา method set ที่ละเอียดอ่อนเมื่อ type ถูกใช้เป็น interface value
Methods บน named type ใดก็ได้
หัวข้อที่มีชื่อว่า “Methods บน named type ใดก็ได้”Methods ไม่จำกัดแค่ structs คุณนิยาม methods บน named type ใด ๆ ใน package ของคุณได้ — ยกเว้น pointer types และ interface types:
type Celsius float64
func (t Celsius) String() string { return fmt.Sprintf("%.1f°C", float64(t))}คุณไม่สามารถนิยาม method บน type จาก package อื่น — เช่น func (t time.Time) Weekday2() string { ... } ผิดกฎ ด้วย error cannot define new methods on non-local type time.Time — แต่การห่อ type นั้นใน named type ใน package ของคุณเองใช้ได้เสมอ
Method sets
หัวข้อที่มีชื่อว่า “Method sets”Method set ของ type กำหนดว่า interfaces ใดที่ตอบสนองได้:
- Method set ของ
Tมี methods ทั้งหมดที่มี value receiver(t T) - Method set ของ
*Tมี methods ทั้งหมด — ทั้ง(t T)และ(t *T)
นี่คือเหตุผลที่ interface ที่ต้องการ pointer-receiver method สามารถตอบสนองได้เฉพาะโดย *T ไม่ใช่ T
Addressability: เมื่อ method set กัดคุณ
หัวข้อที่มีชื่อว่า “Addressability: เมื่อ method set กัดคุณ”การเรียก pointer-receiver method บน value จะ auto-take address ให้เอง — c.Inc() บนตัวแปร c จริง ๆ แล้วหมายถึง (&c).Inc() แต่ Go จะ take address ได้ก็ต่อเมื่อ c เป็น addressable: ตัวแปรธรรมดา, field ของ struct ที่ addressable, หรือ *p สำหรับ pointer p ใด ๆ มี value สองแบบในชีวิตประจำวันที่ไม่ addressable — map value และผลลัพธ์ของ composite literal ที่ใช้ตรง ๆ
type Counter struct{ count int }
func (c *Counter) Inc() { c.count++ }
func main() { m := map[string]Counter{"a": {}} m["a"].Inc() // map values are not addressable}Compile ด้วย Go 1.26.5 ได้ผลลัพธ์:
./main.go:15:9: cannot call pointer method Inc on Counterเหตุผลคือเรื่องโครงสร้าง: map สามารถ rehash และย้าย entry ของมันได้ตลอดเวลา ดังนั้น Go จึงปฏิเสธที่จะให้ address ที่เสถียรเข้าไปในมัน ทางแก้คือเก็บเป็น pointer แทน (map[string]*Counter) หรือ copy ค่าออกมา แก้ไข แล้วเก็บกลับเข้าไป
error แบบเดียวกันเกิดกับ composite literal ที่ใช้ตรง ๆ ด้วย เพราะ literal ไม่มีตัวแปรให้ take address:
Counter{}.Inc()./main.go:12:12: cannot call pointer method Inc on CounterAddressability ยังตัดสิน interface satisfaction ด้วย method set ของ T มีเฉพาะ value-receiver methods ดังนั้นการ assign value ของ T ให้ interface ที่ต้องการ pointer-receiver method จะ fail ตั้งแต่ compile time ไม่ใช่ตอนเรียก:
type Incer interface{ Inc() }
var i Incer = Counter{}./main.go:16:16: cannot use Counter{} (value of struct type Counter) as Incer value in variable declaration: Counter does not implement Incer (method Inc has pointer receiver)var i Incer = &Counter{} compile ผ่าน เพราะ method set ของ *Counter มี Inc รวมอยู่ด้วย
Method value vs Method expression
หัวข้อที่มีชื่อว่า “Method value vs Method expression”Method value — c.Inc — คือ function ที่ผูกกับ receiver c ตัวที่กำหนด ณ ตอนที่คุณ take มันออกมา Method expression — Counter.Value — take type เองมา และ receiver จะกลายเป็น parameter ตัวแรกของ function:
c := &Counter{}inc := c.Inc // bound to c already — inc() means c.Inc()inc()
value := Counter.Value // unbound — receiver is the first argumentfmt.Println(value(*c))Method value มีประโยชน์เมื่อต้องส่ง bound method เป็น callback (http.HandleFunc("/x", h.serveX)); method expression พบไม่บ่อย ส่วนใหญ่เจอใน generic code ที่ต้องเรียก method บน underlying type ของ type parameter
Nil pointer receiver
หัวข้อที่มีชื่อว่า “Nil pointer receiver”การเรียก method ผ่าน nil pointer นั้นถูกกฎหมายใน Go ตราบใดที่ body ของ method ไม่ dereference field ที่เป็น nil ก่อนเช็ก นี่คือวิธีมาตรฐานในการเขียน recursive nil-safe method บนโครงสร้างแบบ linked:
type List struct { val int next *List}
func (l *List) Sum() int { if l == nil { return 0 } return l.val + l.next.Sum()}var l *List; l.Sum() คืนค่า 0 โดยไม่ panic — ตัว call method เองไม่ dereference l และ nil check ใน body ก็จัดการมันเอง
การตั้งชื่อ receiver
หัวข้อที่มีชื่อว่า “การตั้งชื่อ receiver”Receiver แบบ idiomatic ใน Go คือ 1-2 ตัวอักษรที่มาจากชื่อ type — c Counter, r *Rectangle — ไม่เคยใช้ this หรือ self ใช้ชื่อ receiver เดียวกันสำหรับทุก method บน type นั้น standard library ทำตามแบบนี้ตลอด (func (b *Buffer) WriteString, func (t Time) Add)
Method บน named function type
หัวข้อที่มีชื่อว่า “Method บน named function type”เพราะ named type ใด ๆ มี methods ได้ named function type ก็มีได้เหมือนกัน standard library ใช้สิ่งนี้เพื่อแปลง function ธรรมดาให้ตอบสนอง interface ได้ — http.HandlerFunc คือ func(http.ResponseWriter, *http.Request) ที่มี method ServeHTTP ซึ่งเป็นสิ่งเดียวที่ http.Handler ต้องการ:
type IntOp func(int) int
func (f IntOp) Twice(x int) int { return f(f(x))}IntOp(double).Twice(3) เรียก double บนผลลัพธ์ของตัวมันเอง — function เองคือ receiver
Copy mutex: กับดัก value receiver
หัวข้อที่มีชื่อว่า “Copy mutex: กับดัก value receiver”sync.Mutex ห้าม copy หลังจากใช้งานครั้งแรกเด็ดขาด เพราะ copy จะมี lock state เป็นของตัวเองแยกต่างหาก value receiver จะ copy ทั้ง struct ทุกครั้งที่เรียก — ดังนั้น value-receiver method บน struct ที่มี mutex จึง lock copy อย่างเงียบ ๆ โดยไม่ป้องกันอะไรเลย:
type Safe struct { mu sync.Mutex count int}
func (s Safe) Inc() { // value receiver — copies mu on every call s.mu.Lock() defer s.mu.Unlock() s.count++}go vet จับ bug นี้ได้ตรง ๆ:
main.go:11:9: Inc passes lock by value: command-line-arguments.Safe contains sync.Mutexทางแก้คือใช้ pointer receiver เสมอ: func (s *Safe) Inc() struct ใดที่มี sync.Mutex, sync.WaitGroup, หรือคล้ายกัน ต้องใช้ pointer receiver กับทุก method และห้าม pass หรือ return แบบ value เด็ดขาด
package main
import "fmt"
type Counter struct { count int}
// Pointer receiver — mutates the structfunc (c *Counter) Inc() { c.count++}
// Value receiver — read-onlyfunc (c Counter) Value() int { return c.count}
type Celsius float64
func (t Celsius) String() string { return fmt.Sprintf("%.1f\u00b0C", float64(t))}
// List demonstrates a nil pointer receiver: Sum handles a nil *List gracefully.type List struct { val int next *List}
func (l *List) Sum() int { if l == nil { return 0 } return l.val + l.next.Sum()}
// IntOp is a named function type with its own method.type IntOp func(int) int
func (f IntOp) Twice(x int) int { return f(f(x))}
func main() { c := &Counter{} c.Inc() c.Inc() c.Inc() fmt.Println(c.Value()) // 3
temp := Celsius(36.6) fmt.Println(temp.String()) // 36.6°C
// Method value: bound to c the moment it is created. inc := c.Inc inc() fmt.Println(c.Value()) // 4
// Method expression: receiver becomes an explicit first argument. value := Counter.Value fmt.Println(value(*c)) // 4
var empty *List fmt.Println(empty.Sum()) // 0 — nil receiver, handled inside Sum
list := &List{val: 1, next: &List{val: 2}} fmt.Println(list.Sum()) // 3
double := IntOp(func(x int) int { return x * 2 }) fmt.Println(double.Twice(3)) // 12} ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| สิ่งที่ได้ | ประโยชน์ | ต้นทุน |
|---|---|---|
| Value receiver | เข้าใจง่าย, เรียกได้บน value ที่ไม่ addressable | copy struct ทุกครั้งที่เรียก |
| Pointer receiver | mutate ได้, ไม่ copy | ต้อง nil-check, receiver ต้อง addressable |
| Method บน type ที่ไม่ใช่ struct | ขยาย behavior ของ named type ใด ๆ ใน package | เพิ่ม method บน type จาก package อื่นไม่ได้ |
| Method value | ผูก receiver เข้ากับ function ส่งเป็น callback ได้ง่าย | ทำให้ receiver มีชีวิตอยู่ตลอดอายุของ closure |
| Pointer receiver บน type ที่มี mutex | เป็นตัวเลือกที่ถูกต้องเพียงแบบเดียว | ทุก method — แม้แต่ read-only — ต้องรับ pointer |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- value receiver และ pointer receiver ใช้แทนกันได้ — value receiver ทำงานบนสำเนา ไม่สามารถ mutate ค่าต้นฉบับของ caller ได้
- สามารถเพิ่ม method บน type ของ package อื่นได้ — Go ไม่อนุญาต ต้อง wrap type นั้นด้วย named type ในเครื่อง หรือใช้ embedding แทน
Tและ*Tมี method set เหมือนกัน — method set ของ*Tใหญ่กว่า มัน include pointer-receiver methods ด้วย ซึ่งส่งผลต่อ interface satisfaction- map value ที่มี pointer-receiver method ก็ใช้ได้เหมือนตัวแปร struct ธรรมดา — ไม่จริง map value ไม่ addressable ดังนั้น
m[k].Inc()เป็น compile error ไม่ใช่ runtime surprise - copy struct ที่ embed
sync.Mutexไม่เป็นไรถ้ายังไม่รีบLock— copy นั้นพังตั้งแต่วินาทีที่มันถูกสร้างแล้วgo vetflag ที่ตัว copy เอง ไม่ใช่แค่ตอนเรียก lock
💡 ตัวอย่างจากของจริง
net/http:http.ResponseWriterเป็น interface —Handlerทุกตัวรับมันแทน concrete type ทำให้ test handler ได้ง่ายด้วย mockhttp.HandlerFuncเป็น named function type ที่มี methodServeHTTPดังนั้น function ธรรมดาก็ตอบสนองhttp.Handlerได้ด้วยการ convert แค่บรรทัดเดียว
*bytes.Bufferและ*http.Clientทั้งคู่ใช้ pointer receiver กับทุก method เพราะมี internal state ที่ method ต้อง mutate (buffered bytes, connection pool) — ความสม่ำเสมอของ receiver kind สำคัญตรงนี้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เมื่อ snippet ที่ generate มา นิยาม methods ให้เช็กสิ่งเหล่านี้ก่อนเชื่อมัน:
- ทุก method บน type ใช้ receiver kind เดียวกัน หรือเปล่า — value ทั้งหมด หรือ pointer ทั้งหมด? การผสมกันคือสัญญาณเตือน
- ถ้า method mutate field, receiver เป็น pointer หรือเปล่า? value-receiver mutation compile ผ่านแต่ไม่ทำอะไรเลยอย่างเงียบ ๆ
- struct ของ type นั้นมี
sync.Mutex,sync.WaitGroup, หรือคล้ายกันหรือเปล่า? ทุก method ต้องเป็น pointer receiver และ type ต้องไม่ถูก copy หรือ pass แบบ value เด็ดขาด - pointer-receiver method ถูกเรียกบน map value (
m[k].Method()) หรือ composite literal สด ๆ (T{}.Method()) หรือเปล่า? ทั้งสองแบบ compile ไม่ผ่าน — เป็นเรื่อง addressability ไม่ใช่ runtime bug - interface variable ถูก assign ด้วย value type (
var i Iface = T{}) ทั้งที่ method ที่ต้องการมี pointer receiver หรือเปล่า? นั่นคือ compile error ที่โค้ดที่ generate มาอาจไม่เคยถูกรันจริงเลย
🐛 หา bug ในโค้ดนี้
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func main() { m := map[string]Counter{"a": {}} m["a"].Inc()}เฉลย
m["a"] ไม่ addressable — map value ถูกย้ายได้ตลอดเวลาจาก rehash ดังนั้น Go จึงปฏิเสธที่จะ take address ให้กับการเรียก pointer-receiver แบบนี้ compiler message จริงคือ cannot call pointer method Inc on Counter แก้ได้โดยใช้ map[string]*Counter หรือโหลดค่าออกมา mutate แล้วเก็บกลับ: c := m["a"]; c.Inc(); m["a"] = c