DEV Community

Weerasak Chongnguluam
Weerasak Chongnguluam

Posted on • Updated on

สรุปการจัดการข้อมูลแบบ slice ของ Go

Slice เป็นข้อมูลสำหรับเก็บของที่เป็นค่าเดียวกันหลายๆค่า เราใช้ Slice เยอะมากใน Go โพสต์นี้จะรวบรวมสรุปสิ่งที่เราจัดการ Slice ของ Go เอาไว้ ตั้งแต่การสร้าง, การจัดการสมาชิก รวมถึงจัดการหน่วยความจำของสมาชิก แม้ตัวอย่างจะเป็นแค่ slice ของ int แต่ก็สามารถใช้กับ slice ที่สมาชิกเป็น type อื่นๆได้เช่นกัน

1) Creates nil slice (zero value of slice)

ถ้าเราประกาศตัวแปรประเภท slice ขึ้นมาเฉยๆค่ามันจะเป็น nil ซึ่งหมายถึงตัวแปรนี้ยังไม่มีส่วนของพื้นที่สำหรับเก็บ element ใดๆ ดังนั้นถ้าเอาไปหา length ด้วยฟังก์ชัน len ก็จะได้ 0
ตัวอย่างเช่น

// s == nil, len(s) == 0

var s []int
Enter fullscreen mode Exit fullscreen mode

2) Creates empty (0 element) slice

empty slice ต่างจาก slice ที่มีค่า nil คือมีการสร้างส่วนสำหรับเก็บ element แล้วแต่ยังไม่มี element ใดๆอยู่ ถ้าเอาไปหาด้วย len ก็ได้ 0 เช่นกัน
ตัวอย่างเช่น

// s == []int{}, len(s) == 0

var s []int
s = []int{}

// หรือถ้าต้องการสร้าง s พร้อมกับกำหนดค่าให้เป็น empty []int{} ด้วย := เลยก็ได้

s := []int{}

// หรือแบบนี้ถ้าเป็น package scope

var s = []int{}

// หรือใช้ฟังก์ชัน make ช่วยแทนที่จะกำหนดด้วย []int{} ก็ได้ โดยกำหนดจำนวน element ที่ต้องการเป็น 0

s := make([]int, 0)
Enter fullscreen mode Exit fullscreen mode

3) Creates n zero value elements slice

เราสามารถใช้ make เพื่อเตรียม slot สำหรับเก็บ element ไว้ล่วงหน้าได้โดยใช้ make แล้วกำหนดจำนวน element ที่ต้องการโดยที่เราจะได้ slice ที่มี element ตามจำนวนที่กำหนดแต่ว่าทุกค่าจะเป็น zero value ของ type ของ element
ตัวอย่างเช่น

// s จะได้สมาชิกเป็น int 5 ตัวโดยที่ทุกค่าเป็น 0

s := make([]int, 5)
Enter fullscreen mode Exit fullscreen mode

4) Creates n elements slice

ถ้าเราต้องการสร้าง slice ที่มีสมาชิก n ตัวเริ่มต้นที่ค่าต่างกัน เราใช้ slice literal ได้ซึ่งเราทำไปแล้วแบบนี้ []int{} เพื่อกำหนด empty slice ถ้าเราอยากได้สมาชิกเริ่มต้นเท่าไหร่ก็ใส่ไปในปีกกาคั่นด้วย comma ได้เลย
ตัวอย่างเช่น

// s จะได้สมาชิกเป็น int 5 ตัวตามที่เราใส่ใน `{}` เลย

s := []int{1, 2, 3, 4, 5}
Enter fullscreen mode Exit fullscreen mode

5) Access (index started from 0)

การเข้าถึง element ของ slice จะเป็นแบบ index base โดยที่ index เป็นจำนวนเต็ม เริ่มต้นที่ 0 แล้วนับไปทีละ 1
ตัวอย่างเช่น

s := []int{5, 6, 7, 8, 9}

// s[2] == 7

fmt.Println(s[2])

// panic เพราะเข้าถึง index ที่ไม่มีอยู่ของ slice นี้

fmt.Println(s[5])

// index เป็นได้ทั้งตัวเลขโดยตรงหรือจากตัวแปรที่เป็นจำนวนเต็มก็ได้

i := 3

// s[i] == 8
fmt.Println(s[i])
Enter fullscreen mode Exit fullscreen mode

6) Iterate each element (for counter index, for range)

เราสามารถใช้ for เพื่อวนซ้ำตามจำนวน element ของ slice ได้โดยเขียนได้ทั้งแบบ counter index ตาม length หรือจะใช้ for range ช่วยก็ได้
ตัวอย่างเช่น

s := []int{1, 2, 3, 4}

// ใช้ for loop นับ index ไปจาก 0 จนถึงตัวสุดท้ายคือตัวที่น้อยกว่า len(s) อยู่ 1

for i := 0; i < len(s); i++ {
        fmt.Println(s[i])
}

// ใช้ for range ซึ่งจะวนซ้ำจากตัวแรกจนถึงตัวสุดท้ายโดยให้ range กำหนดค่า index `i` ให้ในแต่ละรอบ

for i := range s {
        fmt.Println(s[i])
}

// ใช้ for range ซึ่งจะวนซ้ำจากตัวแรกจนถึงตัวสุดท้ายโดยให้ range กำหนดค่า index `i` ให้ในแต่ละรอบและค่าของ element ในตำแหน่งที่ s[i] ให้ตัวแปร v ให้ในแต่ละรอบ

for i, v := range s {
        fmt.Println(i, v)
}

// แต่ถ้าเราไม่ได้ใช้ index ให้ใช้ _ แทนชื่อตัวแปรได้เช่น

for _, v := range s {
        fmt.Println(v)
}

Enter fullscreen mode Exit fullscreen mode

7) Update element value at index

เราสามารถแก้ไขค่าของสมาชิกของ slice ในตำแหน่งที่ต้องการได้โดยใช้ operator = แล้วกำหนดค่าที่ต้องการให้กับ s ในตำแหน่งที่ต้องการโดยใช้การอ้างอิงตำแหน่งแบบเดียวกับตอนเข้าถึงค่า
ตัวอย่างเช่น

s := []int{1, 2, 3, 4, 5}

// แก้ไขค่า s ใน index ที่ 2 เป็น 9

s[2] = 9

// แก้ไขค่า s โดยแก้ในตำแหน่งที่ตัวแปร i เก็บค่าไว้เช่น i = 4, s[4] ก็เปลี่ยนเป็น 7

i := 4
s[i] = 7

// panic ถ้าเราแก้ไขในตำแหน่งที่ไม่มีอยู่ของ slice

s[5] = 10
Enter fullscreen mode Exit fullscreen mode

8) Multiple slices share element

ตัวแปร slice คนละตัว สามารถแชร์ element ร่วมกันได้ ตราบใดที่ยังไม่มีการเปลี่ยนแปลงหน่วยความจำที่จัดเก็บ element ภายในของ slice ซึ่งอาจจะเปลี่ยนได้ถ้ามีการสร้าง slice ใหม่ หรือมีการเพิ่มจำนวนสมาชิกจนทำให้ต้องสร้างหน่วยความจำใหม่เพื่อให้เพียงพอกับสมาชิกที่เพิ่มขึ้นมา
ตัวอย่างเช่น

s := []int{1, 2, 3, 4, 5}

// กำหนด s ให้กับตัวแปร slice ใหม่ จะทำให้แชร์ element ร่วมกัน ตราบใดที่ยังไม่มีการเปลี่ยนแปลงของหน่วยความจำภายในที่จัดเก็บ element

ss := s

// ดังนั้นถ้าเราแก้ไขค่าสมาชิกผ่าน s หรือ ss พอเข้าถึงค่าในตำแหน่งนั้นก็จะได้ค่าเท่ากัน

s[1] = 6

// ss[1] ก็ได้ 6 เช่นกัน
fmt.Println(ss[1])

ss[2] = 7

// s[2] ก็ได้ 7 เช่นกัน
fmt.Println(s[2])

Enter fullscreen mode Exit fullscreen mode

9) Share with slice function parameter

ด้วยวิธีการแชร์ element ร่วมกันของ slice ทำให้เราจะเห็นโค้ดในการจัดการ element ของ slice แยกออกเป็น function ที่รับ slice แล้วใน function นั้นจะทำการ update ค่าของ element เป็นค่าที่ต้องการให้ เช่น

func plusAllElementWithFive(nums []int) {
        for i := range nums {
                nums[i] = nums[i] + 5
        }
}

func main() {
        s := []int{1, 2, 3, 4, 5}
        plusAllElementWithFive(s)

        // สมาชิกใน s จะถูกบวกไปอีก 5 , s จะได้เป็น [6, 7, 8, 9, 10]

        fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode

อย่างไรก็ตามเทคนิคนี้เราจะให้กับ function ที่อ่านค่าของใน element หรือแก้ไขค่าใน element เท่านั้น เพราะ slice แชร์แค่ element กันอยู่ แต่ตัวแปร slice คือว่าเป็นคนละตัว ถ้าเราเปลี่ยนตัวแปร slice เป็น slice อื่น ตัวแปรที่ส่งมาจะไม่เปลี่ยนไปด้วย เช่น

func cannotChangeCallerSlice(s []int) {
        s = []int{5, 6, 7, 8}
}

func main() {
        s := []int{1, 2, 3, 4}

        // s ไม่เปลี่ยนเป็น [5, 6, 7, 8] เพราะถือว่า s เป็นคนละตัวกัน

        cannotChangeCallerSlice(s)

        // s ยังคงเป็น [1, 2, 3, 4]
        fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode

เทคนิคนี้ Go standard package io ก็ใช้กับ interface Reader กับ interface Write เช่นกันที่รับ slice ของ byte โดยที่ Reader จะรับ []byte ไปแล้วอ่านค่ามาอัพเดทสมาชิกของ []byte ที่ส่งไป ส่วน Writer เอาค่าสมาชิกของ []byte ที่ส่งไปไปเขียนลงปลายทาง

type Reader interface {
        Read(b []byte) (n int, err error)
}

type Writer interface {
        Write(b []byte) (n int, err error)
}
Enter fullscreen mode Exit fullscreen mode

10) Slices a slice

เราสามารถแชร์ สมาชิกของ slice โดยกำหนดให้แชร์แค่บางส่วน ไม่ใช่ทั้งหมดได้โดยใช้ slice operator
ตัวอย่างเช่น

s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

// เราต้องการแชร์ค่าของ s จาก index ที่ 1 ถึง 4 โดยกำหนดเลข 1 คั่นด้วย : แล้ว 5 ใน []
// จะเห็นว่าถ้าเราต้องการถึงตำแหน่งที่ n เราต้องกำหนดค่าหลังเป็น n+1 เช่นต้องการ 4 กำหนดเป็น 5

ss := s[1:5]

// สิ่งที่จะได้คือ ss ที่มีสมาชิก 4 ตัวเริ่มที่ 0 ถึง 3 โดยที่มีค่าเป็น [2, 3, 4, 5]

fmt.Println(ss)

// ss แชร์สมาชิกกับ s อยู่ถ้าเราเปลี่ยน s ในตำแหน่งที่ 1 ถึง 4 ค่าของ ss ตำแหน่งที่ 0 ถึง 3 ก็เปลี่ยนด้วยตามลำดับเช่น

s[1] = 20

// ss[0] เป็น 20 ด้วย
fmt.Println(ss[0])

ss[1] = 30

// s[2] เป็น 30 ด้วย
fmt.Println(s[2])
Enter fullscreen mode Exit fullscreen mode

11) Append new element to slice

การเพิ่มสมาชิกของ slice จะใช้ function append เราสามารถเพิ่มทีละ 1 ค่า หรือหลายค่าพร้อมกันได้เช่น

s := []int{1, 2}

// เริ่มค่า 3 ให้ s เวลาเรียก append ต้องรับค่ามากำหนดให้กับ s ตัวเดิมเสมอ เดี๋ยวอธิบายเพิ่มเติมในเรื่อง maintain capability storage

s = append(s, 3)

// สมาชิกของ s ตอนนี้เป็น [1, 2, 3]
fmt.Println(s)

// เพิ่มหลายค่าได้ เนื่องจาก append รับ variadic argument

s = append(s, 4, 5, 6, 7)

// สมาชิกของ s ตอนนี้เป็น [1, 2, 3, 4, 5, 6, 7]
fmt.Println(s)

// เรายังส่งค่าจากอีก slice เข้าไป append ได้ด้วยท่านี้ เพราะเป็น variadic argument

ss := []int{8, 9, 10}

s = append(s, ss...)

// สมาชิกของ s ตอนนี้เป็น [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
fmt.Println(s)
Enter fullscreen mode Exit fullscreen mode

12) Prepend element to slice

เราสามารถใช้ append ช่วยเพิ่มสมาชิกไปด้านหน้าของ slice ได้เช่นกัน อาศัยเทคนิคเล็กน้อยคือกำหนดค่าแรกเป็น slice ที่มีสมาชิกเดียวที่เราต้องการเพิ่มแล้วใช่ท่ากระจายสมาชิกของ slice เป็น variadic argument เช่น

s := []int{1, 2, 3}

// เพิ่ม 4 ไปด้านหน้า s โดยใช้ append แบบนี้

s = append([]int{4}, s...)

// ตอนนี้สมาชิกใน s เป็น [4, 1, 2, 3]
fmt.Println(s)
Enter fullscreen mode Exit fullscreen mode

13) Copy slice without share element

เรารู้ไปแล้วว่าถ้าเรากำหนดค่าตัวแปร slice ให้กับอีกตัวแปร มันจะแชร์ element กัน แต่ถ้าเราอยาก copy slice โดย copy element ให้แทนที่จะแชร์กัน เราสามารถใช้ function copy ช่วยได้ เช่น

s := []int{1, 2, 3, 4, 5}

// ถ้าเราต้องการ copy s เป็นอีกก้อนโดยไม่แชร์ element กันเราต้องเตรียม slice อีกก้อนขนาดเท่ากันขึ้นมาก่อน ในที่นี้จะใช้ make ช่วยง่ายๆแบบนี้

ss := make([]int, len(s))

// แล้วใช้ copy จาก s มาหา ss

copy(ss, s)

// ตอนนี้ ss จะเป็น [1, 2, 3, 4, 5]
fmt.Println(ss)

// แม้ว่าเปลี่ยนสมาชิกของ ss ค่าของ s ก็ไม่เปลี่ยนตามแล้ว

ss[0] = 7

// s[0] ยังคงเป็น 1
fmt.Println(s[0])
Enter fullscreen mode Exit fullscreen mode

14) Maintain slice capability storage

slice นั้นจะมี storage สำหรับเก็บจำนวนสมาชิกซึ่งจะถูกจองหน่วยความจำไว้จำนวนหนึ่ง ซึ่งสำหรับ Go จะเรียกขนาดของหน่วยความจำที่จองไว้ว่า capacity เราสามารถใช้ function cap หาขนาดตรงนี้ได้เช่น

s := []int{1, 2, 3, 4, 5}

// len(s) เท่ากับ 5

fmt.Println(len(s))

// ตอนนี้ cap(s) เท่ากับ 5 เช่นกัน

fmt.Println(cap(s))
Enter fullscreen mode Exit fullscreen mode

ถ้าเกิดเราใช้ append เพิ่มค่าให้ slice แล้ว capacity ไม่เพียงพอ สิ่งที่ append จะทำคือจะจองหน่วยความจำใหม่ให้ 2 เท่าจากของ capacity เดิมแล้วค่อยเพิ่มสมาชิกเข้าไป เช่น

s := []int{1, 2, 3}

// len(s) เท่ากับ 3 และ cap(s) เท่ากับ 3

fmt.Println(len(s))
fmt.Println(cap(s))

s = append(s, 4)

// len(s) เท่ากับ 4 และ cap(s) เท่ากับ 6

fmt.Println(len(s))
fmt.Println(cap(s))

Enter fullscreen mode Exit fullscreen mode

นี่เป็นเหตุที่เราต้องกำหนดค่าที่ append return กลับมาให้กับตัวแปร slice ที่เราต้องการเพิ่มค่า เพราะเมื่อไหร่ที่ append สร้างหน่วยความจำใหม่ มันจะไม่สามารถแก้ไขค่าของ slice ที่ส่งไปได้ มันจะ return slice ตัวใหม่กลับมาให้แทน

เช่นถ้าเราไม่กำหนดค่าที่ append ส่งกลับมาให้ s จะทำให้ s ไม่มีอะไรเปลี่ยนแปลงหลัง append

s := []int{1, 2, 3}

// len(s) เท่ากับ 3 และ cap(s) เท่ากับ 3

fmt.Println(len(s))
fmt.Println(cap(s))

_ = append(s, 4)

// len(s) จะยังเท่ากับ 3 และ cap(s) เท่ากับ 3

fmt.Println(len(s))
fmt.Println(cap(s))

// s ยังคงเป็น [1, 2, 3]

fmt.Println(s)
Enter fullscreen mode Exit fullscreen mode

เราสามารถใช้ make เพื่อเตรียม slice ที่มียังไม่มีสมาชิก แต่เตรียม capacity เอาไว้รอได้เช่น

// เตรียม capacity เอาไว้ 5 โดยใส่จำนวนที่ต้องการเป็น argument ที่ 3 ของ make

s := make([]int, 0, 5)

// len(s) เท่ากับ 0 และ cap(s) เท่ากับ 5

fmt.Println(len(s))
fmt.Println(cap(s))

// ดังนั้นเราสามารถ append ค่าของ s ได้โดยที่ cap ไม่สร้างใหม่จำกว่าจะไม่พอ

s = append(s, 1, 2, 3, 4)

// len(s) เท่ากับ 4 และ cap(s) เท่ากับ 5

fmt.Println(len(s))
fmt.Println(cap(s))
Enter fullscreen mode Exit fullscreen mode

Buy Me A Coffee

Top comments (0)