สตรักเจอร์ใน Go
สตรักเจอร์สามารถเก็บข้อมูลประเภทต่างกันกลุ่มหนึ่ง เป็นประเภทคอมโพสิต Go ทิ้งคลาสและการสืบทอดไป พร้อมทั้งทิ้งเมธอดคอนสตรัคเตอร์ด้วย อ่อน化了ฟังก์ชัน面向对象 Go ไม่ใช่ภาษา OOP แบบดั้งเดิม แต่ Go ยังคงมีร่องรอยของ OOP ผ่านสตรักเจอร์และเมธอดก็สามารถจำลองคลาสได้ ด้านล่างเป็นตัวอย่างสตรักเจอร์ง่ายๆ
type Programmer struct {
Name string
Age int
Job string
Language []string
}การประกาศ
การประกาศสตรักเจอร์ง่ายมาก ตัวอย่างดังนี้
type Person struct {
name string
age int
}สตรักเจอร์เองและฟิลด์ภายในล้วนปฏิบัติตามวิธีการเปิดเผยการตั้งชื่อตัวพิมพ์ใหญ่เล็ก สำหรับฟิลด์ที่ติดกันประเภทเดียวกัน ไม่ต้องประกาศประเภทซ้ำ ดังนี้
type Rectangle struct {
height, width, area int
color string
}TIP
เมื่อประกาศฟิลด์สตรักเจอร์ ชื่อฟิลด์ไม่สามารถซ้ำกับชื่อเมธอดได้
การสร้างอินสแตนซ์
Go ไม่มีเมธอดคอนสตรัคเตอร์ ในสถานการณ์ส่วนใหญ่ใช้วิธีด้านล่างในการสร้างอินสแตนซ์สตรักเจอร์ เมื่อเริ่มต้นก็เหมือน map ระบุชื่อฟิลด์แล้วเริ่มต้นค่าฟิลด์
programmer := Programmer{
Name: "jack",
Age: 19,
Job: "coder",
Language: []string{"Go", "C++"},
}แต่ก็สามารถละเว้นชื่อฟิลด์ได้ เมื่อละเว้นชื่อฟิลด์แล้ว ต้องเริ่มต้นฟิลด์ทั้งหมด โดยทั่วไปไม่แนะนำให้ใช้วิธีนี้ เพราะความสามารถในการอ่านแย่มาก
programmer := Programmer{
"jack",
19,
"coder",
[]string{"Go", "C++"}}หากกระบวนการสร้างอินสแตนซ์ซับซ้อน คุณสามารถเขียนฟังก์ชันหนึ่งเพื่อสร้างอินสแตนซ์สตรักเจอร์ เช่นด้านล่างนี้ คุณก็สามารถเข้าใจมันเป็นฟังก์ชันคอนสตรัคเตอร์ได้
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}แต่ Go ไม่รองรับการโอเวอร์โหลดฟังก์ชันและเมธอด所以你ไม่สามารถนิยามพารามิเตอร์ต่างกันสำหรับฟังก์ชันหรือเมธอดเดียวกันได้ หากคุณต้องการสร้างอินสแตนซ์สตรักเจอร์ด้วยวิธีต่างๆ ไม่ก็สร้างฟังก์ชันคอนสตรัคเตอร์หลายตัว ไม่ก็แนะนำให้ใช้รูปแบบ options
รูปแบบตัวเลือก
รูปแบบตัวเลือกเป็นรูปแบบการออกแบบที่พบบ่อยมากในภาษา Go สามารถสร้างอินสแตนซ์สตรักเจอร์ได้อย่างยืดหยุ่นมากขึ้น มีความสามารถในการขยายสูง และไม่จำเป็นต้องเปลี่ยนลายเซ็นฟังก์ชันของฟังก์ชันคอนสตรัคเตอร์ สมมติว่ามีสตรักเจอร์ดังนี้
type Person struct {
Name string
Age int
Address string
Salary float64
Birthday string
}ประกาศประเภท PersonOptions มันรับพารามิเตอร์ประเภท *Person มันต้องเป็นพอยน์เตอร์ เพราะเราต้องกำหนดค่าให้ Person ในคลอเซอร์
type PersonOptions func(p *Person)ต่อไปสร้างฟังก์ชันตัวเลือก พวกมันมักขึ้นต้นด้วย With ค่าส่งคืนของพวกมันคือฟังก์ชันคลอเซอร์
func WithName(name string) PersonOptions {
return func(p *Person) {
p.Name = name
}
}
func WithAge(age int) PersonOptions {
return func(p *Person) {
p.Age = age
}
}
func WithAddress(address string) PersonOptions {
return func(p *Person) {
p.Address = address
}
}
func WithSalary(salary float64) PersonOptions {
return func(p *Person) {
p.Salary = salary
}
}ลายเซ็นฟังก์ชันคอนสตรัคเตอร์ที่ประกาศจริงดังนี้ มันรับพารามิเตอร์ประเภท PersonOptions แบบแปรผัน
func NewPerson(options ...PersonOptions) *Person {
// ใช้ options ก่อน
p := &Person{}
for _, option := range options {
option(p)
}
// จัดการค่าเริ่มต้น
if p.Age < 0 {
p.Age = 0
}
......
return p
}เช่นนี้สำหรับความต้องการการสร้างอินสแตนซ์ต่างกันเพียงใช้ฟังก์ชันคอนสตรัคเตอร์เดียวก็เสร็จแล้ว เพียงส่งเข้าฟังก์ชัน Options ที่ต่างกันก็พอ
func main() {
pl := NewPerson(
WithName("John Doe"),
WithAge(25),
WithAddress("123 Main St"),
WithSalary(10000.00),
)
p2 := NewPerson(
WithName("Mike jane"),
WithAge(30),
)
}รูปแบบฟังก์ชันตัวเลือกสามารถเห็นได้ในโปรเจกต์โอเพนซอร์สมากมาย วิธีการสร้างอินสแตนซ์ของ gRPC Server ก็ใช้รูปแบบการออกแบบนี้ รูปแบบฟังก์ชันตัวเลือกเหมาะสำหรับการสร้างอินสแตนซ์ที่ซับซ้อนเท่านั้น หากพารามิเตอร์มีเพียงไม่กี่ตัวง่ายๆ แนะนำให้ใช้ฟังก์ชันคอนสตรัคเตอร์ธรรมดาแก้ปัญหาก็พอ
การรวม
ใน Go ความสัมพันธ์ระหว่างสตรักเจอร์แสดงผ่านการรวม สามารถรวมแบบชัดเจน หรือรวมแบบไม่ระบุชื่อ แบบหลังใช้คล้ายกับการสืบทอดมากกว่า แต่โดยพื้นฐานแล้วไม่มีการเปลี่ยนแปลงใดๆ เช่น
วิธีรวมแบบชัดเจน
type Person struct {
name string
age int
}
type Student struct {
p Person
school string
}
type Employee struct {
p Person
job string
}เมื่อใช้ต้องระบุฟิลด์ p อย่างชัดเจน
student := Student{
p: Person{name: "jack", age: 18},
school: "lili school",
}
fmt.Println(student.p.name)ส่วนการรวมแบบไม่ระบุชื่อไม่ต้องระบุฟิลด์อย่างชัดเจน
type Person struct {
name string
age int
}
type Student struct {
Person
school string
}
type Employee struct {
Person
job string
}ชื่อฟิลด์ไม่ระบุชื่อโดยค่าเริ่มต้นคือชื่อประเภท ผู้เรียกสามารถเข้าถึงฟิลด์และเมธอดของประเภทนั้นได้โดยตรง แต่ยกเว้นสะดวกกว่าแล้วไม่มีความแตกต่างใดๆ กับวิธีแรก
student := Student{
Person: Person{name: "jack",age: 18},
school: "lili school",
}
fmt.Println(student.name)พอยน์เตอร์
สำหรับพอยน์เตอร์สตรักเจอร์แล้ว ไม่ต้อง dereference ก็สามารถเข้าถึงเนื้อหาสตรักเจอร์ได้โดยตรง ตัวอย่างดังนี้
p := &Person{
name: "jack",
age: 18,
}
fmt.Println(p.age,p.name)เมื่อคอมไพล์จะแปลงเป็น (*p).name, (*p).age ที่จริงแล้วยังต้อง dereference แต่เมื่อเขียนโค้ดสามารถละเว้นได้ ถือเป็น syntactic sugar ชนิดหนึ่ง
แท็ก
แท็กสตรักเจอร์เป็นรูปแบบหนึ่งของการเขียนโปรแกรมแบบเมตา ผสมผสานกับการสะท้อนสามารถสร้างฟังก์ชันที่น่าอัศจรรย์มากมาย รูปแบบดังนี้
`key1:"val1" key2:"val2"`แท็กเป็นรูปแบบคู่คีย์-ค่า ใช้ช่องว่างคั่นระหว่างกัน ความทนทานต่อข้อผิดพลาดของแท็กสตรักเจอร์ต่ำมาก หากไม่ได้เขียนแท็กสตรักเจอร์ตามรูปแบบที่ถูกต้อง จะทำให้อ่านไม่ได้ตามปกติ แต่เมื่อคอมไพล์จะไม่มีข้อผิดพลาดใดๆ ด้านล่างเป็นตัวอย่างการใช้งาน
type Programmer struct {
Name string `json:"name"`
Age int `yaml:"age"`
Job string `toml:"job"`
Language []string `properties:"language"`
}แท็กสตรักเจอร์ใช้กันอย่างแพร่หลายที่สุดคือนิยามชื่อเล่นในรูปแบบการซีเรียไลซ์ต่างๆ การใช้แท็กต้องผสมผสานกับการสะท้อนจึงสามารถแสดงฟังก์ชันของมันได้อย่างสมบูรณ์
การจัดเรียงหน่วยความจำ
การกระจายหน่วยความจำฟิลด์สตรักเจอร์ใน Go ปฏิบัติตามกฎการจัดเรียงหน่วยความจำ การทำเช่นนี้สามารถลดจำนวนครั้งที่ CPU เข้าถึงหน่วยความจำได้ ตามที่กินหน่วยความจำมากขึ้น เป็นวิธีการหนึ่งของการแลกพื้นที่กับเวลา สมมติว่ามีสตรักเจอร์ดังนี้
type Num struct {
A int64
B int32
C int16
D int8
E int32
}ทราบจำนวนไบต์ที่ประเภทเหล่านี้กิน
int64กิน 8 ไบต์int32กิน 4 ไบต์int16กิน 2 ไบต์int8กินหนึ่งไบต์
จำนวนไบต์ที่สตรักเจอร์ทั้งหมดกินดูเหมือนจะเป็น 8+4+2+1+4=19 ไบต์ใช่ไหม แน่นอนว่าไม่ใช่เช่นนั้น ตามกฎการจัดเรียงหน่วยความจำแล้ว ความยาวหน่วยความจำที่สตรักเจอร์กินอย่างน้อยต้องเป็นจำนวนเท่าของฟิลด์ที่ใหญ่ที่สุด ส่วนที่ขาดให้เติมให้ครบ ในสตรักเจอร์นี้ใหญ่ที่สุดคือ int64 กิน 8 ไบต์ ดังนั้นการกระจายหน่วยความจำ如下图所示

ดังนั้นที่จริงแล้วกิน 24 ไบต์ ในนั้นมี 5 ไบต์ที่ไม่มีประโยชน์
ดูสตรักเจอร์ด้านล่างนี้ต่อ
type Num struct {
A int8
B int64
C int8
}หลังจากเข้าใจกฎข้างต้นแล้ว สามารถเข้าใจได้เร็วมากว่าหน่วยความจำที่มันกินก็คือ 24 ไบต์เช่นกัน แม้ว่ามันจะมีเพียงสามฟิลด์ เสียไป 14 ไบต์เต็มๆ

แต่เราสามารถปรับฟิลด์ เปลี่ยนเป็นลำดับดังนี้
type Num struct {
A int8
C int8
B int64
}เช่นนี้หน่วยความจำที่กินก็กลายเป็น 16 ไบต์ เสียไป 6 ไบต์ ลดการเสียหน่วยความจำ 8 ไบต์

จากทฤษฎีแล้ว การให้ฟิลด์ในสตรักเจอร์กระจายตามลำดับที่เหมาะสม สามารถลดหน่วยความจำที่มันกินได้ แต่ในกระบวนการเขียนโค้ดจริง ไม่มีเหตุผลจำเป็นที่ต้องทำเช่นนั้น มันไม่จำเป็นต้องนำมาซึ่งการปรับปรุงที่เป็นรูปธรรมในการลดหน่วยความจำที่ใช้ แต่จะเพิ่มความดันทางจิตใจและภาระทางจิตใจของนักพัฒนาอย่างแน่นอน โดยเฉพาะในธุรกิจบางจำนวนฟิลด์ของสตรักเจอร์อาจมีมากถึงหลายสิบหรือหลายร้อย ดังนั้น仅作ความเข้าใจก็พอ
TIP
หากคุณจริงๆ ต้องการประหยัดหน่วยความจำด้วยวิธีนี้ สามารถดูไลบรารีสองนี้
พวกมันจะตรวจสอบสตรักเจอร์ในซอร์สโค้ดของคุณ คำนวณและจัดเรียงฟิลด์สตรักเจอร์ใหม่เพื่อลดหน่วยความจำที่สตรักเจอร์กินให้ต่ำสุด
สตรักเจอร์ว่าง
สตรักเจอร์ว่างไม่มีฟิลด์ ไม่กินหน่วยความจำ เราสามารถใช้ฟังก์ชัน unsafe.Sizeof เพื่อคำนวณจำนวนไบต์ที่กินได้
func main() {
type Empty struct {}
fmt.Println(unsafe.Sizeof(Empty{}))
}เอาต์พุต
0สตรักเจอร์ว่างมีสถานการณ์การใช้งานมากมาย เช่น ที่กล่าวมาข้างต้นแล้ว คือเป็นประเภทค่าของ map สามารถใช้ map เป็น set ได้ หรือเป็นประเภทของแชนเนล แสดงว่าแชนเนลประเภทแจ้งเตือนเท่านั้น
