Skip to main content

Data structure กับ Go

รูปแบบ Data อื่นๆที่สามารถเก็บได้

นอกเหนือจากการเก็บข้อมูลประเภท String (ตัวอักษร), Number (ตัวเลข) และ Boolean (true, false) แล้ว ในภาษา go ยังมีรูปแบบข้อมูลแบบอื่นที่สามารถเก็บได้ โดยจะเน้นไปที่จุดประสงค์ของรูปแบบการเก็บข้อมูล (แทนที่จะเป็นประเภทข้อมูล) ซึ่งจะประกอบไปด้วย

  1. Array = รูปแบบการเก็บข้อมูลเป็น sequence เก็บหลายข้อมูลในตัวแปรเดียว กันไว้ เช่น var a [5]int
  2. Slice = คล้ายๆ Array แต่อนุญาตให้เปลี่ยนขนาดได้ (จากแต่เดิม Array ที่ต้องระบุขนาดเสมอ) เช่น var s[]int
  3. Map = ข้อมูล map ที่คล้ายๆ dictionary ที่สามารถเก็บ key คู่กับ value ตรงๆไว้ได้ (จากเดิมต้องระบุเป็นตำแหน่ง) เช่น map[string]int
  4. Struct = ตัวแปรที่ประกอบไปด้วยกลุ่มของ Variable ออกมาเป็นตัวแปรเดียว (ลักษณะคล้ายๆ Object ในภาษาอื่นๆ) โดยสามารถกำหนดชื่อ field และ type คู่กันไว้ได้ เช่น
type Person struct {
Name string
Age int
}
  1. Function = ในภาษา go function ถูกจัดอยู่ในประเภทหนึ่งของ data type โดยสามารถทำการประกาศเป็นเหมือนตัวแปรตัวแปรหนึ่งขึ้นมาได้ เช่น var squareFunc func(int) int = square เป็นการบอกว่า สร้างตัวแปร function รับเป็น integer และคืนค่าเป็น integer (เดี๋ยวเรามาลงลึกใน function อีกที)
  2. Interface = data type ที่จะทำการระบุ set ของ method ที่จะต้องมีในกลุ่มที่จะใช้ data type ของตัวนี้ (เป็นเหมือนสร้างต้นแบบของ function เอาไว้)
type Shape interface {
Area() float64
Perimeter() float64
}
  1. Pointer = data type ที่ทำการเก็บ memory address ของ variable เอาไว้ เช่น var p *int
  2. 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]

ผลลัพธ์ go-control-01

ดังนั้น ด้วยคุณสมบัติของ 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

  1. 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]
  1. ไม่สามารถที่จะ resize Array ได้ หากประกาศใช้แล้ว ต้องใช้ขนาดนี้เท่านั้น

Array จึงเหมาะกับขนาดข้อมูลที่มีความแน่นอนเช่น

  • เราประกาศมาจากการดึงข้อมูลที่มีขนาดแน่นอน
  • ข้อดีที่แตกต่างกับ Slice คือ มันจะกิน memory ตามขนาดที่มันจองทันที (จะไม่เจอปัญหา memory leak ได้ ขณะแก้ไขข้อมูล)

Slice

slice จะมีความแตกต่างกับ Array คือ

  1. ไม่ต้องประกาศขนาด สามารถประกาศ [] แล้วใช้งานได้เลย
  2. จะกำหนดค่าเริ่มต้น หรือไม่กำหนดค่าเริ่มต้นก็ได้
  3. สามารถใช้คำสั่ง append ในการเพิ่มข้อมูลเข้า array ได้ (ด้วยคุณสมบัติที่ยืดหยุ่นทำให้เราสามารถใช้คำสั่ง append ทั้งเพิ่มและลบข้อมูลได้)
  4. สามารถระบุ 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")
}

ผลลัพธ์ go-control-01

จากตัวอย่าง

  • 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)
}

ผลลัพธ์ go-control-01

จาก 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 มากขึ้น