Skip to content

สไลซ์

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

อาร์เรย์

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

TIP

อาร์เรย์เป็นประเภทค่า เมื่อส่งอาร์เรย์เป็นพารามิเตอร์ให้ฟังก์ชัน เนื่องจาก Go ส่งพารามิเตอร์แบบค่า ดังนั้นจะคัดลอกอาร์เรย์ทั้งหมด

การเริ่มต้น

ความยาวของอาร์เรย์ต้องเป็นค่าคงที่เท่านั้นเมื่อประกาศ ไม่สามารถเป็นตัวแปรได้ คุณไม่สามารถประกาศตัวแปรหนึ่งแล้วใช้ตัวแปรเป็นค่าความยาวของอาร์เรย์

go
// ตัวอย่างถูกต้อง
var a [5]int

// ตัวอย่างผิด
l := 1
var b [l]int

เริ่มต้นอาร์เรย์จำนวนเต็มความยาว 5 ก่อน

go
var nums [5]int

สามารถใช้การเริ่มต้นด้วยองค์ประกอบได้

go
nums := [5]int{1, 2, 3}

ให้คอมไพเลอร์อนุมัติความยาวอัตโนมัติ

go
nums := [...]int{1, 2, 3, 4, 5} // เทียบเท่ากับ nums := [5]int{1, 2, 3, 4, 5} เครื่องหมายจุดไข่ต้องอยู่ มิฉะนั้นจะสร้างเป็นสไลซ์ ไม่ใช่อาร์เรย์

ยังสามารถรับพอยน์เตอร์หนึ่งตัวผ่านฟังก์ชัน new

go
nums := new([5]int)

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

เมื่อเริ่มต้นอาร์เรย์ สิ่งที่ควรระวังคือ ความยาวต้องเป็นนิพจน์ค่าคงที่ มิฉะนั้นจะไม่สามารถผ่านการคอมไพล์ได้ นิพจน์ค่าคงที่คือผลลัพธ์สุดท้ายของนิพจน์เป็นค่าคงที่ ตัวอย่างผิดดังนี้

go
length := 5 // นี่คือตัวแปร
var nums [length]int

length เป็นตัวแปร จึงไม่สามารถใช้เริ่มต้นความยาวอาร์เรย์ได้ ด้านล่างเป็นตัวอย่างถูกต้อง

go
const length = 5
var nums [length]int // ค่าคงที่
var nums2 [length + 1]int // นิพจน์ค่าคงที่
var nums3 [(1 + 2 + 3) * 5]int // นิพจน์ค่าคงที่
var nums4 [5]int // ใช้บ่อยที่สุด

การใช้งาน

เพียงมีชื่ออาร์เรย์และดัชนี ก็สามารถเข้าถึงองค์ประกอบที่ตรงกันในอาร์เรย์ได้

go
fmt.Println(nums[0])

เช่นเดียวกันสามารถแก้ไของค์ประกอบอาร์เรย์ได้

go
nums[0] = 1

还可以通过内置函数 len เข้าถึงจำนวนองค์ประกอบของอาร์เรย์

go
len(nums)

ฟังก์ชันในตัว cap เข้าถึงความจุของอาร์เรย์ ความจุของอาร์เรย์เท่ากับความยาวอาร์เรย์ ความจุมีความหมายสำหรับสไลซ์เท่านั้น

go
cap(nums)

การตัด

รูปแบบการตัดอาร์เรย์คือ arr[startIndex:endIndex] ช่วงที่ตัดเป็น ปิดซ้ายเปิดขวา และอาร์เรย์หลังจากตัดแล้ว จะกลายเป็นประเภทสไลซ์ ตัวอย่างดังนี้

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

nums[:] // ขอบเขตสไลซ์ย่อย [0,5) -> [1 2 3 4 5]
nums[1:] // ขอบเขตสไลซ์ย่อย [1,5) -> [2 3 4 5]
nums[:5] // ขอบเขตสไลซ์ย่อย [0,5) -> [1 2 3 4 5]
nums[2:3] // ขอบเขตสไลซ์ย่อย [2,3) -> [3]
nums[1:3] // ขอบเขตสไลซ์ย่อย [1,3) -> [2 3]
go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  fmt.Printf("%T\n", arr)
  fmt.Printf("%T\n", arr[1:2])
}

เอาต์พุต

[5]int
[]int

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

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := arr[:]
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

เอาต์พุต

array: [0 2 3 4 5]
slice: [0 2 3 4 5]

หากต้องการแก้ไขสไลซ์ที่แปลงแล้ว แนะนำให้ใช้วิธีแปลงแบบนี้

go
func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := slices.Clone(arr[:])
  slice[0] = 0
  fmt.Printf("array: %v\n", arr)
  fmt.Printf("slice: %v\n", slice)
}

เอาต์พุต

array: [1 2 3 4 5]
slice: [0 2 3 4 5]

สไลซ์

ขอบเขตการใช้งานของสไลซ์ใน Go กว้างกว่าอาร์เรย์มาก มันใช้เก็บข้อมูลที่ไม่ทราบความยาว และในการใช้งานต่อมาอาจมีการแทรกและลบองค์ประกอบบ่อยครั้ง

การเริ่มต้น

วิธีการเริ่มต้นสไลซ์มีดังนี้

go
var nums []int // ค่า
nums := []int{1, 2, 3} // ค่า
nums := make([]int, 0, 0) // ค่า
nums := new([]int) // พอยน์เตอร์

จะเห็นว่าความแตกต่างระหว่างสไลซ์กับอาร์เรย์ในด้านรูปลักษณ์ เพียงแค่ขาดการเริ่มต้นความยาวเท่านั้น โดยทั่วไป แนะนำให้ใช้ make สร้างสไลซ์ว่างเปล่า เพียงแต่สำหรับสไลซ์แล้ว ฟังก์ชัน make รับพารามิเตอร์สามตัว คือ ประเภท ความยาว ความจุ ยกตัวอย่างอธิบายความแตกต่างระหว่างความยาวกับความจุ สมมติว่ามีถังน้ำหนึ่งถัง น้ำไม่เต็ม ความสูงของถังคือความจุของถัง หมายถึงสามารถใส่น้ำได้ทั้งหมดเท่าไร ส่วนความสูงของน้ำในถังคือความยาว ความสูงของน้ำต้องน้อยกว่าหรือเท่ากับความสูงของถัง มิฉะนั้นน้ำจะล้น ดังนั้น ความยาวของสไลซ์หมายถึงจำนวนองค์ประกอบในสไลซ์ ความจุของสไลซ์หมายถึงสไลซ์สามารถใส่องค์ประกอบได้ทั้งหมดเท่าไร ความแตกต่างที่สำคัญที่สุดระหว่างสไลซ์กับอาร์เรย์คือความจุของสไลซ์จะขยายอัตโนมัติ ส่วนอาร์เรย์ไม่ทำเช่นนั้น ดูรายละเอียดเพิ่มเติมที่ คู่มืออ้างอิง - ความยาวและความจุ

TIP

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

สไลซ์ที่ประกาศผ่านวิธี var nums []int ค่าเริ่มต้นคือ nil จึงไม่จัดสรรหน่วยความจำให้มัน และเมื่อใช้ make เริ่มต้น แนะนำให้จัดสรรความจุที่เพียงพอล่วงหน้า สามารถลดการกินหน่วยความจำในการขยายขนาดในภายหลังได้อย่างมีประสิทธิภาพ

การใช้งาน

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

สไลซ์สามารถดำเนินการหลายอย่างผ่านฟังก์ชัน append ลายเซ็นฟังก์ชันดังนี้ slice คือสไลซ์เป้าหมายที่จะเพิ่มองค์ประกอบ elems คือองค์ประกอบที่รอเพิ่ม ค่าส่งคืนคือสไลซ์หลังจากเพิ่มแล้ว

go
func append(slice []Type, elems ...Type) []Type

ก่อนสร้างสไลซ์ว่างเปล่าความยาว 0 ความจุ 0 แล้วแทรกองค์ประกอบบางตัวที่ท้ายสุด สุดท้ายเอาต์พุตความยาวและความจุ

go
nums := make([]int, 0, 0)
nums = append(nums, 1, 2, 3, 4, 5, 6, 7)
fmt.Println(len(nums), cap(nums)) // 7 8 จะเห็นว่าความยาวกับความจุไม่ตรงกัน

ขนาดบัฟเฟอร์ที่สไลซ์ใหม่จองไว้ มีกฎเกณฑ์บางอย่าง ก่อนการอัปเดตเวอร์ชัน golang1.18 บทความส่วนใหญ่ในอินเทอร์เน็ตอธิบายกลยุทธ์การขยายขนาดของ slice เช่นนี้ เมื่อความจุของ slice เดิม (oldcap) น้อยกว่า 1024 ความจุของ slice ใหม่จะกลายเป็น 2 เท่าของเดิม เมื่อความจุของ slice เดิมเกิน 1024 ความจุของ slice ใหม่จะกลายเป็น 1.25 เท่าของเดิม หลังจากการอัปเดตเวอร์ชัน 1.18 แล้ว กลยุทธ์การขยายขนาดของ slice เปลี่ยนเป็น เมื่อความจุของ slice เดิม (oldcap) น้อยกว่า 256 ความจุของ slice ใหม่ (newcap) เป็น 2 เท่าของเดิม เมื่อความจุของ slice เดิมเกิน 256 ความจุของ slice ใหม่ newcap = oldcap+(oldcap+3*256)/4

การแทรกองค์ประกอบ

การแทรกองค์ประกอบของสไลซ์ต้องใช้ฟังก์ชัน append ร่วมด้วย มีสไลซ์ดังนี้

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

แทรกองค์ประกอบจากหัว

go
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]

แทรกองค์ประกอบจากดัชนีกลาง i

go
nums = append(nums[:i+1], append([]int{999, 999}, nums[i+1:]...)...)
fmt.Println(nums) // i=3, [1 2 3 4 999 999 5 6 7 8 9 10]

แทรกองค์ประกอบจากท้ายสุด คือการใช้งานดั้งเดิมที่สุดของ append

go
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]

การลบองค์ประกอบ

การลบองค์ประกอบของสไลซ์ต้องใช้ฟังก์ชัน append ร่วมด้วย มีสไลซ์ดังนี้

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

ลบองค์ประกอบ n ตัวจากหัว

go
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]

ลบองค์ประกอบ n ตัวจากท้าย

go
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]

ลบองค์ประกอบ n ตัวเริ่มจากตำแหน่งดัชนี i ที่กำหนดจากกลาง

go
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2, n=3, [1 2 6 7 8 9 10]

ลบองค์ประกอบทั้งหมด

go
nums = nums[:0]
fmt.Println(nums) // []

การคัดลอก

เมื่อคัดลอกสไลซ์ต้องแน่ใจว่าสไลซ์เป้าหมาย มีความยาวเพียงพอ เช่น

go
func main() {
  dest := make([]int, 0)
  src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  fmt.Println(src, dest)
  fmt.Println(copy(dest, src))
  fmt.Println(src, dest)
}
[1 2 3 4 5 6 7 8 9] []
0
[1 2 3 4 5 6 7 8 9] []

แก้ไขความยาวเป็น 10 เอาต์พุตดังนี้

[1 2 3 4 5 6 7 8 9] [0 0 0 0 0 0 0 0 0 0]
9
[1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 0]

การวนซ้ำ

การวนซ้ำสไลซ์เหมือนกับอาร์เรย์ทั้งหมด ลูป for

go
func main() {
   slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
   for i := 0; i < len(slice); i++ {
      fmt.Println(slice[i])
   }
}

ลูป for range

go
func main() {
  slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
  for index, val := range slice {
    fmt.Println(index, val)
  }
}

สไลซ์หลายมิติ

ดูตัวอย่างด้านล่างก่อน ในเอกสารทางการก็มีคำอธิบายเช่นกัน Effective Go - สไลซ์สองมิติ

go
var nums [5][5]int
for _, num := range nums {
   fmt.Println(num)
}
fmt.Println()
slices := make([][]int, 5)
for _, slice := range slices {
   fmt.Println(slice)
}

ผลลัพธ์เอาต์พุตคือ

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[]
[]
[]
[]
[]

จะเห็นว่าอาร์เรย์และสไลซ์สองมิติเหมือนกัน โครงสร้างภายในต่างกัน อาร์เรย์เมื่อเริ่มต้นแล้ว ความยาวมิติแรกและมิติสองคงที่ล่วงหน้า ส่วนความยาวของสไลซ์ไม่คงที่ สไลซ์แต่ละตัวในสไลซ์อาจมีความยาวไม่เท่ากัน ดังนั้นต้องเริ่มต้นแยกต่างหาก แก้ไขส่วนเริ่มต้นสไลซ์เป็นโค้ดด้านล่างก็ได้

go
slices := make([][]int, 5)
for i := 0; i < len(slices); i++ {
   slices[i] = make([]int, 5)
}

ผลลัพธ์เอาต์พุตสุดท้ายคือ

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]
[0 0 0 0 0]

นิพจน์ขยาย

TIP

มีเพียงสไลซ์เท่านั้นที่สามารถใช้นิพจน์ขยายได้

ทั้งสไลซ์และอาร์เรย์สามารถใช้นิพจน์ธรรมดาทำการตัดได้ แต่มีเพียงสไลซ์เท่านั้นที่สามารถใช้นิพจน์ขยายได้ คุณสมบัตินี้เพิ่มในเวอร์ชัน Go1.2 เพื่อแก้ปัญหาการอ่านและเขียนที่สไลซ์แชร์อาร์เรย์ระดับล่าง รูปแบบหลักดังนี้ ต้องเป็นไปตามความสัมพันธ์ low<= high <= max <= cap ความจุของสไลซ์ที่ตัดด้วยนิพจน์ขยายคือ max-low

go
slice[low:high:max]

low กับ high ยังคงมีความหมายเดิมไม่เปลี่ยนแปลง ส่วน max ที่เพิ่มขึ้นมาหมายถึงความจุสูงสุด เช่นในตัวอย่างด้านล่างละเว้น max ดังนั้นความจุของ s2 คือ cap(s1)-low

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4] // cap = 9 - 3 = 6

เช่นนี้จะเห็นปัญหาชัดเจนหนึ่งอย่าง คือ s1 กับ s2 แชร์อาร์เรย์ระดับล่างเดียวกัน เมื่ออ่านและเขียน s2 อาจส่งผลต่อข้อมูลของ s1 ได้ โค้ดด้านล่างเป็นสถานการณ์แบบนี้

go
s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
s2 := s1[3:4]                          // cap = 9 - 3 = 6
s2 = append(s2, 1)   // เพิ่มองค์ประกอบใหม่ เนื่องจากความจุเป็น 6 จึงไม่ขยายขนาด แก้ไขอาร์เรย์ระดับล่างโดยตรง
fmt.Println(s2)
fmt.Println(s1)

เอาต์พุตสุดท้ายคือ

[4 1]
[1 2 3 4 1 6 7 8 9]

จะเห็นว่า明明เพิ่มองค์ประกอบให้ s2 แต่กลับแก้ไข s1 ด้วย นิพจน์ขยายเกิดมาเพื่อแก้ปัญหาประเภทนี้ เพียงแก้ไขเล็กน้อยก็สามารถแก้ปัญหานี้ได้

go
func main() {
   s1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9} // cap = 9
   s2 := s1[3:4:4]                        // cap = 4 - 3 = 1
   s2 = append(s2, 1)    // ความจุไม่เพียงพอ จัดสรรอาร์เรย์ระดับล่างใหม่
   fmt.Println(s2)
   fmt.Println(s1)
}

ครั้งนี้ผลลัพธ์ปกติแล้ว

[4 1]
[1 2 3 4 5 6 7 8 9]

clear

ใน go1.21 เพิ่มฟังก์ชันในตัว clear clear จะตั้งค่าทั้งหมดในสไลซ์เป็นค่าศูนย์

go
package main

import (
    "fmt"
)

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

เอาต์พุต

[0 0 0 0]

หากต้องการล้างสไลซ์ สามารถ

go
func main() {
  s := []int{1, 2, 3, 4}
    s = s[:0:0]
  fmt.Println(s)
}

จำกัดความจุหลังจากตัดแล้ว这样可以หลีกเลี่ยงการทับองค์ประกอบถัดไปของสไลซ์เดิม

Golang by www.golangdev.cn edit