keyboard_tabSkip to content
listGoAPI Essential

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 คู่กันไว้ได้ เช่น
go
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 เอาไว้)
go
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

go
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 ได้

go
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 เช่น ถ้าเราทำแบบนี้
go
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 ได้

go
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 ก็สามารถทำได้ แต่ต้องทำในขนาดที่ประกาศเอาไว้)
go
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 สามารถประยุกต์ใช้กับการต่อข้อมูลได้ เช่น

go
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 ได้
go
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 อยู่จริงสำหรับการใช้งานตัวแปร
go
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 จะทำให้สามารถรวมข้อมูลทั้งหมดมาไว้ในตัวแปรเพียงตัวเดียวได้ เช่นแบบนี้
go
type Student struct {
  Name    string
  Height  int
  Weight  int
  Grade   string
}
  • เราสามารถประกาศ structs หน้าตายังไงก็ได้ (ข้อแค่ระบุ field ที่ต้องการเก็บก็พอ)
  • struct ถือเป็นการเก็บแบบ value type ดังนั้นเมื่อทำการส่งเข้า function จะเป็นการส่งเพียง value เข้าไป รวมถึงการ assign struct เข้าตัวแปรใหม่ = ก็จะเป็นการสร้างตัวแปรใหม่และส่ง value ไปเท่านั้น

มาดูตัวอย่าง code struct จากตัวอย่างของนักเรียนกัน

go
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 เหมือนเดิม)
go
// 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
go
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
go
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 กันเองก็ได้
go
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 มากขึ้น