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
.