Go Arrays & Slices

Phai Panda
6 min readSep 23, 2024

--

ในงานมีใช้ — แต่ไม่ได้ลงลึก เมื่อไปเรียนจึงเกิดความเข้าใจ จึงอยากจะนำมาบอกเล่ากับเพื่อนๆอย่างเคยๆครับ

ประกอบด้วยหัวข้อ

  • Zero values
  • Length and Capacity
  • Slice Indices
  • Slice Appending

Zero values

มันคือการประกาศตัวแปร ระบุชนิดข้อมูล แต่ไม่ระบุค่า อ้างอิง

package main

import "fmt"

type myStruct struct {
name string
age int
}

func main() {
var i int
var f float64
var b bool
var p *int
var myStruct myStruct
var s string

fmt.Printf("i=%v f=%v b=%v p=%v myStruct=%+v s=%q\n", i, f, b, p, myStruct, s)
}
i=0 f=0 b=false p=<nil> myStruct={name: age:0} s=""

หากสนใจ verbs ของ fmt อ้างอิง เช่น %v คือ value ส่วน %+v คือเพิ่มชื่อฟิลด์ให้ด้วย ส่วน %q เพราะเป็นสตริงเฉยๆเราจะมองไม่เห็น จึงเพิ่ม quotes ให้

Arrays

ต้องกำหนดจำนวนสมาชิกหรือทำให้ทราบขนาดที่แน่นอนเสมอ กลายเป็นข้อจำกัดอย่างหนึ่งของมัน

package main

import "fmt"

func main() {
var a1 [3]int
fmt.Printf("%v\n", a1)
}
[0 0 0]

หาก array อยากขยายขนาดล่ะ? ให้ไปดู slice

Slices

ไม่จำเป็นต้องกำหนดขนาด และ zero value ของมันมีค่าเป็น nil

package main

import "fmt"

func main() {
var s1 []int
fmt.Printf("%v %v\n", s1, s1 == nil)
}
[] true

Length and Capacity

เราสามารถหา length (number of elements) ได้จากฟังก์ชัน len และความจุได้จากฟังก์ชัน cap

capacity หรือ cap ของ array มีค่าเท่ากับ length เสมอ ทว่า capacity ของ slice สามารถมีได้มากกว่า length

capacity นี้ชี้ว่า maximum number of elements มีอยู่เท่าไหร่ที่ slice สามารถถือครองไว้ ซึ่งสามารถเปลี่ยนแปลง เช่น เพิ่มขึ้น/ลดลง ได้ในภายหลัง

slice แบบกำหนดจำนวนสมาชิกเอง พิมพ์ค่าออกมาอย่างธรรมดาที่สุด

package main

import "fmt"

func main() {
s1 := []int{1, 2, 3}
fmt.Println(s1)
}
[1 2 3]

ก็จะมีค่าของ length และ capacity เท่ากัน คือ 3

package main

import "fmt"

func main() {
s1 := []int{1, 2, 3}
fmt.Printf("len=%v cap=%v\n", len(s1), cap(s1))
}
len=3 cap=3

ทีนี้ถ้าอยากำหนดจำนวนของ capacity ให้กับ slice เองล่ะ ทำอย่างไร? คำตอบคือมีฟังก์ชัน make([]T, len, cap) ให้ใช้ อ้างอิง

package main

import "fmt"

func main() {
s1 := make([]int, 3, 5)
fmt.Printf("len=%v cap=%v\n", len(s1), cap(s1))
}
len=3 cap=5

เมื่อนำตัวอย่างนี้วาดเป็นภาพ จึงได้ว่า

s1 slice

ฉะนั้นเวลาพิมพ์ออกมาจึงเท่ากับจำนวนสมาชิกที่มี (make ทำให้ — สีเขียว) ตาม len=3 ไม่เชื่อ ลอง loop ด้วย range ดูสิ

package main

import "fmt"

func main() {
s1 := make([]int, 3, 5)

fmt.Println("loop")
for i, v := range s1 {
fmt.Printf("%v: %v\n", i, v)
}

fmt.Println("it self")
fmt.Printf("%v\n", s1)
}
loop
0: 0
1: 0
2: 0
it self
[0 0 0]

จากภาพตัวอย่างทำให้เราทราบว่าแท้จริงแล้ว slice นั้นใช้ pointer อ้างอิงไปยัง array อีกที เรียกว่า underlying array เพราะตัวมันเองไม่ได้เก็บข้อมูลใดๆไว้เลย

Slice Indices

เพื่อนๆทราบอยู่แล้วว่า index ของ array เริ่มต้นที่ 0 และใช้แค่หนึ่ง index แต่ของ slice จะมีถึง 3 indices เบื้องต้นขอเป็น 2 ตำแหน่งแรกก่อน คือ low กับ high อ้างอิง

สัญลักษณ์ [low:high]

package main

import "fmt"

func main() {
s1 := []int{10, 20, 30}
s2 := s1[1:3]
s3 := s1[:3]
s4 := s1[1:]
s5 := s1[:]

fmt.Printf("s1=%v\n", s1)
fmt.Printf("s2=%v\n", s2)
fmt.Printf("s3=%v\n", s3)
fmt.Printf("s4=%v\n", s4)
fmt.Printf("s5=%v\n", s5)
}
s1=[10 20 30]
s2=[20 30]
s3=[10 20 30]
s4=[20 30]
s5=[10 20 30]
  • ไม่กำหนด low เช่น [:3] หมายถึง เริ่มที่ 0 ถึง 3–1 คือ 2
  • ไม่กำหนด high เช่น [1:] หมายถึง เริ่มที่ 1 ถึง n-1
  • ไม่กำหนด low และ high เช่น [:] หมายถึง เริ่มที่ 0 ถึง n-1

นำตัวอย่างนี้วาดเป็นภาพ จึงได้ว่า

slice indices

ลองเปลี่ยนค่า 30 ของ s4 เป็น 100 สิ่งที่เกิดขึ้นคือทุก slice เปลี่ยนหมด (underlying array)

package main

import "fmt"

func main() {
s1 := []int{10, 20, 30}
s2 := s1[1:3]
s3 := s1[:3]
s4 := s1[1:]
s5 := s1[:]

fmt.Println("before ----- ")
print(s1)
print(s2)
print(s3)
print(s4)
print(s5)

s4[1] = 100

fmt.Println("after ----- ")
print(s1)
print(s2)
print(s3)
print(s4)
print(s5)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
before ----- 
len=3 cap=3 [10 20 30]
len=2 cap=2 [20 30]
len=3 cap=3 [10 20 30]
len=2 cap=2 [20 30]
len=3 cap=3 [10 20 30]
after -----
len=3 cap=3 [10 20 100]
len=2 cap=2 [20 100]
len=3 cap=3 [10 20 100]
len=2 cap=2 [20 100]
len=3 cap=3 [10 20 100]

ขนาดของ slice ยังสามารถขยายได้ทันทีอีกด้วย อ้างอิง

package main

import "fmt"

func main() {
s := []int{2, 3, 5, 7, 11, 13}
print(s)

s = s[:0]
print(s)

s = s[:4]
print(s)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

มาไล่โค้ดข้างต้นนี้กัน ให้ s เป็น slice มีสมาชิกเท่ากับ 6

จากนั้นเลือกสมาชิกแรกจนถึง 0 ([:0]) ผลคือได้สมาชิกเท่ากับ 0

จาก slice ก่อนหน้าเลือกสมาชิกแรกจนถึง 4 ([:4])

ผลลัพธ์, length ของมันเพิ่มขึ้น — ธรรมดา

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]

จาก slice ก่อนหน้า (เอาอีก) เพิ่ม low=2 ส่วน high ละไว้เท่าเดิม

s := []int{2, 3, 5, 7, 11, 13}
print(s)

s = s[:0]
print(s)

s = s[:4]
print(s)

s = s[2:] // low=2 ส่วน high ละไว้เท่าเดิม
print(s)

ผลลัพธ์, capacity มันลดลง — คุณพระ!

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

ยังไม่จุใจใช่ไหมล่ะ มาดูตัวอย่าง slice ที่มี 3 indices กัน

Capacity index

ตัวอย่างต่อไปนี้กำหนด array ที่มีจำนวนสมาชิกเท่ากับ 4 จากนั้นสร้าง slice จาก index ที่ 1 โดยระบุความจุ (cap) ให้น้อยกว่าจำนวนสมาชิกที่มี (len)

package main

import "fmt"

func main() {
myArray := [4]int{9, 8, 7, 6}
mySlice := myArray[1:3:3]
print(mySlice)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
len=2 cap=2 [8 7]

จากโค้ด ตัวระบุความจุ คือ capacity index ตำแหน่งที่สามของสัญลักษณ์ [low:high:capacity]

วาดรูปสิ

เพราะ capacity ที่กำหนดนั้นคือ n-1

มาลองอีกตัวอย่าง

package main

import "fmt"

func main() {
myArray := [...]int{9, 8, 7, 6, 5, 4}
mySlice := myArray[1:3:4]
print(mySlice)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

ถามว่าจะได้ len กับ cap เท่าไหร่ครับ ?

คำตอบ

len=2 cap=3 [8 7]

ทบทวน

เพื่อไม่ให้สับสน ลองตอบคำถามจากโจทย์ต่อไปนี้

โจทย์ 1

package main

import "fmt"

func main() {
myArray := [...]int{9, 8, 7, 6, 5, 4}
mySlice := myArray[2:]
print(mySlice)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

ถามว่า cap จะมีค่าเท่าไหร่ เพราะเหตุใด ?

โจทย์ 2

package main

import "fmt"

func main() {
myArray := [...]int{9, 8, 7, 6, 5, 4}
mySlice := myArray[:2]
print(mySlice)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

ถามว่า cap จะมีค่าเท่าไหร่ เพราะเหตุใด ?

Slice Appending

จำนวนสมาชิก (len) สามารถเพิ่มขึ้นกว่าที่ประกาศได้ไหม? คำตอบคือได้ โดย Go มีฟังก์ชัน append(s []T, vs …T) []T ให้ใช้เพื่อการนี้ แต่ว่า slice ที่เป็นผลลัพธ์สามารถปรากฏได้ 2 แบบ ขึ้นอยู่กับค่า capacity ที่เหลือ อ้างอิง

  1. ถ้า capacity เหลือ ผลคือ เราได้ slice ตัวเดิม (ref ไปยัง underlying array เดิม)
  2. ถ้า capacity ไม่พอ ผลคือ เราได้ slice ตัวใหม่ (ref ไปยัง newly allocated array)

อัศจรรย์อีกแล้ว~ 😊

package main

import "fmt"

func main() {
mySlice := []int{10, 20, 30, 40}
newSlice := mySlice[1:3]
newSlice = append(newSlice, 50)

fmt.Println("===> new slice")
print(newSlice)
fmt.Println("===> my slice")
print(mySlice)
}

func print(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
===> new slice
len=3 cap=3 [20 30 50]
===> my slice
len=4 cap=4 [10 20 30 50]

อธิบายได้ว่า mySlice แรกเริ่มมีสมาชิก 10, 20, 30, 40 มี len กับ cap เท่ากัน ต่อมาได้สร้าง newSlice จาก mySlice เลือกสมาชิกที่ low เท่ากับ 1 และ high เท่ากับ 3 ก็คือ 20, 30 — ธรรมดา

ถัดมาใช้ฟังก์ชัน append เพิ่มจำนวนสมาชิกไปอีก คือ 50 ให้กับ newSlice ณ ขณะนี้เมื่อ print จึงได้ว่า newSlice มีค่า 10, 30, 50 — อันนี้ไม่งงเน๊อะ

เพราะ slice เป็น pointer ดังนั้นเมื่อ newSlice ชี้ไปยัง mySlice และได้เพิ่มสมาชิก 50 นั่นทำให้ mySlice ได้รับอานิสงส์ กล่าวคือ ณ ตำแหน่งของ 50 นี้ (สร้าง slice ก็คือ underlying array) ก็คือตำแหน่งเดิมของ 40 เป็นเหตุให้พอเรา print ค่ามันออกมาจึงได้ว่า 40 กลายเป็น 50 ไปแว้วววววว — อันนี้ก็เบเบแม่นบ่

ขอให้สังเกต pointer ของ slice นั้นชี้ไปยัง array เดียวกัน นั่นเพราะยังไม่เกิน capacity ที่มี

เอาล่ะ จะใช้โค้ดข้างต้นนี้แล้วเพิ่มจำนวนสมาชิกไปอีก

newSlice = append(newSlice, 50, 60)

cap ของ newSlice คือ 3 พอเราเพิ่ม 60 เข้าไปอีก จำนวนสมาชิกตอนนี้เกิน cap ไปแล้ว ลองพิมพ์ค่ามันออกมา

===> new slice
len=4 cap=6 [20 30 50 60]
===> my slice
len=4 cap=4 [10 20 30 40]

สังเกตสิ cap ของ newSlice ขยายอัตโนมัติจาก 3 เป็น 6

แล้วสังเกตสิ mySlice นั้นไม่เปลี่ยนแปลงเลย ค่า 50 จากก่อนหน้านี้ที่มาแทนที่ 40 ก็ไม่ปรากฏ

ใช่ครับ — newSlice เป็น slice ใหม่ไปแล้ว (ref ไปยัง newly allocated array)

เพื่อพิสูจน์เรื่องนี้ เรามาหาดูว่า pointer ของ newSlice ตอนที่ append(newSlice, 50) แล้ว append(newSlice, 60) เข้าไปอีกจะเป็นอย่างไร

package main

import (
"fmt"
)

func main() {
mySlice := []int{10, 20, 30, 40}

newSlice := mySlice[1:3]
fmt.Printf("underArr:\t%p\tcap=%d\n", newSlice, cap(newSlice))

newSlice = append(newSlice, 50)
fmt.Printf("underArr_50:\t%p\tcap=%d\n", newSlice, cap(newSlice))

newSlice = append(newSlice, 60)
fmt.Printf("underArr_50_60:\t%p\tcap=%d\n", newSlice, cap(newSlice))
}
underArr:       0x140000b8008   cap=3
underArr_50: 0x140000b8008 cap=3
underArr_50_60: 0x140000b6030 cap=6

อ้างอิง

GO: Arrays & Slices deep dive

Go: Slices intro

Go: Arrays & Slices & Maps

--

--

No responses yet