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

Structs

Struct คือการรวม field ที่เกี่ยวข้องกันไว้ภายใต้ชื่อ type เดียว แต่ละ field มีชื่อและ type:

type Point struct {
X, Y int
}
type User struct {
Name string
Email string
Age int
}

Field ที่ขึ้นต้นด้วยตัวพิมพ์ใหญ่จะถูก export (มองเห็นได้จากภายนอก package) ส่วน field ที่ขึ้นต้นด้วยตัวพิมพ์เล็กจะเป็น unexported

คุณสามารถสร้างค่า struct ได้สองแบบ:

// Positional — ต้องเรียงตามลำดับ field ที่ประกาศไว้
p := Point{3, 4}
// Named fields — ลำดับไม่สำคัญ; field ที่ไม่ได้กำหนดจะได้ zero value
u := User{Name: "Alice", Email: "[email protected]"}

รูปแบบ named-field เป็นที่นิยมมากกว่า เพราะอ่านง่ายและทนต่อการเปลี่ยนลำดับ field

ใช้จุด (dot) เพื่ออ่านหรือเขียน field:

fmt.Println(u.Name) // อ่าน
u.Age = 30 // เขียน

สำหรับ shape ที่ใช้ครั้งเดียว — configuration, test fixture, หรือการจัดกลุ่มภายใน — คุณสามารถนิยาม struct type แบบ inline โดยไม่ตั้งชื่อ:

cfg := struct {
Host string
Port int
}{Host: "localhost", Port: 8080}

ค่า struct สองค่าสามารถเปรียบเทียบด้วย == ได้ ถ้า ทุก field เป็น comparable type การเปรียบเทียบจะทำทีละ field:

a := Point{1, 2}
b := Point{1, 2}
fmt.Println(a == b) // true

Struct ที่มี field เป็น slice, map, หรือ function ไม่สามารถ เปรียบเทียบด้วย ==

Go ไม่มี class inheritance แต่ใช้การ embed struct type หนึ่งไว้ภายในอีก struct เพื่อ promote field และ method:

type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
Address // embedded — City และ Country ถูก promote
}

หลังจาก embed แล้ว person.City คือ shorthand ของ person.Address.City นี่คือ composition ไม่ใช่ inheritance — embedded type ไม่รู้ว่าตัวเองถูก embed และ outer type ไม่ใช่ subtype ของ inner type

package main
import "fmt"
type Address struct {
City string
Country string
}
type Person struct {
Name string
Age int
Address // embedded struct
}
func main() {
// Named-field literal
p1 := Person{
Name: "Alice",
Age: 30,
Address: Address{City: "Bangkok", Country: "Thailand"},
}
fmt.Println(p1.Name, p1.Age)
fmt.Println(p1.City) // promoted from Address
fmt.Println(p1.Country) // promoted from Address
// Struct copy: p2 is an independent value
p2 := p1
p2.Name = "Bob"
fmt.Println(p1.Name, p2.Name) // Alice Bob
// Anonymous struct
cfg := struct {
Host string
Port int
}{Host: "localhost", Port: 8080}
fmt.Printf("%s:%d\n", cfg.Host, cfg.Port)
// Struct comparison (all fields comparable)
a1 := Address{City: "Bangkok", Country: "Thailand"}
a2 := Address{City: "Bangkok", Country: "Thailand"}
fmt.Println(a1 == a2) // true
}
สิ่งที่ได้ประโยชน์ต้นทุน
value semanticspredictable behavior, ไม่มี aliasingcopy cost สำหรับ struct ขนาดใหญ่
embedding (composition)reuse field และ method โดยไม่ต้อง inheritancefield shadowing อาจสับสนเมื่อ embedded type มี field ชื่อเดียวกัน
struct tagsmetadata สำหรับ JSON/DB/validation แบบ declarativeruntime-only — ไม่มี compile-time check ว่า tag ถูกต้อง
exported vs unexported fieldscontrol API surface ได้ชัดเจนrefactoring ที่ expose field ใหม่เป็น breaking change
  • struct embedding = inheritance — Go ไม่มี inheritance — Person ที่ embed Address ไม่ใช่ subtype ของ Address
  • struct copy เสมอแพง — struct เล็ก (2-4 fields) copy เร็วกว่าการใช้ pointer ที่ต้อง dereference และอาจทำให้ object escape to heap
  • unexported field = private ใน type — unexported หมายถึง package-level visibility — code ใน package เดียวกันเข้าถึงได้ทั้งหมด
  • struct tags มีผลตอน compile — struct tags เป็น string literal ที่ reflection อ่านตอน runtime — typo ใน tag จะไม่มี compile error

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

Kubernetes ใช้ struct ลึกหลายชั้น เช่น PodSpec embed Container, Volume, Affinity — struct tags ควบคุม JSON serialization ทั้งหมด

gorm (Go ORM) ใช้ struct tags เพื่อ map struct field ไปยัง database column — gorm:"primaryKey" กำหนด behavior ตอน runtime

คุณเขียน `p2 := p1` โดยที่ p1 เป็น struct `Person` แล้วกำหนด `p2.Name = "Bob"` ค่าของ p1.Name คืออะไร?
รูปแบบ struct literal แบบใดที่เป็น idiomatic Go?
การ embed `Address` ไว้ใน `Person` ให้ประโยชน์อะไร?
สามารถเปรียบเทียบ struct `User` สองตัวด้วย `==` ได้หรือไม่ ถ้า User มี field เป็น `[]string`?