'nil'에 해당되는 글 1건

  1. 2022.06.30 (go-03) Go Slice 타입
반응형

 

 

기본 내장 타입에 추가하여 Array, Slice, Map 타입에 대해서 살펴본다.

Go 에서는 Array 는 거의 사용하지 않는다. 그러므로 이번에는 Slice 와 Map 타입에 대해서만 살펴 본다.

참고로 Go 에서는 nil 이라는 값이 있다. 이는 값이 없다라는 뜻인데, 기본 타입에는 이 nil 과의 비교문 (==)을 쓸 수 없다. (이전에 기본 타입은 전부 zero 값이 nil 이 아니다.)

**nil 과 비교문이 가능한 타입은 앞으로 설명할 slice, map, pointer 이며, 이 3개의 zero 값이 nil 이다.**

 

Slice

slice 는 값을 순차적으로 가질 수 있는 데이터 구조이다. 파이썬에서는 list 와 비슷하다고 생각하면 된다. 단지 다른 것은 하나의 slice 에는 사전에 지정된 타입만 저장될 수 있다.

다음은 int 타입을 slice 저장 한 후에 출력한 결과이다. [] 는 slice 를 만들겠다는 의미이고, 바로 이어서 type 이 오며, 초기값을 지정하기 위해서 { } 를 사용한다.

var s = []int{1, 2, 3, 4, 5}
fmt.Println(s)

--- output ---
[1 2 3 4 5]

 

slice 는 초기화 값을 지정하지 않으면 nil 을 나타낸다. 즉, 변수 선언만 하면 항상 nil 값을 갖게 된다. lencap 을 사용하면 slice 가 가지가 있는 길이와 용량을 알 수 있다.

Slice 는 연속적인 값을 가지고 있는 메모리 주소인 포인터, 길이, 용량 으로 구성되어 있다. (이 부분은 매우 중요하다.)

길이는 실제 들어있는 값이고 용량은 해당 값을 가질 수 있는 크기라고 할 수 있다.

아래 s1 은 nil 값이기 때문에 len 과 cap 이 모두 0 이다.

var s1 []int
fmt.Println(s1 == nil)
fmt.Println(len(s1), cap(s1))

--- output ---
true
0 0

 

슬라이스는 배열처럼 사용하여 s1[0] 첫번째 값을 가져올 수 있다.

하지만 선언만 하여 nil 인 상태에서는 해당 index 를 사용하게 되면 런타임 에러가 난다.

var s1 []int
fmt.Println(s1[0])

--- output ---
panic: runtime error: index out of range [0] with length 0

 

s1 slice 가 nil 인 상태에서도 append 내장 함수를 사용하면 값을 추가할 수 있다(그렇기 때문에 특별한 경우를 제외하고는 nil 값인 변수 선언만 하여 사용하면 된다). append 함수를 쓰면 nil 이라 하더라도 slice 에 값을 추가 할 수 있다. 물론 값이 추가된 결과 return 값은 변수에 할당해야 한다. (변수가 달라도 됨)

var s1 []int
s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[1]

 

slice 에 slice 를 합쳐서 추가할 수 있다. python 의 extend 와 비슷하다고 보면된다. 두번째 slice 에는 여러 아규먼트가 들어간다는 것을 표현해 주기 위해서 ... 을 붙혀야 한다.

var s1 []int
s1 = append(s1, 1)

s2 := []int{2, 3, 4, 5}
s1 = append(s1, s2...)  // append(s1, 2, 3, 4, 5) 와 동일함
fmt.Println(s1)

 

slice 는 값이 추가되어 길이가 커져서 용량에 도달하면 용량이 자동으로 확장된다.

var s []int
fmt.Println(s, len(s), cap(s))
s = append(s, 1)
fmt.Println(s, len(s), cap(s))
s = append(s, 2)
fmt.Println(s, len(s), cap(s))
s = append(s, 3)
fmt.Println(s, len(s), cap(s))
s = append(s, 4)
fmt.Println(s, len(s), cap(s))
s3 := append(s, 5)
fmt.Println("s:", s, len(s), cap(s))
fmt.Println("s3", s3, len(s3), cap(s3))

-- output ---
[] 0 0
[1] 1 1
[1 2] 2 2
[1 2 3] 3 4
[1 2 3 4] 4 4
s: [1 2 3 4] 4 4
s3 [1 2 3 4 5] 5 8

 

 

make 내장 함수

slice 와 map 에서 make 내장 함수를 사용하면 nil 이 아닌 empty slice 를 만들 수 있다. make 함수의 첫번째 아큐먼트는 slice type, 두번째는 길이, 세번째는 용량이다. 세번째 용량은 생략이 가능하다.

아래 make 로 생성한 s 는 nil 과의 비교가 false 임을 알 수 있다.

s := make([]int, 0, 5)
fmt.Println(s == nil)
fmt.Println(s, len(s), cap(s))

--- output ---
false
[] 0 5

 

make 로 길이를 지정하면 그 길이 만큼은 zero 값이 채워진 slice 가 만들어 진다. 그러니 필요없이 길이를 1 이상으로 지정하면 초기 메모리만 차지할 수 있다. 아래는 make 로 만든 s1 slice 에 0번째와 1번째 index 까지 0 값이 채워진 것을 볼 수 있다.

s1 := make([]int, 2)
fmt.Println(s1)

s1 = append(s1, 1)
fmt.Println(s1)

--- output ---
[0 0]
[0 0 1]

 

길이가 0 인 slice 는 아래와 같이 만들 수 도 있다. nil 이 아니면서 길이가 0 인 slice 는 JSON 으로 변환할 때 사용된다.

var s = []int{}   // make([]int, 0) 와 동일하다.

 

 

Slice 자르기

Slice 를 자르는 것은 : 을 이용해서 자를 수 있다. 하지만 자른다고 해도 최종적으로 보는 Reference 는 동일하다. 아래를 보면 이를 이해할 수 있다.

첫번째 단락에서 slice 를 자르면, b 는 0 index 이상, 3 index 미만 까지의 slice 를 갖고, c 는 2 index 이상 끝까지의 slice 값을 갖는다. 중요한 것은 c 처럼 중간에서 마지막까지 자른 slice 는 용량이 그 길이만큼으로 줄어든다는 것이다. (앞의 자른 부분이 없어졌기 때문이다)

두번째 단락에서 c[0] 에 30 을 대입하면 slice 자른 것은 같은 reference 를 바라보기 때문에 a 와 b 에 모두 영향을 준다.

세번째 단락도 마찬가지 이다.

a := []int{1, 2, 3, 4, 5}
b := a[:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

c[0] = 30
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)
fmt.Println("================")

b = append(b, 40)
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

--- output ---
cap(a): 5 cap(b): 5 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
================
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
================
a: [1 2 30 40 5]
b: [1 2 30 40]
c: [30 40 5]

 

Slice 를 자른 것이 append 를 사용해서 추가할 때 같은 레퍼런스를 처다보지 않게 하려면, 용량을 길이와 동일하게 하면 된다. 그러면 append 했을 경우 용량이 초과되어 새로운 slice 가 생겨서 return 되므로 서로 영향이 가지 않는다.

아래 처럼 a 를 잘라서 b 를 만들 때 :3:3 과 같이 마지막에 용량 3을 추가하여 b slice 의 길이와 용량을 똑같이 맞췄다. 그렇게 되면 c[0] 처럼 값을 바꾸는 것은 영향을 받지만, append(b, 40) 과 같이 append 한 것은 새로운 slice 가 만들어져서 return 되므로 a 와 c 에 영향을 주지 않는다.

a := []int{1, 2, 3, 4, 5}
b := a[:3:3]
c := a[2:]
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== (c[0] = 30) =====")
c[0] = 30
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)

fmt.Println("===== append(b, 40) ======")
b = append(b, 40)
b[0] = 10
fmt.Println("cap(a):", cap(a), "cap(b):", cap(b), "cap(c):", cap(c))
fmt.Println("a:", a)
fmt.Println("b:", b)
fmt.Println("c:", c)


--- output ---
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 3 4 5]
b: [1 2 3]
c: [3 4 5]
===== (c[0] = 30) =====
cap(a): 5 cap(b): 3 cap(c): 3
a: [1 2 30 4 5]
b: [1 2 30]
c: [30 4 5]
===== append(b, 40) ======
cap(a): 5 cap(b): 6 cap(c): 3
a: [1 2 30 4 5]
b: [10 2 30 40]
c: [30 4 5]
c: [30 4 5]
===== append(b, 40) ======
a: [1 2 30 4 5]
b: [1 2 30 40]
c: [30 4 5]

 

반응형
Posted by seungkyua@gmail.com
,