1 พื้นฐานของ Struct

ในภาษา Go, struct เป็นชนิดข้อมูลที่รวมข้อมูลประเภทต่าง ๆ หรือเหมือนกันเข้าด้วยกันเป็นหน่วยเดียว โดย struct เป็นสิ่งที่สำคัญใน Go เนื่องจากมันเป็นสิ่งที่พื้นฐานของการเขียนโปรแกรมเชิงวัตถุ อย่างไรก็ตามมันมีความแตกต่างเล็ก ๆ น้อยจากภาษาโปรแกรมเชิงวัตถุทั่วไป

ความจำเป็นของ struct มาจากด้านต่อไปนี้:

  • การจัดระเบียบตัวแปรที่มีความสัมพันธ์แข็งแกร่งเพื่อเสริมความสามารถในการบำรุงรักษาโค้ด
  • การจำลอง "คลาส" ให้สามารถใช้งานง่าย เพื่อสนับสนุนคุณลักษณะการซ่อนและการรวมกัน
  • เมื่อทำงานกับโครงสร้างข้อมูล เช่น JSON, บันทึกฐานข้อมูล เป็นต้น struct มีบทบาทที่สำคัญในการแมปข้อมูลอย่างสะดวก

การจัดระเบียบข้อมูลด้วย struct ช่วยให้การแสดงตัวแทนของวัตถุในโลกแห่งความเป็นจริง เช่นผู้ใช้งาน เรียกข้อมูล เป็นต้น

2 การกำหนด Struct

โครงสร้างของการกำหนด struct เป็นดังนี้:

type ชื่อStruct struct {
    ฟิลด์1 ประเภทของฟิลด์1
    ฟิลด์2 ประเภทของฟิลด์2
    // ... ตัวแปรสมาชิกอื่น ๆ
}
  • คำว่า type นำเสนอการกำหนดของ struct
  • ชื่อStruct คือชื่อของชนิด struct ซึ่งตามกฎการตั้งชื่อของ Go โดยทั่วไปมักจะใช้ตัวพิมพ์ใหญ่เพื่อบ่งชี้ถึงความสามารถในการนำออก
  • คำว่า struct แสดงถึงว่านี่คือชนิด struct
  • ภายในเครื่องหมายปีกกา {} ระบุตัวแปรสมาชิก (ฟิลด์) ของ struct โดยละทุกตัวไปกับประเภทของตัวแปรเอง

ประเภทของสมาชิก struct สามารถเป็นสิ่งใดก็ได้ รวมถึงประเภทพื้นฐาน (เช่น int, string เป็นต้น) และประเภทซับซ้อน (เช่น อาร์เรย์, สไลซ์, struct อื่น ๆ เป็นต้น)

ตัวอย่างการกำหนด struct ที่แทนบุคคล:

type บุคคล struct {
    ชื่อ   string
    อายุ    int
    อีเมล []string // สามารถรวมทั้งประเภทซับซ้อนได้ เช่น สไลซ์
}

ในโค้ดด้านบน บุคคล struct มีสมาชิกทั้งหมด 3 ฟิลด์: ชื่อ ที่มีประเภท string, อายุ ที่มีประเภท integer และ อีเมล ที่มีประเภทสไลซ์ของ string, ระบุถึงความเป็นไปได้ที่มีหลายที่อีเมลของบุคคลแต่ละคน

3 การสร้างและกำหนดค่าเริ่มต้นของ Struct

3.1 การสร้างตัวอย่างของ Struct

มีวิธีสองวิธีในการสร้างตัวอย่างของ struct: การประกาศโดยตรง หรือการใช้คำสำคัญ new

การประกาศโดยตรง:

var p บุคคล

โค้ดด้านบนสร้างตัวอย่าง p ของประเภท บุคคล โดยที่แต่ละตัวสมาชิกของ struct จะเป็นค่าศูนย์ของประเภทของมัน

การใช้คำสำคัญ new:

p := new(บุคคล)

การสร้าง struct โดยใช้คำสำคัญ new จะส่งผลให้ได้เป็นตัวชี้ของ struct ตัวแปร p ที่จุดนี้จะเป็นประเภท *บุคคล ชี้ไปที่ตัวแปรที่ถูกจัดสรรขึ้นใหม่ ประเภท บุคคล ทั้งที่ตัวสมาชิกของมันมีค่าเริ่มต้นเป็นศูนย์เสมอ

3.2 กำหนดค่าเริ่มต้นของ Struct

ตัวอย่างของ struct สามารถกำหนดค่าเริ่มต้นพร้อม ๆ กันเมื่อสร้างด้วยวิธีสองวิธี: ด้วยการระบุชื่อของฟิลด์หรือโดยไม่ระบุชื่อของฟิลด์

การกำหนดพร้อม ๆ กันโดยระบุชื่อของฟิลด์:

p := บุคคล{
    ชื่อ:   "Alice",
    อายุ:    30,
    อีเมล: []string{"[email protected]", "[email protected]"},
}

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

การกำหนดพร้อม ๆ กันโดยไม่ระบุชื่อของฟิลด์:

p := บุคคล{"Bob", 25, []string{"[email protected]"}}

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

นอกจากนี้ struct สามารถกำหนดค่าพร้อม ๆ กันในฟิลด์ที่กำหนดให้แล้ว ซึ่งถึงแม้จะไม่ระบุเองโดยอัตโนมัติเริ่มต้นจะนำค่าเริ่มต้นเพิ่มโดยจะมีโครงสร้างที่ไม่ถึงไว้:

p := บุคคล{ชื่อ: "Charlie"}

ในตัวอย่างนี้ เฉพาะฟิลด์ ชื่อ ถูกกำหนดค่า ในขณะที่ อายุ และ อีเมล จะมีค่าเริ่มต้นของมันเอง

4 การเข้าถึงสมาชิกของ Struct

การเข้าถึงตัวแปรสมาชิกของ struct ใน Go มีความง่ายมาก ๆ ซึ่งสามารถทำได้โดยใช้ ตัวดอท (.) เป็นผู้ดำเนินการ หากคุณมีตัวแปร struct คุณสามารถอ่านหรือแก้ไขค่าตัวแปรสมาชิกของมันได้แบบนี้อย่างง่ายดาย

5 การประกอบ Struct และการฝัง Struct

Struct ไม่ได้มีอิสระเท่านั้น แต่ยังสามารถถูกประกอบและฝังไว้ด้วยเพื่อสร้างโครงสร้างข้อมูลที่ซับซ้อนมากขึ้น

5.1 Struct แบบไม่มีชื่อ

Struct แบบไม่มีชื่อไม่ได้ประกาศชนิดใหม่โดยโดยตรง แต่ใช้การกำหนดโครงสร้างของ struct โดยตรง นี่เป็นประโยชน์เมื่อคุณต้องการสร้าง struct ครั้งเดียวและใช้งานมันโดยง่าย และหลีกเลี่ยงการสร้างชนิดที่ไม่จำเป็น

ตัวอย่าง:

package main

import "fmt"

func main() {
    // กำหนดและกำหนดค่าให้กับ struct แบบไม่มีชื่อ
    person := struct {
        Name string
        Age  int
    }{
        Name: "Eve",
        Age:  40,
    }

    // เข้าถึงสมาชิกของ struct แบบไม่มีชื่อ
    fmt.Println("Name:", person.Name)
    fmt.Println("Age:", person.Age)
}

ในตัวอย่างนี้ ไม่ได้สร้างชนิดข้อมูลใหม่ แต่กำหนด struct โดยตรงและสร้างอินสแตนซ์ของมัน ตัวอย่างนี้แสดงวิธีที่จะกำหนดค่าเริ่มต้นสำหรับ struct แบบไม่มีชื่อ และเข้าถึงสมาชิกของมัน

5.2 การฝัง Struct

การฝัง struct เกี่ยวข้องกับการฝัง struct หนึ่งเป็นสมาชิกของ struct อีกตัวหนึ่ง นี้ทำให้เราสร้างโมเดลข้อมูลที่ซับซ้อนมากขึ้น

ตัวอย่าง:

package main

import "fmt"

// กำหนด struct Address
type Address struct {
    City    string
    Country string
}

// ฝัง struct Address เข้าไปใน struct Person
type Person struct {
    Name    string
    Age     int
    Address Address
}

func main() {
    // กำหนดค่าเริ่มต้นให้กับ Person
    p := Person{
        Name: "Charlie",
        Age:  28,
        Address: Address{
            City:    "New York",
            Country: "USA",
        },
    }

    // เข้าถึงสมาชิกของ struct ที่ถูกฝังไว้
    fmt.Println("Name:", p.Name)
    fmt.Println("Age:", p.Age)
    // เข้าถึงสมาชิกของ struct Address
    fmt.Println("City:", p.Address.City)
    fmt.Println("Country:", p.Address.Country)
}

ในตัวอย่างนี้ เรากำหนด struct Address และฝังมันเป็นสมาชิกใน struct Person ขณะที่สร้างอินสแตนซ์ของ Person เรายังสร้างอินสแตนซ์ของ Address พร้อมกันเราสามารถเข้าถึงสมาชิกของ struct ที่ถูกฝังไว้โดยใช้ตัวอ้างอิงจุด

6 วิธีการของ Struct

คุณสมบัติของโปรแกรมเชิงวัตถุ (OOP) สามารถนำมาใช้ผ่านวิธีการของ struct

6.1 แนวคิดพื้นฐานของวิธีการ

ในภาษา Go แม้ว่าจะไม่มีแนวคิดเรื่องคลาสและออบเจกต์แบบดั้งเดิม แต่คุณสมบัติ OOP ที่คล้ายกันสามารถถูกบำบัดด้วยการผูกวิธีการเข้าไปยัง struct วิธีการของ struct คือ ฟังก์ชันพิเศษหนึ่งชนิดที่สัมพันธ์กับชนิด struct ที่เฉพาะเจาะจง (หรือตัวชี้ไปที่ struct) ทำให้ชนิดนั้นมีเซตของวิธีการของตัวเอง

// กำหนด struct ที่เรียบง่าย
type Rectangle struct {
    length, width float64
}

// กำหนดวิธีการสำหรับ struct Rectangle เพื่อคำนวณพื้นที่ของสี่เหลี่ยม
func (r Rectangle) Area() float64 {
    return r.length * r.width
}

ในโค้ดด้านบน วิธีการ Area ถูกผูกพันกับ struct Rectangle ในการกำหนดวิธีการ คำตอบ r Rectangle คือ ตัวรับที่ระบุว่าวิธีการนี้เผยแพร่กับประเภท Rectangle ตัวรับปรากฏก่อนชื่อวิธีการ

6.2 ตัวรับค่าแบบ Value และตัวรับค่าแบบ Pointer

วิธีการเรียกใช้งานของเมทอดสามารถแบ่งเป็นตัวรับค่าแบบ Value และตัวรับค่าแบบ Pointer ขึ้นอยู่กับประเภทของตัวรับค่า ตัวรับค่าแบบ Value จะใช้สำเนาของ struct ในการเรียกใช้เมทอด ในขณะที่ตัวรับค่าแบบ Pointer จะใช้ตัวชี้ไปที่ struct และสามารถแก้ไขค่าของ struct ต้นฉบับได้

// กำหนดเมทอดที่รับค่าแบบ Value
func (r Rectangle) Perimeter() float64 {
    return 2 * (r.length + r.width)
}

// กำหนดเมทอดที่รับค่าแบบ Pointer ซึ่งสามารถแก้ไขค่าของ struct
func (r *Rectangle) SetLength(newLength float64) {
    r.length = newLength // สามารถแก้ไขค่าของ struct ต้นฉบับได้
}

ในตัวอย่างข้างต้น Perimeter เป็นเมทอดที่รับค่าแบบ Value การเรียกใช้งานมันจะไม่เปลี่ยนแปลงค่าของ Rectangle ในขณะที่ SetLength เป็นเมทอดที่รับค่าแบบ Pointer โดยการเรียกใช้เมทอดนี้จะมีผลกับอินสแตนซ์ Rectangle ต้นฉบับ

6.3 การเรียกใช้เมทอด

คุณสามารถเรียกใช้เมทอดของ struct โดยใช้ตัวแปร struct และตัวชี้ของมันได้

func main() {
    rect := Rectangle{length: 10, width: 5}

    // เรียกใช้เมทอดที่มีตัวรับค่าแบบ Value
    fmt.Println("พื้นที่:", rect.Area())

    // เรียกใช้เมทอดที่มีตัวรับค่าแบบ Value
    fmt.Println("เส้นรอบ:", rect.Perimeter())

    // เรียกใช้เมทอดที่มีตัวรับค่าแบบ Pointer
    rect.SetLength(20)

    // เรียกใช้เมทอดที่มีตัวรับค่าแบบ Value อีกครั้ง โปรดสังเกตว่าความยาวได้รับการแก้ไข
    fmt.Println("หลังการแก้ไข, พื้นที่:", rect.Area())
}

เมื่อคุณเรียกใช้เมทอดโดยใช้ตัวชี้ Go จะจัดการแปลงระหว่างค่าและตัวชี้โดยอัตโนมัติ ไม่ว่าเมทอดของคุณจะถูกกำหนดด้วยตัวรับค่าแบบ Value หรือตัวรับค่าแบบ Pointer

6.4 การเลือกประเภทของตัวรับค่า

เมื่อกำหนดเมทอดคุณควรตัดสินใจว่าจะใช้ตัวรับค่าแบบ Value หรือตัวรับค่าแบบ Pointer ขึ้นอยู่กับสถานการณ์ ต่อไปนี้คือคำแนะนำที่พบบ่อย:

  • หากเมทอดต้องการแก้ไขเนื้อหาของโครงสร้างให้ใช้ตัวรับค่าแบบ Pointer
  • หากโครงสร้างใหญ่และค่าใช้สำเนาสูงให้ใช้ตัวรับค่าแบบ Pointer
  • หากคุณต้องการให้เมทอดแก้ไขค่าที่ตัวรับค่าชี้ไปที่ให้ใช้ตัวรับค่าแบบ Pointer
  • เพราะเหตุผลด้านประสิทธิภาพ แม้แม้ว่าคุณจะไม่แก้ไขเนื้อหาของโครงสร้าง มีเหตุผลที่ดีที่จะใช้ตัวรับค่าแบบ Pointer สำหรับโครงสร้างที่ใหญ่
  • สำหรับโครงสร้างขนาดเล็ก หรือเมื่อต้องการอ่านข้อมูลโดยไม่ต้องการแก้ไขให้ใช้ตัวรับค่าแบบ Value เป็นที่มักจะง่ายและมีประสิทธิภาพมากกว่า

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

7 โครงสร้างและการตรวจสอบ JSON

ใน Go มักจะมีความจำเป็นที่จะต้องแปลงโครงสร้างเป็นรูปแบบ JSON เพื่อการสื่อสารทางเครือข่ายหรือเป็นไฟล์การกำหนดค่า ในทำนองเดียวกันเราก็ต้องสามารถที่จะแปลง JSON เป็นตัวอินสแตนซ์ของโครงสร้าง แพ็กเกจ encoding/json ใน Go จัดหน้าที่ให้ทำงานนี้

นี่คือตัวอย่างเพื่อแปลงระหว่างโครงสร้างและ JSON:

package main

import (
	"encoding/json"
	"fmt"
	"log"
)

// กำหนดโครงสร้าง Person และใช้แท็ก JSON เพื่อกำหนดการแมประหว่างโอเจกต์ของโครงสร้างและชื่อฟิลด์ของ JSON
type Person struct {
	Name   string   `json:"name"`
	Age    int      `json:"age"`
	Emails []string `json:"emails,omitempty"`
}

func main() {
	// สร้างตัวอินสแตนซ์ของ Person
	p := Person{
		Name:   "John Doe",
		Age:    30,
		Emails: []string{"[email protected]", "[email protected]"},
	}

	// แปลงเป็น JSON
	jsonData, err := json.Marshal(p)
	if err != nil {
		log.Fatalf("การกำหนดรูปแบบ JSON ล้มเหลว: %s", err)
	}
	fmt.Printf("รูปแบบ JSON: %s\n", jsonData)

	// แปลงเป็นโครงสร้าง
	var p2 Person
	if err := json.Unmarshal(jsonData, &p2); err != nil {
		log.Fatalf("การนำรูปแบบ JSON มาแปลงเป็นโครงสร้างล้มเหลว: %s", err)
	}
	fmt.Printf("โครงสร้างที่กลับมา: %#v\n", p2)
}

ในโค้ดข้างต้น เรากำหนดโครงสร้าง Person โดยรวมถึงฟิลด์ประเภท Slice ด้วยโปรตบ์ "omitempty" ตัวเลือกนี้ระบบบ่งบไช้ว่าหากฟิลด์ว่างหรือขาดไป จะไม่ถูกรวมเข้าใน JSON

เราใช้ฟังก์ชัน json.Marshal เพื่อแปลงตัวอินสแตนซ์ของโครงสร้างเป็น JSON และ json.Unmarshal เพื่อแปลงข้อมูล JSON เป็นตัวอินสแตนซ์ของโครงสร้าง

8 หัวข้อขั้นสูงในโครงสร้าง

8.1 การเปรียบเทียบ Structs

ใน Go นั้น อนุญาตให้เราเปรียบเทียบสอง instances ของ structs โดยตรง แต่การเปรียบเทียบนี้จะขึ้นอยู่กับค่าของฟิลด์ภายใน structs ถ้าค่าของฟิลด์ทั้งหมดเท่ากัน แล้ว instances สองตัวของ structs จะถูกพิจารณาเป็นเท่ากัน ควรทราบว่าไม่ทุกชนิดของฟิลด์สามารถเปรียบเทียบได้ ตัวอย่างเช่น structs ที่มี slices ไม่สามารถเปรียบเทียบโดยตรง

ด้านล่างเป็นตัวอย่างการเปรียบเทียบ structs:

package main

import "fmt"

type Point struct {
	X, Y int
}

func main() {
	p1 := Point{1, 2}
	p2 := Point{1, 2}
	p3 := Point{1, 3}

fmt.Println("p1 == p2:", p1 == p2) // Output: p1 == p2: true
fmt.Println("p1 == p3:", p1 == p3) // Output: p1 == p3: false
}

ในตัวอย่างนี้ p1 และ p2 ถูกพิจารณาว่าเท่ากันเพราะค่าของฟิลด์ทั้งหมดเหมือนกัน และ p3 ไม่เท่ากับ p1 เพราะค่าของ Y ต่างกัน.

8.2 การคัดลอก Structs

ใน Go, instances ของ structs สามารถถูกคัดลอกด้วยการกำหนดค่า ว่าคัดลอกเป็น deep copy หรือ shallow copy จะขึ้นอยู่กับชนิดของฟิลด์ภายใน structs

ถ้า structs ประกอบด้วยชนิดพื้นฐานเท่านั้น (เช่น int, string, เป็นต้น) การคัดลอกจะเป็น deep copy ถ้า structs ประกอบด้วยชนิดที่อ้างอิง (เช่น slices, maps, เป็นต้น) การคัดลอกจะเป็น shallow copy และ instances ต้นฉบับและ instances ที่คัดลอกจะแชร์หน่วยความจำของชนิดที่อ้างอิง

ต่อไปนี้เป็นตัวอย่างการคัดลอก structs:

package main

import "fmt"

type Data struct {
Numbers []int
}

func main() {
// กำหนดค่าให้ instances ของ Data struct
original := Data{Numbers: []int{1, 2, 3}}

// คัดลอก structs
copied := original

// แก้ไขสมาชิกใน slice ที่ถูกคัดลอก
copied.Numbers[0] = 100

// ดูสมาชิกของ instances ต้นฉบับและที่คัดลอก
fmt.Println("Original:", original.Numbers) // Output: Original: [100 2 3]
fmt.Println("Copied:", copied.Numbers) // Output: Copied: [100 2 3]
}

ตามตัวอย่าง instances original และ copied แชร์ slice เดียวกัน ดังนั้นการแก้ไขข้อมูลใน copied จะส่งผลต่อข้อมูลใน original สามารถหลีกเลี่ยงปัญหานี้ได้โดยการคัดลอกค่าข้อมูลจริงของ slice ไปยัง slice ใหม่:

newNumbers := make([]int, len(original.Numbers))
copy(newNumbers, original.Numbers)
copied := Data{Numbers: newNumbers}

โดยวิธีนี้ การแก้ไข copied จะไม่ส่งผลต่อ original.