Computer_Language/GO

[Go] 7. Go(golang)의 배열과 슬라이스

Joo-Topia 2019. 10. 1. 23:58

오늘 정리할 내용은 배열(array)과 슬라이스(slice)이다.

배열에 대한 내용은 타 언어와 매우 유사하여 금방 정리하겠지만, 슬라이스에 대한 내용은 익숙한 개념이 아니기 때문에 추후 수정이 있을 것 같다.

C++의 벡터와 유사한 것 같기도 한데.. 일단 정리해보자

 

Go언어는 "컬렉션"이라는 개념이 존재한다. 두 개 이상의 변수를 모아 놓은 것을 말하는 단어이며, 오늘 정리할 배열과 슬라이스도 "컬렉션"에 포함된다.

Go언어의 배열


Go언어의 배열은 C언어의 배열, 즉 Managed Language의 배열과 유사하다. 정적 배열이라고 하는데, 쉽게 말해서 컴파일 과정에서 배열의 크기가 정해져 있어야 한다는 뜻이다.

먼저 가장 기본적인 코드를 실행시켜보자.

package main

import "fmt"

func main() {
    var arr [3]int
    fmt.Println(arr)
}

결과

 

C언어와 다르게 모든 원소가 자동으로 0으로 초기화되었다. C언어에서 배열을 초기화하는 몇 글자의 명령어가 항상 귀찮았는데 참 좋은 기능인 것 같다..ㅋㅋ

 

다음은 Go언어 배열의 조금 특이한 초기화 방법을 사용한 코드이다. (가독성을 위해 import 구문까지는 생략하겠다)

func main() {
    var arr [3]int
    fmt.Println(arr)
    arr = [3]int{1, 2, 3}
    fmt.Println(arr)
}

결과

C언어라면 "int arr[3] = {1, 2, 3}"로 해결되지만, Go언어에서는 "[3]int"이 배열 앞에도 붙어있는 것을 볼 수 있다. Go언어에서는 "[3]int"와 "[4]int"도 전혀 다른 자료형으로 취급하는데, 그래서 배열 앞에 한번 더 써 주는 것 같다.

 

 

마지막으로 조금 특이한 문법을 사용한 코드이다.

func main() {
    var arr2 = [...]int{1, 2, 3, 4}
    fmt.Println(arr2)
}

결과

[...] 용법을 사용하면, 배열의 크기를 자동으로 설정해준다. 하지만 동적 배열과는 다른 개념이니 혼동하지 말아야 한다. (이후에 크기를 바꿀 수 있다는 게 아니다!)

 

Go언어의 슬라이스


C언어에서 메모리를 동적으로 관리하기 위해 malloc 등의 함수를 사용했다면 Go언어에는 슬라이스가 있다.

슬라이스는 크기를 미리 지정하지 않고 필요한 크기를 동적으로 변경할 수 있다.

 

먼저 "Nil silce"를 선언한 뒤 슬라이스를 사용해보자.

func main() {
    //nil slice 선언
    var sli []int
    fmt.Println(sli)

    sli = []int{1, 2, 3}
    fmt.Println(sli)
    sli = sli[1:3]
    fmt.Println(sli)
}

결과

Nil slice를 출력하면 빈 배열이 출력되는 것을 볼 수 있다.

후에 슬라이스에 리터럴 값을 지정해주고 다시 선언했던 slice를 출력해보면 [1, 2, 3]이 출력된다.

현재 슬라이스는 배열 [1,2,3]의 앞부분을 가리키고 있다. 이 상태에서 slice의 두 번째 원소부터 세 번째 원소까지를 가리키는 slice로 재 정의를 해본 후 출력하면 [2, 3]이 출력되는 것을 볼 수 있다.

 

슬라이스를 사용하기 위해선 Go언어의 내장 함수 make에 대해 공부할 필요가 있다.

package main

import "fmt"

func main() {
    sli_m := make([]int, 0, 5)
    sli_m = append(sli_m, 1)
    fmt.Println("sli_m의 요소 :", sli_m, "sli_m의 길이 :", len(sli_m), "sli_m의 용량 :", cap(sli_m))

    for i := 2; i <= 7; i++ {
        sli_m = append(sli_m, i)
        fmt.Println("sli_m의 요소 :", sli_m, "sli_m의 길이 :", len(sli_m), "sli_m의 용량 :", cap(sli_m))
    }
}

결과

make(슬라이스 타입, 슬라이스 길이, 슬라이스 용량) 형식에 맞춰서 길이 0 용량 5의 []int형 슬라이스를 선언해준다.

append함수를 통해 원소를 하나 씩 집어넣을 수 있다.

특이한 점은 append를 통해서 원소를 추가하다가 길이가 초기에 선언된 용량을 넘어서게 되면 용량이 자동으로 두 배가 되는 것을 볼 수 있다. 아마 C++의 벡터에서 용량이 자동으로 증가하는 것과 비슷한 원리인 것 같다. C++벡터에 대해서는 따로 정리를 해놓지 않았지만 추후에 정리해서 이 글에 링크를 달아 둘 예정이다.

 

 

마지막으로 슬라이스의 병합과 복사에 대해 정리한 코드이다.

func main() {
	sli1 := []int{1, 2}
	sli2 := []int{3, 4}
	sli3 := append(sli1, sli2...)
	fmt.Println(sli3)

	sli4 := make([]int, 0, 10)
	copy(sli4, sli3)
	fmt.Println(sli4)

	sli5 := make([]int, 10, 10)
	copy(sli5, sli3)
	fmt.Println(sli5)
}

결과

이 전에 사용한 append함수를 slice자체를 추가하기 위해서는 두 번째 인자 뒤에 "..."을 붙여줘야 한다.

결과 사진의 첫 번째 줄을 보면 정확하게 slice가 추가된 것을 볼 수 있다.

 

slice의 copy에 대한 결과를 보면 조금 이상한 결과가 보인다. 복사 대상이 될 slice의 length가 0 일 경우 복사가 되지 않고 속이 빈 slice가 출력되는 것을 볼 수 있다. 복사를 하기 위해서는 복사하길 원하는 슬라이스의 길이만큼의 공간이 있어야 복사가 되기 때문이다. sli5의 출력을 보면 0으로 초기화된 10개의 원소 중 앞의 네 개의 원소는 정확하게 복사가 된 것을 볼 수 있다.

슬라이스를 복사하는 방법에는 copy함수 말고 대입 연산으로도 복사를 할 수 있다. 예상할 수 도있겠지만 이 복사는 얕은 복사가 된다. 예시 코드의 결과를 보고 정리해보겠다.

func main() {
    sli6 := sli5
    sli6[0] = 5
    fmt.Println(sli5)
    fmt.Println(sli6)
}

(sli5는 위 코드에서 이어지는 변수이다.)

위 코드의 결과에서 sli5는 [1 2 3 4 0 0 0 0 0 0]가 출력되었지만 얕은 복사를 수행한 sli6을 수정하니 원본 sli5의 값도 수정되는 것을 볼 수 있다.

 

마무리


정리하다 보니 내용이 많이 길어졌다. 하지만 슬라이스는 Go언어에서 정말 많이 사용하는 기능이기 때문에 제대로 정리해 둘 필요가 있다고 생각했다.

나도 영향력 있는 개발자가 되기 위해 매일 공부할 것이다.

 

*구름 edu의 바로 실행해보면서 배우는 Go Lang"을 수강 후 정리한 내용입니다.
소스코드는 깃허브 저장소를 참고!