Skip to content

สตรักเจอร์ใน Go

สตรักเจอร์สามารถเก็บข้อมูลประเภทต่างกันกลุ่มหนึ่ง เป็นประเภทคอมโพสิต Go ทิ้งคลาสและการสืบทอดไป พร้อมทั้งทิ้งเมธอดคอนสตรัคเตอร์ด้วย อ่อน化了ฟังก์ชัน面向对象 Go ไม่ใช่ภาษา OOP แบบดั้งเดิม แต่ Go ยังคงมีร่องรอยของ OOP ผ่านสตรักเจอร์และเมธอดก็สามารถจำลองคลาสได้ ด้านล่างเป็นตัวอย่างสตรักเจอร์ง่ายๆ

go
type Programmer struct {
  Name     string
  Age      int
  Job      string
  Language []string
}

การประกาศ

การประกาศสตรักเจอร์ง่ายมาก ตัวอย่างดังนี้

go
type Person struct {
   name string
   age int
}

สตรักเจอร์เองและฟิลด์ภายในล้วนปฏิบัติตามวิธีการเปิดเผยการตั้งชื่อตัวพิมพ์ใหญ่เล็ก สำหรับฟิลด์ที่ติดกันประเภทเดียวกัน ไม่ต้องประกาศประเภทซ้ำ ดังนี้

go
type Rectangle struct {
  height, width, area int
  color               string
}

TIP

เมื่อประกาศฟิลด์สตรักเจอร์ ชื่อฟิลด์ไม่สามารถซ้ำกับชื่อเมธอดได้

การสร้างอินสแตนซ์

Go ไม่มีเมธอดคอนสตรัคเตอร์ ในสถานการณ์ส่วนใหญ่ใช้วิธีด้านล่างในการสร้างอินสแตนซ์สตรักเจอร์ เมื่อเริ่มต้นก็เหมือน map ระบุชื่อฟิลด์แล้วเริ่มต้นค่าฟิลด์

go
programmer := Programmer{
   Name:     "jack",
   Age:      19,
   Job:      "coder",
   Language: []string{"Go", "C++"},
}

แต่ก็สามารถละเว้นชื่อฟิลด์ได้ เมื่อละเว้นชื่อฟิลด์แล้ว ต้องเริ่มต้นฟิลด์ทั้งหมด โดยทั่วไปไม่แนะนำให้ใช้วิธีนี้ เพราะความสามารถในการอ่านแย่มาก

go
programmer := Programmer{
   "jack",
   19,
   "coder",
   []string{"Go", "C++"}}

หากกระบวนการสร้างอินสแตนซ์ซับซ้อน คุณสามารถเขียนฟังก์ชันหนึ่งเพื่อสร้างอินสแตนซ์สตรักเจอร์ เช่นด้านล่างนี้ คุณก็สามารถเข้าใจมันเป็นฟังก์ชันคอนสตรัคเตอร์ได้

go
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 สามารถสร้างอินสแตนซ์สตรักเจอร์ได้อย่างยืดหยุ่นมากขึ้น มีความสามารถในการขยายสูง และไม่จำเป็นต้องเปลี่ยนลายเซ็นฟังก์ชันของฟังก์ชันคอนสตรัคเตอร์ สมมติว่ามีสตรักเจอร์ดังนี้

go
type Person struct {
  Name     string
  Age      int
  Address  string
  Salary   float64
  Birthday string
}

ประกาศประเภท PersonOptions มันรับพารามิเตอร์ประเภท *Person มันต้องเป็นพอยน์เตอร์ เพราะเราต้องกำหนดค่าให้ Person ในคลอเซอร์

go
type PersonOptions func(p *Person)

ต่อไปสร้างฟังก์ชันตัวเลือก พวกมันมักขึ้นต้นด้วย With ค่าส่งคืนของพวกมันคือฟังก์ชันคลอเซอร์

go
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 แบบแปรผัน

go
func NewPerson(options ...PersonOptions) *Person {
    // ใช้ options ก่อน
  p := &Person{}
    for _, option := range options {
        option(p)
    }

  // จัดการค่าเริ่มต้น
  if p.Age < 0 {
    p.Age = 0
  }
  ......

    return p
}

เช่นนี้สำหรับความต้องการการสร้างอินสแตนซ์ต่างกันเพียงใช้ฟังก์ชันคอนสตรัคเตอร์เดียวก็เสร็จแล้ว เพียงส่งเข้าฟังก์ชัน Options ที่ต่างกันก็พอ

go
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 ความสัมพันธ์ระหว่างสตรักเจอร์แสดงผ่านการรวม สามารถรวมแบบชัดเจน หรือรวมแบบไม่ระบุชื่อ แบบหลังใช้คล้ายกับการสืบทอดมากกว่า แต่โดยพื้นฐานแล้วไม่มีการเปลี่ยนแปลงใดๆ เช่น

วิธีรวมแบบชัดเจน

go
type Person struct {
   name string
   age  int
}

type Student struct {
   p      Person
   school string
}

type Employee struct {
   p   Person
   job string
}

เมื่อใช้ต้องระบุฟิลด์ p อย่างชัดเจน

go
student := Student{
   p:      Person{name: "jack", age: 18},
   school: "lili school",
}
fmt.Println(student.p.name)

ส่วนการรวมแบบไม่ระบุชื่อไม่ต้องระบุฟิลด์อย่างชัดเจน

go
type Person struct {
  name string
  age  int
}

type Student struct {
  Person
  school string
}

type Employee struct {
  Person
  job string
}

ชื่อฟิลด์ไม่ระบุชื่อโดยค่าเริ่มต้นคือชื่อประเภท ผู้เรียกสามารถเข้าถึงฟิลด์และเมธอดของประเภทนั้นได้โดยตรง แต่ยกเว้นสะดวกกว่าแล้วไม่มีความแตกต่างใดๆ กับวิธีแรก

go
student := Student{
   Person: Person{name: "jack",age: 18},
   school: "lili school",
}
fmt.Println(student.name)

พอยน์เตอร์

สำหรับพอยน์เตอร์สตรักเจอร์แล้ว ไม่ต้อง dereference ก็สามารถเข้าถึงเนื้อหาสตรักเจอร์ได้โดยตรง ตัวอย่างดังนี้

go
p := &Person{
   name: "jack",
   age:  18,
}
fmt.Println(p.age,p.name)

เมื่อคอมไพล์จะแปลงเป็น (*p).name, (*p).age ที่จริงแล้วยังต้อง dereference แต่เมื่อเขียนโค้ดสามารถละเว้นได้ ถือเป็น syntactic sugar ชนิดหนึ่ง

แท็ก

แท็กสตรักเจอร์เป็นรูปแบบหนึ่งของการเขียนโปรแกรมแบบเมตา ผสมผสานกับการสะท้อนสามารถสร้างฟังก์ชันที่น่าอัศจรรย์มากมาย รูปแบบดังนี้

go
`key1:"val1" key2:"val2"`

แท็กเป็นรูปแบบคู่คีย์-ค่า ใช้ช่องว่างคั่นระหว่างกัน ความทนทานต่อข้อผิดพลาดของแท็กสตรักเจอร์ต่ำมาก หากไม่ได้เขียนแท็กสตรักเจอร์ตามรูปแบบที่ถูกต้อง จะทำให้อ่านไม่ได้ตามปกติ แต่เมื่อคอมไพล์จะไม่มีข้อผิดพลาดใดๆ ด้านล่างเป็นตัวอย่างการใช้งาน

go
type Programmer struct {
    Name     string `json:"name"`
    Age      int `yaml:"age"`
    Job      string `toml:"job"`
    Language []string `properties:"language"`
}

แท็กสตรักเจอร์ใช้กันอย่างแพร่หลายที่สุดคือนิยามชื่อเล่นในรูปแบบการซีเรียไลซ์ต่างๆ การใช้แท็กต้องผสมผสานกับการสะท้อนจึงสามารถแสดงฟังก์ชันของมันได้อย่างสมบูรณ์

การจัดเรียงหน่วยความจำ

การกระจายหน่วยความจำฟิลด์สตรักเจอร์ใน Go ปฏิบัติตามกฎการจัดเรียงหน่วยความจำ การทำเช่นนี้สามารถลดจำนวนครั้งที่ CPU เข้าถึงหน่วยความจำได้ ตามที่กินหน่วยความจำมากขึ้น เป็นวิธีการหนึ่งของการแลกพื้นที่กับเวลา สมมติว่ามีสตรักเจอร์ดังนี้

go
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 ไบต์ที่ไม่มีประโยชน์

ดูสตรักเจอร์ด้านล่างนี้ต่อ

go
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 เพื่อคำนวณจำนวนไบต์ที่กินได้

go
func main() {
   type Empty struct {}
   fmt.Println(unsafe.Sizeof(Empty{}))
}

เอาต์พุต

0

สตรักเจอร์ว่างมีสถานการณ์การใช้งานมากมาย เช่น ที่กล่าวมาข้างต้นแล้ว คือเป็นประเภทค่าของ map สามารถใช้ map เป็น set ได้ หรือเป็นประเภทของแชนเนล แสดงว่าแชนเนลประเภทแจ้งเตือนเท่านั้น

Golang by www.golangdev.cn edit