ชนิดข้อมูล zero value และ struct
บทที่แล้วจบที่ hello world บทนี้ orderctl เริ่มมีของจริง
เราจะสร้าง package internal/order ที่เก็บชนิดข้อมูลของออเดอร์ทั้งใบ แล้วเขียนเทสต์ตัวแรก
ให้มัน ระหว่างทางมีสามจุดที่คนย้ายมาจาก C#, Java หรือ TypeScript เดาผิดเกือบทุกครั้ง
ประกาศตัวแปรแล้วได้ค่าใช้ได้ทันที
หัวข้อที่มีชื่อว่า “ประกาศตัวแปรแล้วได้ค่าใช้ได้ทันที”Go ไม่มีสถานะ “ยังไม่กำหนดค่า” ตัวแปรที่ประกาศเปล่าจึงแตะ field ได้เลยโดยไม่ panic
zero valuezero valueค่าที่ตัวแปรได้ทันทีที่ประกาศโดยไม่กำหนดค่า — `0` `false` `""` หรือ `nil` ตามชนิด Go ไม่มีสถานะ "ยังไม่กำหนดค่า" จึงไม่มี NullReferenceException แบบที่ C# มี คือค่าที่ตัวแปรได้ทันทีที่ประกาศโดยไม่กำหนดค่า มันไม่ใช่ค่าขยะที่ค้างอยู่ใน หน่วยความจำ และไม่ใช่สถานะพิเศษที่ต้องเช็กก่อนใช้
package main
import "fmt"
type Line struct { SKU string Qty int Note *string}
func main() { var l Line fmt.Printf("%q %d %v\n", l.SKU, l.Qty, l.Note == nil) fmt.Printf("%+v\n", l)}$ go run ."" 0 true{SKU: Qty:0 Note:<nil>}l.SKU เป็นสตริงว่างจริงๆ ไม่ใช่ nil ที่แกล้งเป็นสตริง l.Qty เป็น 0 จริงๆ
ส่วน l.Note เป็น pointer จึงเป็น nil เพราะนั่นคือ zero value ของ pointer
ค่าเริ่มต้นตามชนิดมีไม่กี่แบบ จำได้ทั้งชุดในบรรทัดเดียว
- ชนิดตัวเลขทุกตัว เป็น
0 boolเป็นfalsestringเป็นสตริงว่าง""- pointer, slice, map, channel, function และ interface เป็น
nil
ใน C# หรือ Java การประกาศตัวแปรของ reference type แล้วแตะสมาชิกทันที คือ NullReferenceException ที่รอเกิด คุณจึงติดนิสัยเช็ก null ก่อนแตะทุกครั้ง
ใน Go var o Order ให้ struct ที่มีค่าจริงมาเลย ทุก field ถูกตั้งเป็น zero value ของ
ชนิดตัวเอง และสเปกเขียนไว้ว่าการตั้งค่านี้ทำแบบเรียกซ้ำลงไปทุกชั้น
นั่นลบ bug ทั้งตระกูลหนึ่งทิ้ง แต่สร้างอีกตระกูลขึ้นมาแทน — ค่าที่ผิดจะไม่ร้อง มันจะเงียบ
และเป็น 0 ออเดอร์ที่ลืมตั้ง Table จึงกลายเป็นออเดอร์ของโต๊ะศูนย์โดยไม่มีใครรู้
ข้อแลกนี้เปลี่ยนวิธีออกแบบชนิด ถ้า 0 เป็นค่าที่ผิดสำหรับ field ไหน คุณต้องมีจุดตรวจ
ของตัวเอง ไม่มี compiler มาช่วย บทที่ 6 จะสร้างจุดตรวจนั้นให้ orderctl
ชนิดใหม่ ไม่ใช่ชื่อเล่น
หัวข้อที่มีชื่อว่า “ชนิดใหม่ ไม่ใช่ชื่อเล่น”
type Baht int64สร้างชนิดที่ระบบชนิดแยกจากint64จริง แปลงชัดทุกครั้ง และผูก methodได้
เงินเก็บเป็น float64 ไม่ได้ orderctl จึงเก็บเป็นสตางค์ในจำนวนเต็ม แต่ถ้าเก็บเป็น int64
เปล่าๆ มันจะบวกกับ int64 ตัวอื่นในโปรแกรมได้เงียบสนิท ไม่ว่าอีกฝั่งจะเป็นส่วนลด
จำนวนวินาที หรือรหัสอะไรก็ตาม
defined typedefined typeชนิดใหม่ที่สร้างด้วย `type A B` ระบบชนิดแยกจาก `B` ต้องแปลงชัดทุกครั้ง และไม่รับ method ของ `B` มาด้วย แก้ข้อนี้ให้ ชื่อใหม่ที่สร้างด้วย type A B ไม่ใช่คำพ้องของ B
มันคือชนิดใหม่ที่ระบบชนิดแยกออก
package main
import "fmt"
// Baht เก็บเงินเป็นสตางค์ ไม่ใช่ float64type Baht int64
func (b Baht) String() string { sign := "" if b < 0 { // การหารและ mod ของ Go ตัดเข้าหาศูนย์ ทั้งสองผลจึงติดลบพร้อมกัน sign, b = "-", -b } return fmt.Sprintf("%s%d.%02d บาท", sign, b/100, b%100)}
func main() { var unit Baht = 6000 qty := 2 total := unit * Baht(qty) // Baht(qty) ละไม่ได้ fmt.Println(total)}$ go run .120.00 บาทBaht(qty) ตรงนั้นละไม่ได้ ถ้าเขียน unit * qty เฉยๆ compiler ตอบว่า
invalid operation: unit * qty (mismatched types Baht and int) หัวข้อ Operators ของสเปก
บังคับว่าตัวถูกดำเนินการสองฝั่งต้องเป็นชนิดเดียวกัน ยกเว้นการเลื่อนบิตและค่าคงที่ที่ยังไม่มีชนิด
unit * 2 จึงคอมไพล์ผ่าน เพราะ 2 เป็นค่าคงที่ที่ยังไม่มีชนิด compiler แปลงให้เป็น Baht
ให้เอง ส่วน qty เป็นตัวแปรที่มีชนิดแล้ว จึงไม่มีการแปลงให้ฟรี
เปลี่ยน qty เป็น int64 ก็ยังไม่ผ่าน ทั้งที่ int64 คือชนิดฐานของ Baht เพราะหัวข้อ
Assignability ยอมข้ามชื่อให้ต่อเมื่อทั้งสองฝั่งมีชนิดฐานตรงกัน และ อย่างน้อยหนึ่งฝั่ง
ไม่ใช่ named type — Baht กับ int64 เป็น named type ทั้งคู่
ความน่ารำคาญตรงนี้คือสิ่งที่คุณจ่าย และสิ่งที่ได้กลับมาคือการเอาเงินไปบวกกับจำนวนชิ้น โดยไม่ตั้งใจนั้น ทำไม่ได้เลยตั้งแต่ตอนคอมไพล์
เครื่องหมาย = ตัวเดียวเปลี่ยนความหมายทั้งหมด type Baht = int64 คือ alias —
เป็นชื่อที่สองของชนิดเดิม ไม่ใช่ชนิดใหม่
alias ไม่แยกจากชนิดเดิมในสายตาระบบชนิด และคุณผูก methodกับมันไม่ได้ เพราะ methodต้องผูก กับชนิดที่นิยามใน package เดียวกัน
ถ้าคุณมาจาก TypeScript ที่ type A = B คือเครื่องมือปกติ ระวังข้อนี้ให้มาก
สิ่งที่คุณต้องการเกือบทุกครั้งใน Go คือแบบไม่มี =
method String() ที่ผูกกับ Baht ทำให้ fmt.Println พิมพ์ 120.00 บาท แทนเลข 12000
นี่คือของแถมที่ alias ให้ไม่ได้
การหารจำนวนเต็มกับ % ของ Go ตัดเข้าหาศูนย์ ทั้งคู่ · -12345 / 100 จึงได้ -123
และ -12345 % 100 ได้ -45 ติดลบตามกัน
เขียนตรงๆ ว่า fmt.Sprintf("%d.%02d", b/100, b%100) จะได้ -123.-45 ซึ่งไม่ใช่จำนวนเงิน
· ภาษาที่ % คืนค่าไม่ติดลบเสมอ อย่าง Python จะไม่มีปัญหานี้ Go ไม่ใช่ภาษานั้น
orderctl ไม่เคยผลิตยอดติดลบ เพราะกฎส่วนลดหยุดที่ศูนย์ · แต่ String เป็น method
ที่คนอื่นเรียกได้ทุกเมื่อ และค่าที่ยังไม่ผ่านการตรวจก็ติดลบได้
ค่าคงที่ชุดเดียวกันด้วย iota
หัวข้อที่มีชื่อว่า “ค่าคงที่ชุดเดียวกันด้วย iota”Go ไม่มี enum แต่มี const block กับตัวนับที่ทำงานแทนได้ พร้อมหลักว่าค่า 0 ต้องปลอดภัย
iotaiotaตัวนับที่รีเซ็ตเป็น 0 ที่ต้นทุก block `const` และเพิ่มทีละหนึ่งต่อบรรทัด ใช้ตั้งค่าคงที่ชุดที่เรียงกัน คือตัวนับที่รีเซ็ตเป็น 0 ที่ต้นทุก block const แล้วเพิ่มทีละหนึ่งต่อบรรทัด
package main
import ( "fmt" "strconv")
type Status int
const ( StatusDraft Status = iota // 0 และเป็น zero value ของ Status ด้วย StatusPaid StatusCancelled)
var statusName = [...]string{"draft", "paid", "cancelled"}
func (s Status) String() string { if s < 0 || int(s) >= len(statusName) { return "Status(" + strconv.Itoa(int(s)) + ")" } return statusName[s]}
func main() { var s Status fmt.Println(s, StatusPaid, StatusCancelled, Status(9))}$ go run .draft paid cancelled Status(9)สองบรรทัดล่างของ const ไม่ต้องเขียน Status = iota ซ้ำ block const ทำซ้ำนิพจน์ของ
บรรทัดก่อนหน้าให้เอง เมื่อบรรทัดนั้นเว้นค่าไว้
ลำดับในนี้ไม่ได้เรียงตามความสวยงาม StatusDraft อยู่บนสุดเพราะมันจะได้ค่า 0
และ 0 คือ zero value ของ Status ทุกตัวที่ยังไม่ตั้งค่า
var o Order ให้ o.Status เป็น 0 โดยอัตโนมัติ ถ้าคุณเผลอวาง StatusPaid ไว้บรรทัดแรก
ออเดอร์ที่ยังไม่มีใครแตะจะกลายเป็นออเดอร์ที่จ่ายเงินแล้ว
นี่ไม่ใช่ข้อควรระวังทางทฤษฎี มันคือกฎการออกแบบที่ใช้กับทุก const block ที่ผูกกับ struct
ค่า 0 ต้องแปลว่า “ยังไม่เกิดอะไรขึ้น” เสมอ
String() ที่เขียนเองรับค่านอกช่วงด้วย ถ้าใครแปลง Status(9) เข้ามา มันคืน
Status(9) แทนที่จะ panic ตอนอ่าน statusName[9]
คนที่มาจาก C# หรือ Java คาดว่าเขียน String() แล้ว JSON จะได้ชื่อสถานะไปด้วย ลองจริง
แล้วผลไม่ใช่อย่างนั้น
package main
import ( "encoding/json" "fmt")
type Status int
const ( StatusDraft Status = iota StatusPaid)
func (s Status) String() string { return [...]string{"draft", "paid"}[s] }
func main() { b, err := json.Marshal(map[string]Status{"status": StatusPaid}) if err != nil { panic(err) } fmt.Println(string(b)) fmt.Println(StatusPaid)}$ go run .{"status":1}paidString() มีผลกับ fmt เท่านั้น encoding/json มองหา MarshalJSON ของ json.Marshaler
หรือ MarshalText ของ encoding.TextMarshaler แทน ไม่มีตัวไหนเกี่ยวกับ String() เลย
struct ของออเดอร์
หัวข้อที่มีชื่อว่า “struct ของออเดอร์”ชนิดที่สร้างมาทั้งหมดประกอบกันเป็น
internal/order/order.goของorderctl
Line คือหนึ่งรายการอาหาร Order คือใบเดียวที่ถือหลายรายการ ทั้งคู่เป็น struct ธรรมดา
ที่ไม่มี class ฐาน ไม่มี constructor และไม่มีอะไรซ่อนอยู่
// Package order เก็บชนิดข้อมูลของออเดอร์package order
import ( "fmt" "strconv")
// Baht เก็บเงินเป็นสตางค์ ไม่ใช่ float64type Baht int64
func (b Baht) String() string { sign := "" if b < 0 { // การหารและ mod ของ Go ตัดเข้าหาศูนย์ ทั้งสองผลจึงติดลบพร้อมกัน sign, b = "-", -b } return fmt.Sprintf("%s%d.%02d บาท", sign, b/100, b%100)}
// Status คือสถานะของออเดอร์ ค่า 0 คือ StatusDrafttype Status int
const ( StatusDraft Status = iota StatusPaid StatusCancelled)
var statusName = [...]string{"draft", "paid", "cancelled"}
func (s Status) String() string { if s < 0 || int(s) >= len(statusName) { return "Status(" + strconv.Itoa(int(s)) + ")" } return statusName[s]}
// Line คือหนึ่งรายการในออเดอร์type Line struct { SKU string Category string Qty int Unit Baht}
// Order คือออเดอร์หนึ่งใบtype Order struct { ID string Table int Lines []Line Status Status}
// Subtotal ใช้ value receiver เพราะอ่านอย่างเดียวfunc (o Order) Subtotal() Baht { var sum Baht for _, l := range o.Lines { sum += l.Unit * Baht(l.Qty) // Baht(l.Qty) ละไม่ได้ } return sum}
// AddLine ใช้ pointer receiver เพราะแก้ค่าใน receiverfunc (o *Order) AddLine(l Line) { o.Lines = append(o.Lines, l) }ชื่อที่ขึ้นต้นด้วยตัวใหญ่มองเห็นได้จาก package อื่น ชื่อที่ขึ้นต้นด้วยตัวเล็กมองไม่เห็น
statusName จึงเป็นของภายในที่ไม่มีใครนอก package แตะได้
method ใน Go ไม่ได้อยู่ข้างในวงเล็บปีกกาของ struct มันเป็นฟังก์ชันที่มีตัวรับเพิ่มมาข้างหน้าชื่อ ผลคือคุณผูก methodกับชนิดไหนก็ได้ที่นิยามใน package เดียวกัน ไม่จำกัดแค่ struct
receiver สองแบบ และเส้นแบ่งระหว่างมัน
หัวข้อที่มีชื่อว่า “receiver สองแบบ และเส้นแบ่งระหว่างมัน”
Subtotalรับสำเนาแล้วอ่านอย่างเดียวAddLineรับที่อยู่เพราะต้องแก้ของจริง
Go ส่งทุกอย่างแบบคัดลอกค่า value receiver จึงได้ struct ก้อนใหม่มาทั้งใบ การแก้อะไรในนั้น หายไปพร้อมสำเนาเมื่อ methodจบ
pointer receiverpointer receivermethod ที่รับ `*T` แก้ค่าใน receiver ได้ และทำให้ค่าแบบ value ไม่เข้ากับ interface ที่มี method นั้น รับที่อยู่ของตัวจริงแทน การแก้จึงถึงตัวจริง ลองเทียบสองแบบตรงๆ
package main
import "fmt"
type Line struct { SKU string Qty int}
type Order struct { ID string Lines []Line}
// ❌ แบบที่ผิด: value receiver ได้สำเนามา แก้แล้วไม่ถึงตัวจริงfunc (o Order) addLineBroken(l Line) { o.Lines = append(o.Lines, l) }
// pointer receiver: แก้ตัวจริงfunc (o *Order) AddLine(l Line) { o.Lines = append(o.Lines, l) }
func main() { o := Order{ID: "A1"} o.addLineBroken(Line{SKU: "phad-thai", Qty: 1}) fmt.Println(len(o.Lines)) o.AddLine(Line{SKU: "phad-thai", Qty: 1}) fmt.Println(len(o.Lines))}$ go run .01addLineBroken ไม่แจ้งเตือนอะไรเลย มันคอมไพล์ผ่าน รันผ่าน และไม่ทำอะไรที่มองเห็นได้
นี่คือรูปแบบความผิดพลาดที่เงียบที่สุดของ Go
flowchart TB
A["o := Order{ID: A1}<br/>ตัวจริงอยู่ในตัวแปร o"] --> B["เรียก o.addLineBroken(l)<br/>value receiver"]
A --> C["เรียก o.AddLine(l)<br/>pointer receiver"]
B --> D["Go คัดลอก Order ทั้งก้อน<br/>method แก้สำเนา"]
D --> E["สำเนาถูกทิ้งเมื่อ methodจบ<br/>o ไม่เปลี่ยน"]
C --> F["Go ส่งที่อยู่ของ o ให้เอง<br/>method แก้ที่เดียวกับตัวจริง"]
F --> G["o เปลี่ยนจริง"]
คำบรรยายภาพ: การเรียกผ่าน value receiver ทำให้ Go คัดลอก Order ทั้งก้อน method จึงแก้สำเนาที่ถูกทิ้งเมื่อจบ · การเรียกผ่าน pointer receiver ส่งที่อยู่ของตัวจริงให้ การแก้จึงอยู่กับ o ต่อไป
สังเกตว่าเราเขียน o.AddLine(...) ไม่ใช่ (&o).AddLine(...) เมื่อตัวแปรหาที่อยู่ได้
Go เติม & ให้เอง ทิศกลับกันก็ทำให้ method แบบ value เรียกผ่าน pointer ได้เช่นกัน
Go Code Review Comments ให้หลักไว้สั้นๆ ว่า “when in doubt, use a pointer receiver” และ “Don’t mix receiver types”
ข้อหลังสำคัญกว่าที่คิด method ที่รับ pointer ทำให้ method set ของ Order กับ *Order
ต่างกัน AddLine จึงอยู่ใน method set ของ *Order เท่านั้น
การปนสองแบบทำให้คุณเผลอคิดว่า
Order มี methodครบ เพราะบาง methodเรียกจากค่าธรรมดาได้จริง แล้วบทที่ 5 คุณจะเจอว่าค่าแบบ
value ไม่เข้ากับ interface ที่คุณเพิ่งประกาศ ทั้งที่ methodมีครบ
คอร์สนี้ปนสองแบบใน Order โดยตั้งใจ เพื่อให้บทที่ 5 มีของจริงให้ชี้
เทสต์ตัวแรก ไม่มี library เพิ่ม
หัวข้อที่มีชื่อว่า “เทสต์ตัวแรก ไม่มี library เพิ่ม”ตัวรันเทสต์อยู่ในกล่องเดียวกับ compiler และสำนวนมาตรฐานคือเทียบ
gotกับwantเอง
เทสต์ของ Go อยู่ใน file ที่ลงท้ายด้วย _test.go ในไดเรกทอรีเดียวกับของที่มันทดสอบ
ฟังก์ชันต้องขึ้นต้นด้วย Test และรับ *testing.T ตัวเดียว
// ชิ้นส่วน ไม่ใช่โปรแกรมสมบูรณ์// internal/order/order_test.go — อยู่ package order เดียวกับ order.go ข้างบนpackage order
import "testing"
func TestSubtotal(t *testing.T) { o := Order{Lines: []Line{{SKU: "phad-thai", Qty: 2, Unit: 6000}}} if got, want := o.Subtotal(), Baht(12000); got != want { t.Errorf("Subtotal() = %v, want %v", got, want) }}
func TestAddLine(t *testing.T) { var o Order o.AddLine(Line{SKU: "tom-yum", Qty: 1, Unit: 12000}) if got, want := len(o.Lines), 1; got != want { t.Errorf("len(Lines) = %d, want %d", got, want) }}$ go test ./internal/orderok example.com/orderctl/internal/order 0.150sไม่มี assertThat ไม่มี Should().Be() มีแค่ if กับ t.Errorf และนี่คือสำนวนที่
standard library เองใช้ ไม่ใช่ทางลัดสำหรับผู้เริ่มต้น
ชื่อตัวแปร got กับ want เป็นธรรมเนียมที่ทั้งชุมชนใช้ ข้อความความผิดพลาดจึงอ่านตรงกัน
ทุก repository และสมองคุณไม่ต้องแปลใหม่ทุกครั้ง
TestAddLine เริ่มด้วย var o Order ได้เลย เพราะ zero value ของ Order มี Lines
เป็น nil slice และ append ทำงานกับ nil slice ได้ตามปกติ
t.Errorf บันทึกว่าเทสต์นี้ล้ม แล้ว เดินต่อ ในฟังก์ชันเดิม เหมาะกับกรณีที่ยังตรวจข้ออื่นได้อีก
t.Fatalf บันทึกแล้ว หยุดฟังก์ชันนั้นทันที ใช้เมื่อบรรทัดถัดไปจะ panic แน่ๆ ถ้าเดินต่อ
เช่นตอนที่เพิ่งได้ error กลับมาแทนค่าที่ต้องใช้
เลือกผิดข้างไหนก็เสียอย่าง Errorf ที่ควรเป็น Fatalf ทำให้ผลกลายเป็น panic ที่อ่านยาก
ส่วน Fatalf ที่ควรเป็น Errorf ปิดบังความผิดพลาดข้ออื่นที่อยู่ถัดไป
กับดัก: ค่าใน map แก้ field ตรงๆ ไม่ได้
หัวข้อที่มีชื่อว่า “กับดัก: ค่าใน map แก้ field ตรงๆ ไม่ได้”
orders["A1"].Status = StatusPaidเป็นความผิดพลาดตอนคอมไพล์ ไม่ใช่ตอนรัน
orderctl จะเก็บออเดอร์หลายใบใน map ในบทหลัง และบรรทัดแรกที่ทุกคนเขียนคือบรรทัดที่ผิด
package main
import "fmt"
type Status int
const ( StatusDraft Status = iota StatusPaid)
type Order struct { ID string Status Status}
var orders = map[string]Order{"A1": {ID: "A1"}}
func main() { // orders["A1"].Status = StatusPaid // cannot assign to struct field orders["A1"].Status in map
o := orders["A1"] // สมาชิกของ map ไม่ addressable ต้องคัดออกมา o.Status = StatusPaid orders["A1"] = o // แล้วเขียนกลับ fmt.Println(orders["A1"].Status == StatusPaid)}$ go run .trueเหตุผลอยู่ที่ map ย้ายค่าของตัวเองได้เมื่อมันโต ที่อยู่ของสมาชิกจึงไม่คงที่ และภาษาไม่ยอมให้ คุณถือที่อยู่นั้นไว้ compiler เลยปฏิเสธตั้งแต่ต้น
ทางออกมีสองทางที่ใช้จริง เลือกตามว่าคุณอยากให้คนอ่านเห็นอะไร
- คัดค่าออกมาแก้แล้วเขียนกลับ ชัดเจนว่ามีการเขียนกลับ และค่าใน map ยังเป็น struct เต็มใบ
- เก็บเป็น
map[string]*Orderแทน แก้ผ่าน pointer ได้ตรงๆ แต่ต้องรับผิดชอบเรื่องค่าnilและเรื่องที่หลายจุดถือของชิ้นเดียวกันอยู่
orderctl เลือกทางแรกเสมอ เพราะข้อมูลออเดอร์ในคอร์สนี้เดินทางข้าม package บ่อย
และการแชร์ pointer ทำให้บทที่ 8 เรื่องทำงานพร้อมกันยากขึ้นโดยไม่จำเป็น
สรุปก่อนไปต่อ
หัวข้อที่มีชื่อว่า “สรุปก่อนไปต่อ”
internal/orderมีชนิดครบและมีเทสต์แล้ว แต่ยังอ่านออเดอร์จากที่ไหนไม่ได้
คุณสร้างชนิดโดเมนของตัวเองแล้ว รู้ว่าทำไม Baht ต้องเป็นชนิดใหม่ ไม่ใช่ int64 เปล่าๆ
และรู้ว่าทำไมบรรทัดแรกของ const block ต้องเป็นสถานะที่ปลอดภัย
คุณเลือก receiver ได้ด้วยเหตุผล ไม่ใช่ด้วยความเคยชิน และเขียนเทสต์ที่ไม่พึ่ง library ใดเลย
บทหน้า orderctl จะอ่านออเดอร์จริงจาก JSON บน disk แล้วพิมพ์ยอดออกมา ตรงนั้นคุณจะเจอ
struct tag, การห่อความผิดพลาดด้วย %w และ defer ที่ปิดของให้ทุกทางออก
บทนี้อิงต้นทางที่ลงวันที่กำกับ อ่านต่อได้โดยตรง:
- The Go Programming Language Specification (Language version go1.27, 2026-05-26) — หัวข้อ The zero value, Type definitions, Iota และ Method sets
- Go Specification — Operators (Language version go1.27, 2026-05-26) — “For other binary operators, the operand types must be identical unless the operation involves shifts or untyped constants.”
- Go Specification — Assignability (Language version go1.27, 2026-05-26) — ข้อที่ยอมข้ามชื่อให้ ต้องการทั้งชนิดฐานที่ตรงกัน และอย่างน้อยหนึ่งฝั่งที่ไม่ใช่ named type
- Go Code Review Comments — receiver type (เข้าถึง 2026-08-27) — “when in doubt, use a pointer receiver” และ “Don’t mix receiver types”
เช็กความเข้าใจ — บทที่ 2
ข้อ 1 / 3ประกาศ `var o Order` แล้วอ่าน field ของมันทันที เกิดอะไรขึ้น