Data structure กับ Go
รูปแบบ Data อื่นๆที่สามารถเก็บได้
นอกเหนือจากการเก็บข้อมูลประเภท String (ตัวอักษร), Number (ตัวเลข) และ Boolean (true, false) แล้ว ในภาษา go ยังมีรูปแบบข้อมูลแบบอื่นที่สามารถเก็บได้ โดยจะเน้นไปที่จุดประสงค์ของรูปแบบการเก็บข้อมูล (แทนที่จะเป็นประเภทข้อมูล) ซึ่งจะประกอบไปด้วย
- Array = รูปแบบการเก็บข้อมูลเป็น sequence เก็บหลายข้อมูลในตัวแปรเดียว กันไว้ เช่น
var a [5]int
- Slice = คล้ายๆ Array แต่อนุญาตให้เปลี่ยนขนาดได้ (จากแต่เดิม Array ที่ต้องระบุขนาดเสมอ) เช่น
var s[]int
- Map = ข้อมูล map ที่คล้ายๆ dictionary ที่สามารถเก็บ key คู่กับ value ตรงๆไว้ได้ (จากเดิมต้องระบุเป็นตำแหน่ง) เช่น
map[string]int
- Struct = ตัวแปรที่ประกอบไปด้วยกลุ่มของ Variable ออกมาเป็นตัวแปรเดียว (ลักษณะคล้ายๆ Object ในภาษาอื่นๆ) โดยสามารถก ำหนดชื่อ field และ type คู่กันไว้ได้ เช่น
type Person struct {
Name string
Age int
}
- Function = ในภาษา go function ถูกจัดอยู่ในประเภทหนึ่งของ data type โดยสามารถทำการประกาศเป็นเหมือนตัวแปรตัวแปรหนึ่งขึ้นมาได้ เช่น
var squareFunc func(int) int = square
เป็นการบอกว่า สร้างตัวแปร function รับเป็น integer และคืนค่าเป็น integer (เดี๋ยวเรามาลงลึกใน function อีกที) - Interface = data type ที่จะทำการระบุ set ของ method ที่จะต้องมีในกลุ่มที่จะใช้ data type ของตัวนี้ (เป็นเหมือนสร้างต้นแบบของ function เอาไว้)
type Shape interface {
Area() float64
Perimeter() float64
}
- Pointer = data type ที่ทำการเก็บ memory address ของ variable เอาไว้ เช่น
var p *int
- Channel = data type ใช้สำหรับการ communication ระหว่าง goroutines (ตัวสำหรับทำ concurrent thread ของการ execution ของ go) โดยประกาศเป็น
chan int
(** ตัวนี้เราจะยังไม่พูดถึงในหัวข้อนี้ เนื่องจากมีรายละเอียดพอสมควร เราจะเก็บไว้เล่ากันช่วงกลางๆ Course อีกที)
Array and slice
Array และ Slice ใช้สำหรับเก็บข้อมูลที่เป็น sequence ของ Variable ใช้สำหรับการเก็บข้อมูลประเภทเดียวกันแต่เก็บทีละหลายตัวไปพร้อมๆกันได้
- โดยทั้ง Array และ Slice จะต้องประกาศ
[]
เพิ่มเพื่อเป็นการบอกว่าตัวแปรนี้จะเป้นประเภท Array - โดย Array จะต้องระบุขนาดตอนประกาศว่าจะใช้ตัวแปรประเภทนี้เป็น Array ที่มีขนาด (จำนวนตัวแปรที่จะใช้งาน) จำนวนเท่าไหร่ เช่น ถ้าจะใช้งาน 3 ตัวก็ต้องประกาศเป็น
var myArray[3] int
เป็นต้น - Slice จะกลับกัน slice ไม่จำเป็นต้องประกาศขนาดของ Array ไว้ก่อน สามารถใส่เพียง
[]
และค่อยมาจัดการเพิ่ม data ได้
เรามาลองดูตัวอย่างการใช้ Array และ Slice กัน
Array
var myArray [3]int // An array of 3 integers
myArray[0] = 10 // Assign values
myArray[1] = 20
myArray[2] = 30
fmt.Println(myArray) // Output: [10 20 30]
ผลลัพธ์
ดังนั้น ด้วยคุณสมบัติของ Array เราจึงสามารถใช้งานร่วมกับ loop ในการแสดงผลข้อมูล โดยการใช้ "ขนาดของ Array" (length) ในการเป็นตัวแทนของการวน loop ได้
var myArray [3]int // An array of 3 integers
myArray[0] = 10 // Assign values
myArray[1] = 20
myArray[2] = 30
// Looping through the array
for i := 0; i < len(myArray); i++ {
fmt.Println(myArray[i])
}
ก็จะสามารถแสดงผล array รายตัวได้
จุดพิจารณาของการใช้ Array
- Array ไม่สามารถทำการ assign ซ้ำได้ หากจะ update ต้อง update เป็นราย index เช่น ถ้าเราทำแบบนี้
var myArray [3]int // An array of 3 integers
myArray[0] = 10 // Assign values
myArray[1] = 20
myArray[2] = 30
fmt.Println(myArray) // Output: [10 20 30]
myArray = [10, 20, 30]
fmt.Println(myArray) // Output: [10 20 30]
code จะเกิด error ออกมาทันที เนื่องจาก go มองว่า เรากำลังพยายาม assign ตัวแปรที่มีขนาดแตกต่างจากเดิมเข้าไปใหม่
แต่ยังคงสามารถที่จะเปลี่ยนแปลงข้อมูลราย index ได้
var myArray [3]int // An array of 3 integers
myArray[0] = 10 // Assign values
myArray[1] = 20
myArray[2] = 30
// Reassigning the elements of the array
myArray[0] = 100
myArray[1] = 200
myArray[2] = 300
fmt.Println(myArray) // Output: [100 200 300]
- ไม่สามารถที่จะ resize Array ได้ หากประกาศใช้แล้ว ต้องใช้ขนาดนี้เท่านั้น
Array จึงเหมาะกับขนาดข้อมูลที่มีความแน่นอนเช่น
- เราประกาศมาจากการดึงข้อมูลที่มีขนาดแน่นอน
- ข้อดีที่แตกต่างกับ Slice คือ มันจะกิน memory ตามขนาดที่มันจองทันที (จะไม่เจอปัญหา memory leak ได้ ขณะแก้ไขข้อมูล)
Slice
slice จะมีความแตกต่างกับ Array คือ
- ไม่ต้องประกาศขนาด สามารถประกาศ
[]
แล้วใช้งานได้เลย - จะกำหนดค่าเริ่มต้น หรือไม่กำหนดค่าเริ่มต้นก็ได้
- สามารถใช้คำสั่ง
append
ในการเพิ่มข้อมูลเข้า array ได้ (ด้วยคุณสมบัติที่ยืดหยุ่นทำให้เราสามารถใช้คำสั่งappend
ทั้งเพิ่มและลบข้อมูลได้) - สามารถระบุ index ระหว่างกลางเพื่อดึงข้อมูลออกมาตามขนาดที่เพิ่มขึ้น เช่น
myslice[1:3]
เก็บการหยิบ index ตั้งแต่ 1 ถึง 3 ออกมา (Array ก็สามารถทำได้ แต่ต้องทำในขนาดที่ประกาศเอาไว้)
package main
import (
"fmt"
)
func main() {
mySlice := []int{10, 20, 30, 40, 50} // A slice of integers
fmt.Println(mySlice) // Output: [10 20 30 40 50]
fmt.Println(len(mySlice)) // Length of the slice: 5
fmt.Println(cap(mySlice)) // Capacity of the slice: 5
// Slicing a slice
subSlice := mySlice[1:3] // Slice from index 1 to 2
fmt.Println(subSlice) // Output: [20 30]
}
ซึ่ง slice สามารถประยุกต์ใช้กับการต่อข้อมูลได้ เช่น
package main
import (
"fmt"
)
func main() {
var mySlice []int // Declared but not initialized
// Appending data to the slice
mySlice = append(mySlice, 10)
mySlice = append(mySlice, 20, 30)
fmt.Println(mySlice) // Output: [10 20 30]
}
- Array สามารถ convert มาเป็น slice ได้
package main
import (
"fmt"
)
func main() {
var myArray [3]int // An array of 3 integers
myArray[0] = 10 // Assign values
myArray[1] = 20
myArray[2] = 30
// Converting array to slice
mySlice := myArray[:]
// Resizing slice by appending new elements
mySlice = append(mySlice, 40, 50)
fmt.Println(mySlice) // Output will show a slice with 5 elements: [10 20 30 40 50]
}
ความแตกต่างอีก 1 อย่างระหว่าง Slice และ Array คือ
- เมื่อตอนส่งเข้า function Array จะส่งข้อมูลเข้าไปตรงๆ (เป็นการ pass by value)
- แต่ตอนส่งเข้า function ด้วย Slice จะเป็นการส่ง Reference เข้าไปแทน (เป็นการ pass by reference)
** เดี๋ยวเราจะพูดถึงในหัวข้อ function อีกที
ข้อควรระวังของการใช้ Slice คือ
- การประกาศ Slice มาใหม่เพิ่มเรื่อยๆ จะเท่ากับการจอง array ใหม่เพิ่มขึ้นเสมอ อาจจะส่งผลทำให้เกิดภาวะ memory ค้างไว้อยู่ได้ (อาจจะส่งผลทำให้ใช้ memory เกินความจำเป็นได้)
- รวมถึงการ pass by reference ของ Slice อาจจะส่งผลทำให้ข้อมูลสามารถโดนเปลี่ยนแปลงระหว่างทางการส่งข้อมูลเข้าไปได้
Map
Map คือ data type ที่ใช้สำหรับเก็บ key คู่กับ value เอาไว้
- จาก Array, Slice จะเป็นการเก็บ index ตามลำดับ sequence ของข้อมูล
- แต่ map สามารถเก็บ key ตามที่ต้องการคู่กับ value ไว้ได้ (จะให้ idea เหมือน dictionary / hash table ในบางภาษาคือสามารถจิ้มไปที่ key และดึง value ออกมาได้เลย)
- map เป็น unordered collection ที่แต่ละ key "ต้อง unique" และจะต้อง map กับ value เสมอ (เมื่อมีการประกาศ key)
โดย คุณสมบัติของ Key คือ
- key ต้อง unique. 1 key สามารถมีได้เพียง 1 value เท่าั้น
- key ใน map จะ "ไม่ guaranteed" ว่าถูกเรียงอย่างถูกต้องหรือไม่ (ถ้าซีเรียสว่าข้อมูลต้องเรียงก ัน ไม่แนะนำให้ใช้ map หรือควร sort ก่อน)
- Map เป็น reference types เมื่อมีการส่งเข้า function จะเป็นการ pass by reference เช่นเดียวกันกับ Slice
มาดูตัวอย่างการใช้ map กัน
- เริ่มต้นด้วยการประกาศ
make(map[<ประเภท key>]<ประเภท value>)
- คำสั่ง
make
คือคำสั่ง initialize maps เพื่อเป็นการจอง memory สำหรับการใช้ map และสร้าง data structure ประเภทนี้ขึ้นมา (แบบ reference types) = เป็นคำสั่งสำหรับทำ memory allocation เพื่อให้แน่ใจว่ามี memory อยู่จริงสำหรับการใช้งานตัวแปร
myMap := make(map[string]int)
// Add key-value pairs to the map
myMap["apple"] = 5
myMap["banana"] = 10
myMap["orange"] = 8
// Access and print a value for a key
fmt.Println("Apples:", myMap["apple"])
// Update the value for a key
myMap["banana"] = 12
// Delete a key-value pair
delete(myMap, "orange")
// Iterate over the map
for key, value := range myMap {
fmt.Printf("%s -> %d\n", key, value)
}
// Checking if a key exists
val, ok := myMap["pear"]
if ok {
fmt.Println("Pear's value:", val)
} else {
fmt.Println("Pear not found inmap")
}
ผลลัพธ์
จากตัวอย่าง
- map สามารถที่จะเพิ่มข้อมูลได้ผ่านการระบุ key
- สามารถลบข้อมูลได้ผ่าน function
delete
- สามารถเช็คว่า key มีอยู่จริงได้จากการรับตัวแปรมา 2 ตัว คือ
val, ok
โดยval
คือตัวแทนของค่าที่กำลังหาok
คือ boolean (ที่คืนมาจากคำสั่ง map) เป็นตัวที่บอกว่าเจอหรือไม่ ออกมาได้ (เป็น built-in function ของ go)
เคสไหนควรใช้ map บ้าง
- Fast Lookup = search จาก key จะค้นหาได้ไวกว่า search จาก array (Caching)
- Unique Key = key สำคัญ ซ้ำกันไม่ได้ พวก map จะช่วยในการจัดการข้อมูลที่ต้องมี unique ได้
- Dynamic = ขนาด data เป็น dynamic และสามารถเพิ่ม / ลบข้อมูลได้ตลอดเวลา
Struct
Struct คือ data type ที่ทำการรวม data หลายประเภทเข้ามาไว้ในตัวแปรตัวเดียวกัน
โจทย์ของ structure นั้นจะใช้สำหรับเก็บข้อมูลที่มีความ Complex กว่า data ปกติ รวมถึงต้องการจำกัดโครงสร้างของข้อมูลให้ออกมาหน้าตาถูกต้อง = struct จะช่วยในเคสนี้ได้
คุณสมบัติใหญ่ๆของ struct
- ช่วยในการ group data ได้ ในกรณีที่ต้องการเก็บข้อมูลเอาไว้ด้วยกัน เช่น ถ้าเราต้องการเก็บข้อมูลนักเรียนที่มี ส่วนสูง, น้ำหนัก และ เกรด เอาไว้ด้วยกัน การประกาศ struct จะทำให้สามารถรวมข้อมูลทั้งหมดมาไว้ในตัวแปรเพียงตัวเดียวได้ เช่นแบบนี้
type Student struct {
Name string
Height int
Weight int
Grade string
}
- เราสามารถประกาศ structs หน้าตายังไงก็ได้ (ข้อแค่ระบุ field ที่ต้องการเก็บก็พอ)
- struct ถือเป็นการเก็บแบบ value type ดังนั้นเมื่อทำการส่งเข้า function จะเป็นการส่งเพียง value เข้าไป รวมถึงการ assign struct เข ้าตัวแปรใหม่ = ก็จะเป็นการสร้างตัวแปรใหม่และส่ง value ไปเท่านั้น
มาดูตัวอย่าง code struct จากตัวอย่างของนักเรียนกัน
package main
import "fmt"
// Define a struct type
type Student struct {
Name string
Weight int
Height int
Grade string
}
func main() {
// Create an instance of the Student struct
var student1 Student
student1.Name = "Mikelopster"
student1.Weight = 60
student1.Height = 180
student1.Grade = "F"
// Print struct values
fmt.Println(student1)
}
ผลลัพธ์
จาก code ด้านบน
- อย่างที่เห็นว่าเราสามารถ assign แต่ละ key ของ struct ออกมาได้
- จริงๆเราสามารถลดการเขียนลงเหลือเพียงเท่านี้ได้ (ไม่จำเป็นต้องมาระบุราย key เหมือนเดิม)
// Create an instance of the Student struct
var student1 Student = Student{
Name: "Mikelopster",
Weight: 60,
Height: 180,
Grade: "F",
}
// Print struct values
fmt.Println(student1)
รวมถึงสามารถใช้งานร่วมกับ data type ประเภทอื่นๆได้ เช่น
- ใช้งานร่วมกับ array
package main
import "fmt"
// Define a struct type
type Student struct {
Name string
Weight int
Height int
Grade string
}
func main() {
// Create an array of Student structs
var students [3]Student
// Initialize the first student
students[0] = Student{
Name: "Mikelopster",
Weight: 60,
Height: 180,
Grade: "F",
}
// Initialize the second student
students[1] = Student{
Name: "Alice",
Weight: 55,
Height: 165,
Grade: "A",
}
// Initialize the third student
students[2] = Student{
Name: "Bob",
Weight: 68,
Height: 175,
Grade: "B",
}
// Print array of structs
fmt.Println(students)
}
- ใช้งานร่วมกับ map
package main
import "fmt"
// Define the Student struct
type Student struct {
Name string
Weight int
Height int
Grade string
}
func main() {
// Create a map with string keys and Student struct values
students := make(map[string]Student)
// Add Student structs to the map
students["st01"] = Student{Name: "Mikelopster", Weight: 60, Height: 180, Grade: "F"}
students["st02"] = Student{Name: "Alice", Weight: 55, Height: 165, Grade: "A"}
students["st03"] = Student{Name: "Bob", Weight: 68, Height: 175, Grade: "B"}
// Print the map
fmt.Println("Students Map:", students)
// Access and print a specific student by key
fmt.Println("Student st01:", students["st01"])
}
- รวมถึงใช้งานร่วมกับ struct กันเองก็ได้
package main
import "fmt"
// Define a struct type
type Person struct {
Name string
Age int
Address Address
}
// Another struct type used in Person
type Address struct {
Street string
City string
ZipCode int
}
func main() {
// Create an instance of the Person struct
var person Person
person.Name = "Alice"
person.Age = 30
person.Address = Address{
Street: "123 Main St",
City: "Gotham",
ZipCode: 12345,
}
// Alternative way to initialize a struct
bob := Person{
Name: "Bob",
Age: 25,
Address: Address{
Street: "456 Elm St",
City: "Metropolis",
ZipCode: 67890,
},
}
// Print struct values
fmt.Println(person)
fmt.Println(bob)
}
และนี่ก็คือ data type 3 ประเภทแรกที่เน้นเก็บข้อมูลเป็นหลัก
ตัวต่อไปเราจะพูดถึง function ที่เป็น data type ที่เน้นความเป็น functional มากขึ้น