Featured image of post [golang] nil과 빈 slice의 차이

[golang] nil과 빈 slice의 차이

Go 100가지 실수 패턴과 솔루션을 보고 정리한 글입니다.

nil 슬라이스와 빈 슬라이스

nil 과 빈 슬라이스 는 개념적으로 혼동하기 쉬우나 상황에 따라 적합한 방법으로 만들어야 할 수 있습니다.

Go 언어에서 슬라이스 초기화 하는 법

Go 에서 슬라이스 를 초기화 하는 방법에는 크게 네가지가 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func main() {
    var s []string // 방법 1
    log(1, s)
    
    s = []string(nil) // 방법 2
    log(2, s)
    
    s = []string{} // 방법 3
    log(3, s)
    
    s = make([]string, 0) // 방법 4
    log(4, s)
}

func log(i int, s []string) {
    fmt.Printf("%d: empty: %v\tnil: %t\n", i, len(s) == 0, s == nil)
}

위 코드의 실행 결과는 다음과 같습니다.

1
2
3
4
1: empty: true  nil: true
2: empty: true  nil: true
3: empty: true  nil: false
4: empty: true  nil: false

네가지 방식 모두 길이가 0인 empty 슬라이스 입니다. 따라서 nil 인 슬라이스 도 empty 슬라이스 입니다.

무의미한 할당 피하기

nil 슬라이스와 빈 슬라이스 의 가장 큰 차이점은 할당 방식입니다. nil 슬라이스로 초기화하면 할당 할 필요가 있지만 빈 슬라이스 는 할당할 필요가 있습니다. 따라서 슬라이스 를 리턴하는 함수를 작성 할 경우 다른 언어에서 안전을 위해 빈 컬렉션을 리턴하는 것과 달리 nil 슬라이스 를 리턴하는 것이 좋습니다.

1
2
3
4
5
6
7
func example() []string {
    var s []string // 방법 1
    if (foo()) {
        s = append(s, "foo")
    }
    return foo
}

위와 같은 함수의 경우와 같이 foo 가 false 인 경우 무의미한 빈 할당이 발생할 수 있기 때문에 방법 1을 사용하는 것이 좋습니다.

생성 할 slice 의 크기를 아는 경우

1
2
3
4
5
6
7
func example2(ints []int) []string {
    s := make([]string, len(ints)) // 방법 4
    for i, v := range ints {
        s[i] = strconv.Itoa(v)
    }
    return s
}

다만 생성할 slice 의 크기를 아는 경우 방법 4를 사용하여 불필요한 할당과 복제를 피하는 것이 좋습니다.

편의 구문(syntex sugar)

방법 2의 경우 널리 사용되지는 않으나, append 와 같은 함수를 호출할 때 nil 슬라이스 를 한 줄로 전달할 수 있어 편의를 위해 사용할 수 있습니다. 반드시 가독성이 높아지는 방법은 아니지만 알아두면 좋습니다.

1
2
3
4
var s1 []int // 방법 1
s1 = append(s1, 42)

s2 := append([]int(nil), 42) // 방법 2

초기값을 넣을 경우

방법 3의 경우 초기 값을 넣을 경우 적합합니다.

1
s := []string{"foo", "bar", "baz"} // 방법 3

하지만 생성할 때 초기값을 넣을 필요가 없는 경우 이 방법을 사용 해선 안 됩니다. 슬라이스가 nil 이 아니라는 점만 제외하면 방법 1과 차이가 없으며 무의미한 할당만이 발생합니다.

nil 과 빈 슬라이스 를 구분해야 하는 경우

어떤 라이브러리 에서는 nil 슬라이스 와 빈 슬라이스 를 구분하는 경우가 있습니다. 예를 들어 encoding/json 패키지가 그렇습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
var s1 []float32 // nil slice
customer1 := customer{
    ID: "foo",
    Operations: s1,
}
b, _ := json.Marshal(customer1)
fmt.Println(string(b))

s2 := make([]float32, 0) // nil 이 아닌 empty slice
customer2 := customer{
    ID: "bar",
    Operations: s2,
}
b, _ = json.Marshal(customer2)
fmt.Println(string(b))

이 예제를 실행시 두 구조체에 대한 마샬링 결과가 다르다는 점에 주목해야합니다.

1
2
{"ID":"foo","Operations":null}
{"ID":"bar","Operations":[]}

여기서 nil 슬라이스는 null 로 마샬링 되었으나 nil 이 아닌 빈 슬라이스는 빈 배열로 마샬링 되었습니다. null 과 [] 를 엄격히 구분하는 JSON 클라이언트 를 처리할 때는 이러한 차이를 명확히 구분 해야합니다.

슬라이스가 비었는지 제대로 확인하는 방법

nil 슬라이스와 empty 슬라이스에는 차이가 있으므로 슬라이스가 비어있는지 확인하기 위한 코드를 명확하게 작성하지 않으면 미묘한 버그가 발생할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func handleOperations(id string) {
    operations := getOperations(id)
    if operations != nil { // 슬라이스가 비어있는지 확인
        handle(operations)
    }
}

func getOperations(id string) []float32 {
    operations := make([]float32, 0) // 슬라이스 초기화
    if id == "" {
        return operations // id 가 비어있으면 operations 반환
    }

    // operations 에 원소 추가

    return operations
}

위 예제에서는 operations 슬라이스가 nil 인지 확인하는 방식으로 슬라이스에 원소가 있는지 확인했습니다. 하지만 getOperations 에서는 nil 슬라이스가 아닌 empty 슬라이스를 리턴하기 때문에 operations != nil 는 항상 true 가 되며 문제가 발생합니다.

1
2
3
4
5
6
7
func getOperations(id string) []float32 {
    operations := make([]float32, 0)
    if id == "" {
        return nil
    }
    ...
}

해결하는 방법 중 하나로는 getOperations 함수에서 id 가 비어있을 시 nil 을 반환하는 것입니다. 이렇게 바꾸면 슬라이스가 nil 인지 확인하는 코드를 사용할 수 있습니다. 하지만 외부라이브러리를 사용하는 경우 와 같이 호출 대상을 수정할 수 없을 경우 문제가 발생할 수 있으므로 슬라이스가 empty 슬라이스 인지 nil 인지 확인할 필요가 있을수 있습니다. 그런 경우는 다음과 같이 길이를 검사하면 됩니다.

1
2
3
4
5
6
func handleOperations(id string) {
    operations := getOperations(id)
    if len(operations) != 0 { // 슬라이스가 비어있는지 확인
        handle(operations)
    }
}

앞에서 확인한 것과 같이 빈 슬라이스는 본래 정의에 의해 길이가 0 이므로 nil 슬라이스도 항상 비어있습니다. 그래서 슬라이스의 길이를 검사하면 보든 경우를 골라낼 수 있습니다. ‘따라서 길이를 검사하는 방법이 가장 바람직합니다. 우리가 호출하는 함수를 항상 수정할 수 없기 때문입니다.

Go 언어의 공식 문서에서는 인터페이스를 디자인 할 때 nil 슬라이스와 empty 슬라이스를 구분하는 방식을 피하라고 권장합니다. 미묘한 에러를 발생시킬 수 있기 때문입니다. 리턴하는 슬라이스가 nil 인지 아니면 비어있는지에 따라 의미나 구문이 달라져선 안됩니다. 호출하는 입장에서는 두 경우 모두 의미가 같기 때문입니다.

Hugo로 만듦
JimmyStack 테마 사용 중