สไลซ์
ใน Go อาร์เรย์และสไลซ์ดูเหมือนจะเหมือนกันเกือบทั้งหมด แต่ฟังก์ชันมีความแตกต่างไม่น้อย อาร์เรย์เป็นโครงสร้างข้อมูลความยาวคงที่ หลังจากกำหนดความยาวแล้วไม่สามารถเปลี่ยนแปลงได้ ส่วนสไลซ์เป็นความยาวไม่แน่นอน สไลซ์จะขยายขนาดเองเมื่อความจุไม่เพียงพอ
อาร์เรย์
หากทราบความยาวของข้อมูลที่จะเก็บล่วงหน้า และในการใช้งานต่อมาไม่มีความต้องการขยายขนาด ก็สามารถพิจารณาใช้อาร์เรย์ได้ อาร์เรย์ใน Go เป็นประเภทค่า ไม่ใช่การอ้างอิง ไม่ใช่พอยน์เตอร์ที่ชี้ไปที่องค์ประกอบหัว
TIP
อาร์เรย์เป็นประเภทค่า เมื่อส่งอาร์เรย์เป็นพารามิเตอร์ให้ฟังก์ชัน เนื่องจาก Go ส่งพารามิเตอร์แบบค่า ดังนั้นจะคัดลอกอาร์เรย์ทั้งหมด
การเริ่มต้น
ความยาวของอาร์เรย์ต้องเป็นค่าคงที่เท่านั้นเมื่อประกาศ ไม่สามารถเป็นตัวแปรได้ คุณไม่สามารถประกาศตัวแปรหนึ่งแล้วใช้ตัวแปรเป็นค่าความยาวของอาร์เรย์
// ตัวอย่างถูกต้อง
var a [5]int
// ตัวอย่างผิด
l := 1
var b [l]intเริ่มต้นอาร์เรย์จำนวนเต็มความยาว 5 ก่อน
var nums [5]intสามารถใช้การเริ่มต้นด้วยองค์ประกอบได้
nums := [5]int{1, 2, 3}ให้คอมไพเลอร์อนุมัติความยาวอัตโนมัติ
nums := [...]int{1, 2, 3, 4, 5} // เทียบเท่ากับ nums := [5]int{1, 2, 3, 4, 5} เครื่องหมายจุดไข่ต้องอยู่ มิฉะนั้นจะสร้างเป็นสไลซ์ ไม่ใช่อาร์เรย์ยังสามารถรับพอยน์เตอร์หนึ่งตัวผ่านฟังก์ชัน new
nums := new([5]int)หลายวิธีข้างต้นจะจัดสรรพื้นที่หน่วยความจำขนาดคงที่ให้กับ nums ความแตกต่างคือวิธีสุดท้ายได้ค่าเป็นพอยน์เตอร์
เมื่อเริ่มต้นอาร์เรย์ สิ่งที่ควรระวังคือ ความยาวต้องเป็นนิพจน์ค่าคงที่ มิฉะนั้นจะไม่สามารถผ่านการคอมไพล์ได้ นิพจน์ค่าคงที่คือผลลัพธ์สุดท้ายของนิพจน์เป็นค่าคงที่ ตัวอย่างผิดดังนี้
length := 5 // นี่คือตัวแปร
var nums [length]intlength เป็นตัวแปร จึงไม่สามารถใช้เริ่มต้นความยาวอาร์เรย์ได้ ด้านล่างเป็นตัวอย่างถูกต้อง
const length = 5
var nums [length]int // ค่าคงที่
var nums2 [length + 1]int // นิพจน์ค่าคงที่
var nums3 [(1 + 2 + 3) * 5]int // นิพจน์ค่าคงที่
var nums4 [5]int // ใช้บ่อยที่สุดการใช้งาน
เพียงมีชื่ออาร์เรย์และดัชนี ก็สามารถเข้าถึงองค์ประกอบที่ตรงกันในอาร์เรย์ได้
fmt.Println(nums[0])เช่นเดียวกันสามารถแก้ไของค์ประกอบอาร์เรย์ได้
nums[0] = 1还可以通过内置函数 len เข้าถึงจำนวนองค์ประกอบของอาร์เรย์
len(nums)ฟังก์ชันในตัว cap เข้าถึงความจุของอาร์เรย์ ความจุของอาร์เรย์เท่ากับความยาวอาร์เรย์ ความจุมีความหมายสำหรับสไลซ์เท่านั้น
cap(nums)การตัด
รูปแบบการตัดอาร์เรย์คือ arr[startIndex:endIndex] ช่วงที่ตัดเป็น ปิดซ้ายเปิดขวา และอาร์เรย์หลังจากตัดแล้ว จะกลายเป็นประเภทสไลซ์ ตัวอย่างดังนี้
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]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หากต้องการแปลงอาร์เรย์เป็นประเภทสไลซ์ ไม่ต้องใส่พารามิเตอร์ทำการสไลซ์ก็ได้ สไลซ์ที่แปลงแล้วกับอาร์เรย์เดิมชี้ไปที่พื้นที่หน่วยความจำเดียวกัน การแก้ไขสไลซ์จะทำให้เนื้อหาอาร์เรย์เดิมเปลี่ยนแปลง
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]หากต้องการแก้ไขสไลซ์ที่แปลงแล้ว แนะนำให้ใช้วิธีแปลงแบบนี้
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 กว้างกว่าอาร์เรย์มาก มันใช้เก็บข้อมูลที่ไม่ทราบความยาว และในการใช้งานต่อมาอาจมีการแทรกและลบองค์ประกอบบ่อยครั้ง
การเริ่มต้น
วิธีการเริ่มต้นสไลซ์มีดังนี้
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 คือองค์ประกอบที่รอเพิ่ม ค่าส่งคืนคือสไลซ์หลังจากเพิ่มแล้ว
func append(slice []Type, elems ...Type) []Typeก่อนสร้างสไลซ์ว่างเปล่าความยาว 0 ความจุ 0 แล้วแทรกองค์ประกอบบางตัวที่ท้ายสุด สุดท้ายเอาต์พุตความยาวและความจุ
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 ร่วมด้วย มีสไลซ์ดังนี้
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}แทรกองค์ประกอบจากหัว
nums = append([]int{-1, 0}, nums...)
fmt.Println(nums) // [-1 0 1 2 3 4 5 6 7 8 9 10]แทรกองค์ประกอบจากดัชนีกลาง i
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
nums = append(nums, 99, 100)
fmt.Println(nums) // [1 2 3 4 5 6 7 8 9 10 99 100]การลบองค์ประกอบ
การลบองค์ประกอบของสไลซ์ต้องใช้ฟังก์ชัน append ร่วมด้วย มีสไลซ์ดังนี้
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}ลบองค์ประกอบ n ตัวจากหัว
nums = nums[n:]
fmt.Println(nums) //n=3 [4 5 6 7 8 9 10]ลบองค์ประกอบ n ตัวจากท้าย
nums = nums[:len(nums)-n]
fmt.Println(nums) //n=3 [1 2 3 4 5 6 7]ลบองค์ประกอบ n ตัวเริ่มจากตำแหน่งดัชนี i ที่กำหนดจากกลาง
nums = append(nums[:i], nums[i+n:]...)
fmt.Println(nums)// i=2, n=3, [1 2 6 7 8 9 10]ลบองค์ประกอบทั้งหมด
nums = nums[:0]
fmt.Println(nums) // []การคัดลอก
เมื่อคัดลอกสไลซ์ต้องแน่ใจว่าสไลซ์เป้าหมาย มีความยาวเพียงพอ เช่น
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
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
func main() {
slice := []int{1, 2, 3, 4, 5, 7, 8, 9}
for index, val := range slice {
fmt.Println(index, val)
}
}สไลซ์หลายมิติ
ดูตัวอย่างด้านล่างก่อน ในเอกสารทางการก็มีคำอธิบายเช่นกัน Effective 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]
[]
[]
[]
[]
[]จะเห็นว่าอาร์เรย์และสไลซ์สองมิติเหมือนกัน โครงสร้างภายในต่างกัน อาร์เรย์เมื่อเริ่มต้นแล้ว ความยาวมิติแรกและมิติสองคงที่ล่วงหน้า ส่วนความยาวของสไลซ์ไม่คงที่ สไลซ์แต่ละตัวในสไลซ์อาจมีความยาวไม่เท่ากัน ดังนั้นต้องเริ่มต้นแยกต่างหาก แก้ไขส่วนเริ่มต้นสไลซ์เป็นโค้ดด้านล่างก็ได้
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
slice[low:high:max]low กับ high ยังคงมีความหมายเดิมไม่เปลี่ยนแปลง ส่วน max ที่เพิ่มขึ้นมาหมายถึงความจุสูงสุด เช่นในตัวอย่างด้านล่างละเว้น max ดังนั้นความจุของ s2 คือ cap(s1)-low
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 ได้ โค้ดด้านล่างเป็นสถานการณ์แบบนี้
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 ด้วย นิพจน์ขยายเกิดมาเพื่อแก้ปัญหาประเภทนี้ เพียงแก้ไขเล็กน้อยก็สามารถแก้ปัญหานี้ได้
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 จะตั้งค่าทั้งหมดในสไลซ์เป็นค่าศูนย์
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4}
clear(s)
fmt.Println(s)
}เอาต์พุต
[0 0 0 0]หากต้องการล้างสไลซ์ สามารถ
func main() {
s := []int{1, 2, 3, 4}
s = s[:0:0]
fmt.Println(s)
}จำกัดความจุหลังจากตัดแล้ว这样可以หลีกเลี่ยงการทับองค์ประกอบถัดไปของสไลซ์เดิม
