
Go 1.27 เขียน Struct ซ้อนให้น้อยลง: ใช้ Promoted Fields ได้ตั้งแต่ตอนสร้างค่า
เวลาเราอ่านค่าจาก struct ที่ฝัง struct อื่นไว้ Go ให้เรียก promoted field หรือ “ฟิลด์ที่ถูกยกขึ้นมาให้เข้าถึงจากชั้นนอก” ได้ตรง ๆ มานานแล้ว เช่น employee.City แทน employee.Address.City
แต่ก่อน Go 1.27 ตอน สร้างค่า เรากลับต้องเปิดกล่องทุกชั้น เขียนชื่อ Address แล้วสร้าง Address{...} ซ้อนเข้าไปอีกที แม้ต้องการกำหนดเพียง City กับ State ความไม่สมมาตรนี้ไม่ได้ทำให้โปรแกรมผิด แต่มันเพิ่มวงเล็บ เพิ่ม indentation และบังคับให้คนอ่านแกะโครงสร้างก่อนเห็นข้อมูลสำคัญ
Go 1.27 ออกเมื่อ 19 สิงหาคม 2026 และแก้จุดนี้ด้วยกติกาสั้น ๆ: key ใน struct literal ใช้ field selector ที่ถูกต้องของ struct นั้นได้ รวมถึง field ที่ถูก promote มาจาก embedded struct ด้วย
WHY: ถ้าเข้าห้องได้ตรง ๆ ตอนอ่าน ก็ควรเข้าตรง ๆ ตอนจัดของ
นึกถึงตู้เอกสารที่มีกล่อง Employee อยู่ด้านนอก ข้างในมีกล่อง Address และ Contact อีกชั้น ก่อนหน้านี้เวลาจะใส่กระดาษ City เราต้องพูดเต็มว่า “เปิด Employee → เปิด Address → ใส่ City”
Go 1.27 ยอมให้บอกว่า “ใส่ City ใน Employee” ได้เลย เพราะ compiler รู้จากโครงสร้างอยู่แล้วว่า City ต้องเดินผ่าน Address ไปเก็บตรงไหน ผลลัพธ์ยังเป็นตู้ใบเดิม กล่องด้านในยังอยู่ครบ สิ่งที่หายไปมีเพียงพิธีการตอนเขียน literal
ประโยชน์จึงไม่ใช่การลดจำนวนบรรทัดเพื่อแข่งขันกันให้โค้ดสั้นที่สุด แต่คือการดันข้อมูลที่เราสนใจขึ้นมาอยู่ในระดับสายตาเดียวกัน โดยเฉพาะ config, request model และ domain model ที่ประกอบจาก struct ย่อยหลายกลุ่ม
HOW: กำหนด promoted field ได้โดยตรง
ตัวอย่างนี้ครอบคลุมทั้ง embedded struct สองตัว และการ promote ผ่านสองระดับ โค้ดถูกนำไปรันด้วย go test จริง ไม่ได้ตรวจเพียงว่าหน้าตาเหมือน Go ครับ
package promotedfields
import "testing"
type Address struct {
City string
State string
}
type Contact struct {
Email string
Phone string
}
type Employee struct {
Name string
Age int
Address
Contact
}
type Coordinates struct {
Latitude float64
Longitude float64
}
type Location struct {
Coordinates
Altitude float64
}
type Site struct {
Name string
Location
}
func TestEmployeePromotedFields(t *testing.T) {
employee := Employee{
Name: "Bob",
Age: 25,
City: "Bangkok",
State: "Bangkok",
Email: "[email protected]",
Phone: "02-000-0000",
}
if employee.Address.City != "Bangkok" {
t.Fatalf("Address.City = %q, want Bangkok", employee.Address.City)
}
if employee.Contact.Email != "[email protected]" {
t.Fatalf("Contact.Email = %q, want [email protected]", employee.Contact.Email)
}
}
func TestPromotedFieldsThroughTwoLevels(t *testing.T) {
site := Site{
Name: "Data Center",
Latitude: 13.7563,
Longitude: 100.5018,
Altitude: 12.5,
}
if site.Location.Coordinates.Latitude != 13.7563 {
t.Fatalf("Latitude = %v, want 13.7563", site.Latitude)
}
if site.Location.Altitude != 12.5 {
t.Fatalf("Altitude = %v, want 12.5", site.Altitude)
}
}
ใน test แรก City และ State ถูกส่งไปยัง Address ส่วน Email และ Phone ถูกส่งไปยัง Contact โดยไม่ต้องเขียน literal ซ้อน ใน test ที่สอง compiler เดินผ่าน Location และ Coordinates สองระดับเพื่อหา Latitude กับ Longitude
หลังสร้างค่าแล้ว เรายังเข้าถึงได้ทั้งรูปแบบสั้นอย่าง site.Latitude และเส้นทางเต็มอย่าง site.Location.Coordinates.Latitude เพราะฟีเจอร์นี้เปลี่ยนวิธีเขียนตอน initialize เท่านั้น ไม่ได้ flatten หรือย้าย field ในหน่วยความจำ
จุดที่ compiler จะไม่เดาให้
ความกระชับมีประโยชน์เมื่อเส้นทางชัดเจน แต่ Go ยังเลือกหยุดเมื่อคำตอบอาจผิดใจเรา
- ชื่อกำกวม: ถ้า embedded struct สองตัวต่างก็ promote ฟิลด์ชื่อ
NameการเขียนName:จะไม่ผ่าน ต้องกลับไปกำหนดผ่าน struct เจ้าของฟิลด์ให้ชัดเจน - เส้นทางผ่าน pointer: ภาษา Go กำหนดไว้ ว่า embedded field ที่ต้องเดินผ่านเพื่อไปยังฟิลด์ปลายทางต้องไม่เป็น pointer เพราะ compiler ไม่ควรสร้างค่าที่ pointer จะชี้ไปให้เงียบ ๆ
- กำหนดกล่องและของในกล่องพร้อมกัน: literal เดียวกันห้ามใส่ทั้ง
Address: Address{...}และCity:เพราะทั้งคู่เขียนทับพื้นที่เดียวกัน - โปรเจกต์ยัง target Go รุ่นเก่า: ต้องใช้ toolchain และ language version Go 1.27 ขึ้นไป โค้ดเดิมยังทำงานเหมือนเดิม แต่ syntax ใหม่นี้ไม่ควรถูกใส่ใน module ที่ยังต้อง build ด้วย Go 1.26
แล้วควรใช้เป็นค่าเริ่มต้นทุกครั้งไหม
พี่โชว์แนะนำให้เลือกจาก “ภาระของคนอ่าน” มากกว่าจำนวนบรรทัดครับ
ถ้า embedded struct เล็ก ชื่อฟิลด์ไม่ชนกัน และคนอ่านสนใจค่ารวมของ object รูปแบบใหม่ช่วยให้เห็นภาพเร็ว แต่ถ้า DatabaseConfig มีสามสิบฟิลด์ การเขียนกลุ่ม DatabaseConfig: DatabaseConfig{...} ไว้ชัด ๆ อาจช่วยบอกขอบเขตได้ดีกว่า
หลักง่าย ๆ คือใช้ promoted fields เมื่อมันตัดพิธีการ และใช้ nested literal เมื่อชื่อกลุ่มยังมีความหมายต่อการทำความเข้าใจ อย่าทำให้โค้ดสั้นลงจนคนอ่านต้องเปิด type definition เพื่อเดาว่าแต่ละค่าถูกเก็บไว้ที่ไหน
Recap
Go 1.27 ทำให้การสร้าง struct สอดคล้องกับการใช้งานมากขึ้น: ถ้าเราเรียก promoted field จากชั้นนอกได้ เราก็กำหนดค่าจากชั้นนอกได้เช่นกัน Compiler เป็นคนหาเส้นทางไปยัง embedded struct ส่วน layout และพฤติกรรมหลังสร้างค่ายังเหมือนเดิม
จำไว้สามข้อ: ใช้เมื่อชื่อไม่กำกวม, อย่าผสม embedded field กับ promoted field ที่ทับกันใน literal เดียว และเก็บ nested syntax ไว้ใช้เมื่อมันช่วยเล่าโครงสร้างได้ชัดกว่า ฟีเจอร์เล็ก ๆ แบบนี้อาจไม่เปลี่ยนสถาปัตยกรรมของระบบ แต่ช่วยให้ทุกครั้งที่สร้าง model อ่านง่ายขึ้นจริงครับ