슬라이스
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 버전 업데이트 전까지 인터넷의 대부분 글은 슬라이스 확장 전략을 다음과 같이 기술했습니다: 원래 슬라이스 용량이 1024 보다 작을 때 새 슬라이스 용량은 원래의 2 배가 됩니다; 원래 슬라이스 용량이 1024 를 초과하면 새 슬라이스 용량은 원래의 1.25 배가 됩니다. 1.18 버전 업데이트 후 슬라이스 확장 전략은 다음과 같이 변경되었습니다: 원래 슬라이스 용량 (oldcap) 이 256 보다 작을 때 새 슬라이스 (newcap) 용량은 원래의 2 배가 됩니다; 원래 슬라이스 용량이 256 을 초과하면 새 슬라이스 용량 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]중간 지정 인덱스 i 위치에서 n 개 요소 삭제
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]
[]
[]
[]
[]
[]동일하게 二次元 배열과 슬라이스이지만 내부 구조가 다릅니다. 배열은 초기화 시 1 차원과 2 차원 길이가 이미 고정되어 있지만, 슬라이스 길이는 고정되지 않으며 슬라이스의 각 슬라이스 길이는 모두 다를 수 있으므로 별도로 초기화해야 합니다. 슬라이스 초기화 부분을 다음과 같이 수정하면 됩니다.
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)
}슬라이싱 후 용량을 제한하면 원래 슬라이스의 후속 요소를 덮어쓰는 것을 방지할 수 있습니다.
