ข้ามไปยังเนื้อหา

Methods

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()

Receiver สามารถเป็น value หรือ pointer ก็ได้:

func (c Counter) Value() int { return c.count } // value receiver
func (c *Counter) Inc() { c.count++ } // pointer receiver
Value 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 ไม่จำกัดแค่ 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 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

การเรียก 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 Counter

Addressability ยังตัดสิน 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 — 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 argument
fmt.Println(value(*c))

Method value มีประโยชน์เมื่อต้องส่ง bound method เป็น callback (http.HandleFunc("/x", h.serveX)); method expression พบไม่บ่อย ส่วนใหญ่เจอใน generic code ที่ต้องเรียก method บน underlying type ของ type parameter

การเรียก 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 แบบ idiomatic ใน Go คือ 1-2 ตัวอักษรที่มาจากชื่อ type — c Counter, r *Rectangle — ไม่เคยใช้ this หรือ self ใช้ชื่อ receiver เดียวกันสำหรับทุก method บน type นั้น standard library ทำตามแบบนี้ตลอด (func (b *Buffer) WriteString, func (t Time) Add)

เพราะ 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

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 struct
func (c *Counter) Inc() {
c.count++
}
// Value receiver — read-only
func (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 ที่ไม่ addressablecopy struct ทุกครั้งที่เรียก
Pointer receivermutate ได้, ไม่ 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 vet flag ที่ตัว copy เอง ไม่ใช่แค่ตอนเรียก lock

💡 ตัวอย่างจากของจริง

net/http: http.ResponseWriter เป็น interface — Handler ทุกตัวรับมันแทน concrete type ทำให้ test handler ได้ง่ายด้วย mock http.HandlerFunc เป็น named function type ที่มี method ServeHTTP ดังนั้น function ธรรมดาก็ตอบสนอง http.Handler ได้ด้วยการ convert แค่บรรทัดเดียว

*bytes.Buffer และ *http.Client ทั้งคู่ใช้ pointer receiver กับทุก method เพราะมี internal state ที่ method ต้อง mutate (buffered bytes, connection pool) — ความสม่ำเสมอของ receiver kind สำคัญตรงนี้

เมื่อ 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

method ที่มี value receiver (c Counter) แก้ไข c.count จะเกิดอะไรกับตัวแปรต้นฉบับ?
method set ของ type ใดที่รวม methods ทั้ง value-receiver และ pointer-receiver?
โค้ดนี้ compile ผ่านไหม?
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }

func main() {
    m := map[string]Counter{"a": {}}
    m["a"].Inc()
}
go vet รายงานอะไร?
type Safe struct {
    mu    sync.Mutex
    count int
}
func (s Safe) Inc() {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.count++
}
คุณต้องการนิยาม method บน type Kelvin float64 มีข้อจำกัดอะไร?
inc() จะเพิ่มค่าให้ Counter ตัวไหน?
c := &Counter{}
inc := c.Inc
c = &Counter{}
inc()