1 · กฎข้อเดียวที่ต้องจำ
ใน Go ทุกอย่างเป็น pass by value 100% ไม่มีข้อยกเว้น
การใช้ pointer ไม่ได้ทำให้เป็น "pass by reference" — มันแค่เปลี่ยนจาก
ก็อปของ เป็น ก็อปที่อยู่ของของ
ก่อนอื่น — * กับ & อ่านยังไง
ตัวแปรทุกตัวใน Go นั่งอยู่ที่ "ช่องหนึ่งในหน่วยความจำ" และช่องนั้นมีเลขที่อยู่ของมันเอง เหมือนบ้านมีเลขที่ pointer คือตัวแปรที่เก็บ "เลขที่บ้าน" ของตัวแปรอีกตัว ไม่ได้เก็บตัวของมันเอง
| เขียนว่า | อ่านว่า | แปลว่า |
|---|---|---|
*int | "ชนิด pointer-to-int" | ใช้ตอนประกาศชนิด — ตัวแปรนี้เก็บที่อยู่ของ int ไม่ใช่เก็บ int |
&x | "แอมเปอร์แซนด์ x" / "address of x" | ขอที่อยู่ของ x ออกมา |
*n | "ดีเรฟเฟอเรนซ์ n" | ใช้ตอนเขียนคำสั่ง — เดินตามที่อยู่ใน n ไปหยิบ/แก้ของจริงที่ปลายทาง |
สังเกตว่า * มีสองหน้าที่คนละเรื่องกัน วิธีแยกคือ
ถ้า * ติดอยู่กับชื่อชนิด = ประกาศชนิด · ถ้าติดอยู่กับชื่อตัวแปร = เดินตามที่อยู่ไป
func tryValue(n int) { n = 999 }
func tryPtr(n *int) { *n = 999 }
x := 42
tryValue(x) // x ยังเป็น 42 — แก้ copy ของเลข
tryPtr(&x) // x เป็น 999 — แก้ผ่านที่อยู่
ตัว tryPtr ก็ได้ copy ของ pointer มาเหมือนกัน — แต่ copy ของ
ที่อยู่บ้าน ก็ยังชี้ไปบ้านหลังเดิม เลยเข้าไปแก้ของในบ้านได้
เอกสารต้นฉบับวางบนโต๊ะที่บ้าน เพื่อนขอดู เรามีสองทาง
· ถ่ายสำเนาให้ไป — เพื่อนขีดเขียนยังไงก็ได้ ต้นฉบับยังเหมือนเดิม =
tryValue(x)
· เขียนที่อยู่บ้านลงกระดาษแล้วยื่นให้ — เพื่อนเดินตามที่อยู่ไปแก้ต้นฉบับได้เลย
(&x = ขอที่อยู่ · *n = เดินตามที่อยู่ไปแก้ของจริง)
ที่ไม่เหมือนกัน: กระดาษที่ยื่นให้เพื่อนก็ยังเป็นสำเนาอยู่ดี —
ถ้าเพื่อนลบที่อยู่บนกระดาษตัวเองแล้วเขียนบ้านหลังอื่นแทน (โค้ดคือ n = &y)
บ้านเราไม่ได้ย้ายตาม นี่คือเหตุผลที่ Go ยืนยันว่าทุกอย่างเป็น pass by value —
สิ่งที่ก็อปไปคือกระดาษ ไม่ใช่บ้าน
- pass by value
- "ส่งสำเนาไป" — Go เป็นแบบนี้ทั้งหมด · เวลาแก้แล้วค่าไม่เปลี่ยน พูดว่า "ฟังก์ชันนี้รับแบบ pass by value ค่าข้างนอกเลยไม่ถูกแก้" จะสื่อกว่า "มันไม่ทำงาน" (pass by reference = ส่งตัวจริง เป็นของภาษาอื่น Go ไม่มี)
- pointer
- ตัวแปรที่เก็บ "ที่อยู่" ของตัวแปรอีกตัว · ถามว่า
"ตรงนี้ควรรับเป็น
*UserหรือUser" ชัดกว่าถาม "ควรส่งแบบไหน" - address-of (
&) / dereference (*n) - "ขอที่อยู่" กับ "เดินตามที่อยู่ไปหยิบของจริง" · มี pointer อยู่ในมือแล้วลืมใส่
*เรียกว่า "ลืม dereference" — พูดคำนี้แล้ว senior เข้าใจทันที
อาการที่เงียบจริง (คอมไพล์ผ่าน ไม่มี error):fmt.Println(n)พิมพ์ ที่อยู่ อย่าง0x5a53ea10020แทนที่จะเป็นเลข ·n1 == n2เทียบว่าชี้บ้านหลังเดียวกันไหม ไม่ได้เทียบค่า (ต้องเขียน*n1 == *n2)
ส่วนการเขียนn = 999ทั้งที่nเป็น*intไม่ใช่บั๊กเงียบ — compiler ดักให้ตั้งแต่แรกว่าcannot use 999 (untyped int constant) as *int value in assignment
2 · จุดที่กฎนี้เล่นงานคุณ #1: for range
type User struct{ Name string; Age int }
users := []User{{"Alice", 30}, {"Bob", 25}}
for _, u := range users {
u.Name = "CHANGED"
}
fmt.Println(users[0].Name)
พิมพ์ออกมาว่าอะไร?
Alice — ไม่เปลี่ยนเลย
ทุกรอบของ for range ตัวแปร u ได้ copy
ของ struct มา แก้ u ก็คือแก้ copy พอจบรอบ copy ก็ถูกทิ้ง
แก้ให้ถูกต้องใช้ index แทน:
for i := range users {
users[i].Name = "CHANGED" // ✓ เข้าถึงช่องจริงใน slice
}
มันเหมือนกันทุกประการครับ — for _, v := range nums { v = 0 }
ก็ไม่เปลี่ยนอะไรเหมือนกัน สิ่งที่ทำให้คนสับสนคือ
users[i].Name = "x" (ผ่าน index) กับ u.Name = "x"
(ผ่านตัวแปรวนลูป) หน้าตาคล้ายกันมาก แต่ตัวแรกแตะของจริง ตัวหลังแตะ copy
กลับไปที่ภาพเมื่อกี้: u คือสำเนาที่ถ่ายมาใหม่ทุกรอบ
ส่วน users[i] คือต้นฉบับที่ยังวางอยู่บนโต๊ะ
3 · จุดที่กฎนี้เล่นงานคุณ #2: struct ใน map
type Product struct{ Name string; Qty int }
inv := map[string]Product{
"SKU-1": {Name: "Mouse", Qty: 50},
}
inv["SKU-1"].Qty = 49
โค้ดนี้ผลลัพธ์เป็นยังไง?
compile ไม่ผ่าน — cannot assign to struct field
inv["SKU-1"].Qty in map
inv["SKU-1"] คืน copy ไม่ใช่ที่อยู่จริง Go เลย
ห้ามตั้งแต่ตอน compile ไม่ให้คุณเขียนโค้ดที่ดูเหมือนแก้ได้แต่ไม่มีผล
เหตุผลลึก: ข้างใน map ไม่ได้เรียงของไว้นิ่ง ๆ
พอของเยอะขึ้นถึงจุดหนึ่ง map จะขยายตัวแล้วย้ายทุก key ไปวางที่ใหม่หมด
(เรียกว่า rehash) แปลว่าที่อยู่ของ inv["SKU-1"] วันนี้กับพรุ่งนี้อาจคนละที่
→ Go เลยไม่ยอมให้เอาที่อยู่ของมันออกมาเลยตั้งแต่แรก เพราะที่อยู่ที่ขยับได้ใช้ไม่ได้จริง
→ ค่าที่ "เอาที่อยู่ออกมาไม่ได้" แบบนี้ Go เรียกว่า non-addressable
และ = ฝั่งซ้ายต้องเป็นของที่ addressable เสมอ
ค่าหนึ่ง ๆ เรียกว่า addressable ถ้าเอา & ไปแปะหน้ามันแล้วคอมไพล์ผ่าน
— แปลว่าค่านั้นมี "ที่อยู่ถาวร" ให้ชี้ได้ ลองในหัวง่าย ๆ ว่าเขียน &สิ่งนั้น ได้ไหม
เขียน & ได้ | เขียนไม่ได้ (non-addressable) |
|---|---|
&x — ตัวแปรธรรมดา | &42 — ค่าคงที่ ไม่มีบ้านอยู่ |
&s[0] — ช่องใน slice | &m["k"] — value ใน map |
&p.Name — field ของตัวแปร struct | &f() — ค่าที่ฟังก์ชันเพิ่งคืนมา |
เกณฑ์เดียวคือ ของสิ่งนั้นมีที่อยู่ที่ Go รับประกันว่าไม่ขยับไหม —
ตัวแปรมี, ช่องใน array/slice มี, แต่ค่าใน map ไม่มี เพราะ map ย้ายของได้ตอนโต
ภาพง่าย ๆ: map เหมือนร้านที่รื้อชั้นวางใหม่ทั้งร้านเวลาของเยอะขึ้น
จดไว้ว่า "นมอยู่ชั้น 3 ช่องซ้าย" แล้วเอากลับมาใช้พรุ่งนี้ไม่ได้
และเพราะ compiler ไม่มีทางรู้ล่วงหน้าว่าร้านจะรื้อชั้นตอนไหน Go เลย
ห้ามจดที่อยู่ตั้งแต่แรกทุกกรณี ต่อให้ map ก้อนนั้นจะไม่มีวันโตเลยก็ตาม —
&m["k"] คอมไพล์ไม่ผ่านเสมอ
(cannot take address of m["k"])
ทางออก 3 แบบ เรียงตามที่ใช้จริงบ่อยที่สุด:
// สมมติว่า Product เพิ่ม field Tags เข้ามาแล้ว:
// type Product struct{ Name string; Qty int; Tags []string }
// 1) เก็บเป็น pointer ตั้งแต่แรก — ใช้บ่อยสุดในงานจริง
inv := map[string]*Product{"SKU-1": {Name: "Mouse", Qty: 50}}
inv["SKU-1"].Qty = 49 // ✓ ได้
// 2) อ่านออกมา แก้ แล้วยัดกลับทั้งก้อน
p := inv["SKU-1"]
p.Qty = 49
inv["SKU-1"] = p // ✓ ได้
// 3) field ที่เป็น slice แก้ได้เลย แม้ map value จะเป็น non-addressable
// (ตรงนี้กลับมาใช้ map แบบไม่ pointer เพื่อให้เห็นประเด็นชัด ๆ)
inv2 := map[string]Product{"SKU-1": {Name: "Mouse", Qty: 50, Tags: []string{"old"}}}
inv2["SKU-1"].Tags[0] = "new" // ✓ ได้
inv2["SKU-1"].Qty = 49 // ✗ ยังไม่ได้ — compile error เหมือนเดิม
ข้อ 3 ดูขัดกับที่เพิ่งบอกไป แต่ไม่ขัดเลย เพราะ Tags เป็น slice
ซึ่งตัวมันเองก็เป็นแค่ header (ป้ายสรุปเล็ก ๆ ที่จดไว้ว่าของจริงกองอยู่ที่ไหน ยาวเท่าไหร่)
ที่ชี้ไป array ก้อนหนึ่ง (บทที่ 06)
Tags[0] จึงไม่ได้แตะตัว map เลย แต่แตะ array ที่อยู่นอก map ต่างหาก
— และช่องใน array นั้นมีที่อยู่คงที่ เลย addressable
เวลาอ่าน map จำไว้ว่า key ที่ไม่มีจะคืน zero value ไม่ใช่ error
(zero value = ค่าเริ่มต้นของชนิดนั้น ๆ เช่น int เป็น 0,
string เป็นข้อความว่าง, pointer เป็น nil)
ดังนั้น inv["ghost"].Qty ได้ 0 ซึ่งแยกไม่ออกจาก
"มีของแต่หมดสต็อก" → ใช้ p, ok := inv[k] ทุกครั้งที่สองกรณีนี้
มีความหมายต่างกัน
- addressable / non-addressable
- "มีที่อยู่ให้ชี้ได้ / ไม่มี" — ทดสอบคร่าว ๆ คือเขียน
&นำหน้าแล้วคอมไพล์ผ่านไหม (ยกเว้นท่า&Point{1, 2}ที่ Go เปิดช่องให้เป็นพิเศษ — ตัว composite literal ไม่ addressable แต่เขียนติด&แบบนี้ผ่าน) · เจอ error ที่ลงท้ายว่าin mapอย่างบรรทัดข้างบน ให้ถามว่า "อันนี้เพราะ map value เป็น non-addressable ใช่ไหม" — จบใน 1 ประโยค
(ถ้าเป็นcannot assign to ...เฉย ๆ ยังสรุปไม่ได้ว่าเรื่อง map — เขียนทับตัวอักษรในstringหรือทับค่าที่ฟังก์ชันเพิ่งคืนมา ก็ขึ้นข้อความนี้เหมือนกัน) - rehash
- ตอนที่ map โตแล้วย้ายของทุก key ไปวางที่ใหม่ · เป็นคำที่ใช้ตอบเหตุผล ว่าทำไม map value ถึง non-addressable
- zero value / comma-ok
- ค่าเริ่มต้นที่ Go ใส่ให้เอง กับ ท่า
v, ok := m[k]ที่รับสองค่า · พูดว่า "ตรงนี้ได้ zero value มา ไม่ได้แปลว่า key มีจริง ใช้ comma-ok ด้วยดีกว่า"
ท่าเดียวกันนี้ยังใช้กับ type assertion (v, ok := x.(T)) และ channel (v, ok := <-ch) ด้วย — เจอคำนี้ที่ไหนก็แปลว่า "ค่าที่สองบอกว่าของจริงมีไหม"
4 · จุดที่กฎนี้เล่นงานคุณ #3: receiver ของ method
Method ก็คือฟังก์ชันที่มี parameter พิเศษข้างหน้าชื่อ receiver
ต่างจากฟังก์ชันธรรมดาตรงวงเล็บชุดแรกก่อนชื่อเท่านั้น —
วงเล็บชุดนั้นเรียกว่า receiver และมันคือตัวบอกว่า
method นี้ "ห้อยอยู่กับ" ชนิดไหน เวลาเรียกจึงเขียนเป็น acc.Deposit(500)
func (u User) — value | func (u *User) — pointer | |
|---|---|---|
| ได้อะไรมา | copy ของ struct | ที่อยู่ของ struct จริง |
| แก้ field แล้วมีผลข้างนอก | ❌ ไม่มี | ✅ มี |
| struct ใหญ่ | ก็อปทั้งก้อนทุกครั้ง | ส่งแค่ 8 bytes |
| เหมาะกับ | getter (method ที่มีไว้อ่านค่าอย่างเดียว) | ทุกอย่างที่ต้องแก้ค่า |
type Account struct{ Balance float64 }
func (a Account) Show() float64 { return a.Balance }
func (a *Account) Deposit(x float64) { a.Balance += x }
acc := Account{Balance: 1000}
acc.Deposit(500) // Go แปลงให้เป็น (&acc).Deposit(500)
fmt.Println(acc.Show()) // 1500
acc ไม่ใช่ pointer แต่เรียก acc.Deposit() ได้ เพราะ Go
ใส่ & ให้อัตโนมัติเมื่อตัวแปร addressable —
เวลาเขียนจึงแทบไม่รู้สึกต่าง แต่ผลลัพธ์ต่างกันสิ้นเชิง
ต้องดูที่ประกาศ method เสมอ ไม่ใช่ดูที่จุดเรียก
· func (a Account) Show() — value receiver =
เราถือสลิปยอดคงเหลือที่ปริ้นออกมา อ่านยอดได้ครบ ไม่มีปัญหา
· func (a *Account) Deposit() — pointer receiver =
ทำรายการผ่านเลขบัญชี เงินเข้าบัญชีจริง
เอาปากกาไปเขียนเพิ่มยอดลงบนสลิปที่ถืออยู่ — ตัวเลขบนกระดาษเปลี่ยนจริง
แต่เงินในบัญชีไม่ขึ้นสักบาท นั่นคือการเขียน a.Balance += x
บน value receiver
ที่ไม่เหมือนกัน — และเป็นจุดอันตราย:
เขียนบนสลิปเรารู้ตัวทันทีว่าไม่มีประโยชน์ แต่ Go ยอมให้เขียนแบบนี้เงียบ ๆ
คอมไพล์ผ่าน รันผ่าน ไม่มี error มีแค่ค่าที่ไม่เปลี่ยน
อีกจุด: ถ้า field ข้างในเป็น map หรือ slice
ต่อให้เป็น value receiver ก็ยังแตะของจริงที่ปลายทางได้ — แต่ไม่เท่ากันทั้งสองชนิด:
map เพิ่ม/แก้ key ได้จริง · slice เขียนทับช่องเดิม
(s[0] = x) ได้จริง แต่ append ไม่ออกไปข้างนอก
เพราะ append ไปแก้ที่ตัว header (Len/Cap/Data) ซึ่งเป็นของที่ก็อปมา
(ดูเกณฑ์ข้อ slice ด้านล่าง และภารกิจข้อ 5 ท้ายบท)
- receiver
- วงเล็บชุดแรกก่อนชื่อ method — ตัวบอกว่า method นี้สังกัดชนิดไหน
- value receiver / pointer receiver
- รับ "สลิป" กับ รับ "เลขบัญชี" · คำที่ต้องใช้ให้ตรงที่สุดในบทนี้ — เวลาแก้แล้วค่าไม่เปลี่ยน ให้ถาม senior หรือพิมพ์ถาม AI ว่า "method นี้เป็น value receiver หรือ pointer receiver" จะได้คำตอบตรงจุดทันที ต่างจากถามว่า "ทำไมมันไม่เปลี่ยน"
- signature
- "หน้าตาของ method ทั้งชุด" = receiver + ชื่อ + parameter + ชนิดที่คืนกลับ
(เส้นที่ลากไว้ในภาพหัวข้อนี้) ·
"เปลี่ยน signature ไม่ได้ เดี๋ยวโค้ดที่เรียกอยู่พัง"
เกร็ด: ตอน Go เช็กว่า type หนึ่งตรงกับ interface ไหม มันเทียบแค่ ชื่อ + parameter + ชนิดที่คืนกลับ ไม่ได้เอา receiver มาเทียบด้วย - syntactic sugar
- "ท่าเขียนสั้นที่ภาษาแปลให้เอง" เช่น
acc.Deposit(500)ที่ Go เติม&ให้เป็น(&acc).Deposit(500)
เกณฑ์ตัดสินจาก Go Code Review Comments
นี่คือเกณฑ์ที่ reviewer จริงใช้ตัดสิน — ย่อจาก Go Code Review Comments · Receiver Type:
- ถ้า method ต้องแก้ค่า receiver → ต้องเป็น pointer (บังคับ)
- ถ้า receiver เป็น struct ใหญ่ หรือ array ใหญ่ → pointer มีประสิทธิภาพกว่า ("How large is large? Assume it's equivalent to passing all its elements as arguments")
- ถ้า receiver มี
sync.Mutexอยู่ข้างใน → ต้องเป็น pointer ไม่งั้น copy แล้ว lock ไม่ทำงาน (sync.Mutex= ตัวล็อกที่กันไม่ให้งานหลายชิ้นแก้ของก้อนเดียวกันพร้อมกัน · พอ copy ไปก็กลายเป็นล็อกคนละดอก เลยไม่ได้กันอะไรเลย) - ถ้า receiver เป็น map, func, channel → ไม่ต้องใช้ pointer
เพราะสามชนิดนี้ตัวมันเองเป็นแค่ header เล็ก ๆ ที่ชี้ไปโครงสร้างจริงข้างหลังอยู่แล้ว
(แบบเดียวกับ slice header ในบทที่ 06) ก็อปมันไปก็ยังแตะของจริงก้อนเดิมได้
ใส่
*เพิ่มก็ได้แค่ความยุ่งยากในการเขียน - ถ้าเป็น slice ก็เหตุผลเดียวกัน — ยกเว้นเมื่อ method นั้นต้อง
appendหรือเปลี่ยนความยาวของ slice เอง เพราะสองอย่างนั้นแก้ที่ตัว header (Len/Cap/Data) ซึ่งเป็นของที่ก็อปมา → ต้องใช้ pointer receiver ถึงจะมีผลข้างนอก - ถ้าเป็น struct เล็ก ไม่มี field ที่ต้องแก้ และเป็น value โดยธรรมชาติ
(แบบ
time.Time) → value receiver เหมาะกว่า - "Don't mix receiver types" — type เดียวกันเลือกแบบเดียวทั้งหมด
- "When in doubt, use a pointer receiver."
5 · ทดสอบความจำ
1. ข้อความไหนถูกต้องเกี่ยวกับการส่งค่าใน Go?
Go ไม่มี pass by reference เลย ส่ง slice = ก็อป slice header (24 bytes) ที่บังเอิญชี้ array เดิม จึงดูเหมือน by reference แต่กลไกคือ by value ทั้งหมด
พิสูจน์ได้: ฟังก์ชันที่รับ []int
append ข้างในแล้ว len ของตัวแปรข้างนอกไม่เปลี่ยน
เพราะแก้ได้แค่ header ที่ก็อปมา (Rob Pike
อธิบายไว้)
2. m := map[string]User{...} แล้ว m["a"].Age = 31 จะเกิดอะไรขึ้น?
Go เลือกที่จะ ห้ามตั้งแต่ compile เพราะถ้าปล่อยให้รันได้แล้ว ค่าไม่เปลี่ยน จะเป็นบั๊กที่หาไม่เจอ — map value เป็น non-addressable
3. struct มี sync.Mutex อยู่ข้างใน ควรใช้ receiver แบบไหน?
copy struct ที่มี mutex = ได้ mutex คนละตัว → lock ไม่ป้องกันอะไรเลย
เป็นบั๊ก concurrency ที่เงียบมาก go vet จะเตือนเรื่องนี้ให้
4. for _, u := range users แล้วแก้ u.Name ไม่มีผล เพราะอะไร?
range ก็อป element ใส่ตัวแปรวนลูปทุกรอบ ใช้ users[i].Name = ...
หรือประกาศ slice เป็น []*User ถ้าตั้งใจให้แก้ได้
6 · ลงมือทำ
ภารกิจ: แปลง Inventory ให้แก้ค่าได้จริง
- ใน Playground สร้าง
type Product struct{ Name string; Qty int }และinv := map[string]Product{...} - ลองเขียน
inv["a"].Qty = 5— อ่าน error ที่ compiler พ่นออกมาให้ครบ คุ้นหน้ามันไว้ - ซ่อมด้วยวิธี "อ่าน–แก้–ยัดกลับ"
- ซ่อมอีกรอบด้วย
map[string]*Productเทียบว่าโค้ดสั้นลงแค่ไหน - ประกาศชนิดใหม่ครอบ map เดิม แล้วเขียน method แบบ value receiver:
เรียกtype Inventory struct { items map[string]*Product } func (inv Inventory) Add(p *Product) { // ← value receiver inv.items[p.Name] = p }Addแล้วPrintln(len(inv.items))— ผลที่ควรเห็น: จำนวนเพิ่มขึ้นจริง ทั้งที่เป็น value receiver (เพราะinvที่ก็อปมา มี fielditemsที่เป็น map header ซึ่งยังชี้ตารางเดียวกันกับของเดิม — เหมือน slice header ในบทที่ 06 เป๊ะ ไม่ใช่ข้อยกเว้นของกฎ pass by value แต่อย่างใด) - ท้าทาย: เพิ่ม field
count intในInventoryแล้วให้Addทำinv.count++ด้วย เรียกAdd3 ครั้ง แล้วPrintln(inv.count)— ผลที่ควรเห็น: ได้ 0 ไม่ใช่ 3 อธิบายให้ได้ว่าทำไมitemsถึงเพิ่มได้แต่countเพิ่มไม่ได้ ทั้งที่อยู่ใน struct ก้อนเดียวกัน แล้วซ่อมด้วยการเปลี่ยนเป็นfunc (inv *Inventory) Add(...)
เวลาเห็นโค้ดที่ "แก้แล้วไม่เปลี่ยน" คุณจะไล่ได้ใน 3 คำถาม — ตัวนี้เป็น copy รึเปล่า? มัน addressable ไหม? receiver เป็น value หรือ pointer?
อ่านต่อจากต้นทาง
แหล่งหลักที่แนะนำ: Go Code Review Comments — หัวข้อ Receiver Type เป็นเกณฑ์ที่ทีม Go ใช้ review จริง ยาวไม่ถึงหน้าเดียว ควรอ่านให้จบทั้งหน้าเลยเพราะหัวข้ออื่น (Indent Error Flow, Don't Panic) จะเจอในคอร์สวันถัด ๆ ไปแน่นอน
💬 ติดตรงไหนถามได้เลย — โดยเฉพาะข้อ 6 ของภารกิจ ถ้าอธิบายไม่ได้ว่าทำไม value receiver ถึงพัง ให้ถามมาเลย นั่นคือจุดที่ความเข้าใจ กำลังจะแน่นพอดี
อ้างอิง
- Go Code Review Comments — Receiver Type — เกณฑ์เลือก value vs pointer receiver
- The mechanics of 'append' — "even though a slice contains a pointer, it is itself a value"
- Go spec — Address operators — นิยามของ addressable