슬라이스 표현식
슬라이스 표현식 (Slice expressions)
문자열이나 배열, 배열 포인터, 슬라이스에서 원하는 구간만 잘라내는 표현식을 슬라이스 표현식이라고 해요. 슬라이스를 자주 쓰다 보면 자연스럽게 마주치는 문법이라, 하나씩 정리해 보면 훨씬 수월해져요.
출처: Go Specification
본문
슬라이스 표현식은 문자열(string), 배열(array), 배열 포인터(pointer to array), 또는 슬라이스 피연산자로부터 부분 문자열(substring)이나 슬라이스를 만들어 내요. 두 가지 형태가 있어요. 시작과 끝 경계만 지정하는 **simple form(단순형)**과, 용량(capacity)의 경계까지 함께 지정하는 **full form(전체형)**이에요.
피연산자 타입이 type parameter(타입 매개변수)인 경우를 먼저 볼게요. 그 타입 집합이 문자열 타입을 포함하고 있지 않다면, 타입 집합의 모든 타입은 같은 기본 타입(underlying type)을 가져야 하고, 슬라이스 표현식은 해당 타입의 피연산자에 대해 유효해야 해요. 만약 타입 집합이 문자열 타입을 포함한다면, 기본 타입이 []byte인 바이트 슬라이스도 함께 포함될 수 있어요. 이 경우 슬라이스 표현식은 string 타입의 피연산자에 대해 유효해야 합니다.
단순 슬라이스 표현식 (Simple slice expressions)
문자열, 배열, 배열 포인터, 또는 슬라이스 a에 대해, 주 표현식(primary expression)
a[low : high]
은 부분 문자열이나 슬라이스를 만들어 내요. 인덱스(indices) low와 high는 피연산자 a의 어떤 요소들이 결과에 나타날지 정해요. 결과는 0에서 시작하는 인덱스를 가지며, 길이는 high - low와 같아요. 배열 a를 슬라이스한 뒤를 생각해 볼게요.
a := [5]int{1, 2, 3, 4, 5}
s := a[1:4]
이때 슬라이스 s는 타입이 []int, 길이가 3, 용량이 4이고, 다음과 같은 요소들을 가져요.
s[0] == 2
s[1] == 3
s[2] == 4
편의상 인덱스는 어느 것이든 생략할 수 있어요. low 인덱스를 빼면 0으로 간주되고, high 인덱스를 빼면 잘라내는 피연산자의 길이로 간주돼요.
a[2:] // same as a[2 : len(a)]
a[:3] // same as a[0 : 3]
a[:] // same as a[0 : len(a)]
만약 a가 배열 포인터라면, a[low : high]는 (*a)[low : high]의 줄임 표현이에요.
배열이나 문자열의 경우, 0 <= low <= high <= len(a)이면 인덱스가 *범위 안(in range)*에 있다고 해요. 그렇지 않으면 *범위 밖(out of range)*이에요. 슬라이스의 경우에는 위쪽 경계가 길이가 아니라 슬라이스 용량 cap(a)이에요. 상수(constant) 인덱스는 음수가 아니어야 하고, int 타입의 값으로 나타낼 수 있어야 하며(representable), 배열이나 상수 문자열의 경우 상수 인덱스는 반드시 범위 안에 있어야 해요. 두 인덱스가 모두 상수라면 low <= high를 만족해야 해요. 인덱스가 실행 시점에 범위를 벗어나면 run-time panic(런타임 패닉)이 발생해요.
untyped strings(비타입 문자열)을 제외하고, 잘라내는 피연산자가 문자열이나 슬라이스라면 슬라이스 연산의 결과는 피연산자와 같은 타입의 비상수(non-constant) 값이에요. 비타입 문자열 피연산자의 경우 결과는 string 타입의 비상수 값이네요. 잘라내는 피연산자가 배열이라면 그 배열은 주소 지정 가능(addressable)해야 하고, 슬라이스 연산의 결과는 배열과 같은 요소 타입을 가진 슬라이스예요.
유효한 슬라이스 표현식의 잘라내는 피연산자가 nil 슬라이스라면, 결과도 nil 슬라이스예요. 그 외에 결과가 슬라이스라면, 피연산자와 같은 기본 배열을 공유해요.
var a [10]int
s1 := a[3:7] // underlying array of s1 is array a; &s1[2] == &a[5]
s2 := s1[1:4] // underlying array of s2 is underlying array of s1 which is array a; &s2[1] == &a[5]
s2[1] = 42 // s2[1] == s1[2] == a[5] == 42; they all refer to the same underlying array element
var s []int
s3 := s[:0] // s3 == nil
전체 슬라이스 표현식 (Full slice expressions)
배열, 배열 포인터, 또는 슬라이스 a(문자열은 제외)에 대해, 주 표현식
a[low : high : max]
은 단순 슬라이스 표현식 a[low : high]와 같은 타입, 같은 길이, 같은 요소를 가진 슬라이스를 만들어 내요. 여기에 더해 결과 슬라이스의 용량을 max - low로 설정해서 제어해요. 첫 번째 인덱스만 생략할 수 있고, 생략하면 0으로 간주돼요. 배열 a를 슬라이스한 뒤를 볼게요.
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
이때 슬라이스 t는 타입이 []int, 길이가 2, 용량이 4이고, 다음과 같은 요소들을 가져요.
t[0] == 2
t[1] == 3
단순 슬라이스 표현식과 마찬가지로, a가 배열 포인터라면 a[low : high : max]는 (*a)[low : high : max]의 줄임 표현이에요. 잘라내는 피연산자가 배열이라면 그 배열은 주소 지정 가능(addressable)해야 해요.
0 <= low <= high <= max <= cap(a)이면 인덱스가 *범위 안(in range)*에 있고, 그렇지 않으면 *범위 밖(out of range)*이에요. 상수(constant) 인덱스는 음수가 아니어야 하고, int 타입의 값으로 나타낼 수 있어야 하며(representable), 배열의 경우 상수 인덱스는 반드시 범위 안에 있어야 해요. 여러 인덱스가 상수라면, 존재하는 상수들은 서로에 대해 범위 안에 있어야 해요. 인덱스가 실행 시점에 범위를 벗어나면 run-time panic(런타임 패닉)이 발생해요.
더 알아보기
- 슬라이스의 길이와 용량이 어떻게 다른지는 실제 코드에서
len과cap을 함께 살펴보면 감이 와요. - 슬라이스가 기본 배열을 공유한다는 성질 때문에, 부분 슬라이스 하나를 수정하면 원본이 함께 바뀌는 경우가 많아요. 이 점은 메모리 동작을 이해하는 데 핵심이에요.
- full slice expression으로 용량까지 명시하면 잘라낸 슬라이스가 원본 배열을 불필요하게 오래 붙잡는 문제를 줄일 수 있어요.