[Golang] Slice

2021-11-09 hit count image

Golang에서 Slice(슬라이스)에 대해 알아보고 사용하는 방법에 대해서 살펴보도록 하겠습니다.

개요

이번 블로그 포스트에서는 Golang의 Slice(슬라이스)에 대해 자세히 알아보고 사용하는 방법에 대해서 살펴보려고 합니다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.

Slice

슬라이스는 Golang에서 제공하는 동적 배열 타입(배열을 가리키는 포인터 타입)입니다.

  • 정적(Static): 컴파일 시점(Compile)에 결정
  • 동적(Dynamic): 실행 시점(Runtime)에 결정

다음은 Golang에서 배열을 선언하는 방법입니다.

var v [10]int

슬라이스는 다음과 같이 배열의 사이즈를 설정하지 않고 선언합니다.

var v []int

이를 확인하기 위해 main.go 파일을 생성하고 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  var a [2]string
  var b []string

  fmt.Println(a)
  fmt.Println(b)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
[ ]
[]

배열과 슬라이스

슬라이스는 배열을 가리키는 포인터 타입입니다. 이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func changeArr(arr2 [5]int) {
  arr2[0] = 100
}
func changeSlice(slice2 []int) {
  slice2[0] = 100
}

func main() {
  arr := [5]int{1, 2, 3, 4, 5}
  slice := []int{1, 2, 3, 4, 5}

  changeArr(arr)
  changeSlice(slice)

  fmt.Println(arr)
  fmt.Println(slice)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
[1 2 3 4 5]
[100 2 3 4 5]

배열은 포인터 타입이 아니므로, changeArr함수를 호출할 때, arr2라는 새로운 인스턴스를 생성한 후, arr의 내용을 복사하게 됩니다. 따라서 changeArr함수안에서 arr2값을 변경하여도 arr값은 변경되지 않습니다.

하지만, 슬라이스는 포인터 타입입니다. 따라서 changeSlice함수를 호출하면, slice2라는 새로운 인스턴스를 생성하는게 아니라 slice의 메모리 주소값을 복사하게 됩니다. 따라서 changeSlice함수 안에서 slice2값을 변경하면 slice값도 변경됩니다.

len과 cap

슬라이스는 배열과는 다르게 len이외에도 cap이라는 데이터를 가지고 있습니다. 슬라이스의 len은 현재 슬라이스에서 사용중인 길이를 의미하며, cap은 현재 슬라이스의 총 크기(사용중인 길이 + 비어있는 길이)를 의미합니다.

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  slice1 := []int{1, 2, 3, 4, 5}
  slice2 := make([]int, 2, 10)

  fmt.Printf("slice1(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
slice1(0xc0000b2000): len=5 cap=5 [1 2 3 4 5]
slice2(0xc0000b4000): len=2 cap=10 [0 0]

make 함수

Golang에서 제공하는 make 함수를 사용하여 슬라이스를 생성할 수 있습니다.

slice1 := make([]int, 10)
fmt.Println(slice1)

위와 같이 make함수를 사용하여 10의 크기를 갖는 슬라이스를 생성할 수 있습니다.

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

또는, 위와 같이 make함수를 사용하여 10의 크기를 갖는 슬라이스를 생성하고 두 개의 요소만 사용하도록 할 수 있습니다.

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  slice1 := make([]int, 10)
  fmt.Println(slice1)

  slice2 := make([]int, 2, 10)
  fmt.Println(slice2)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
[0 0 0 0 0 0 0 0 0 0]
[0 0]

슬라이싱

배열의 일부를 잘라내어 슬라이스를 만드는 것을 슬라이싱(Slicing)이라고 합니다.

  • Array => Slicing => Slice

Golang에서 슬라이싱은 다음과 같이 사용할 수 있습니다.

Array[startIndex:endIndex]

startIndex에서부터 endIndex 직전(endIndex -1)까지 값을 반환합니다. 이때, 반환된 Slice의 cap은 startIndex부터 Array의 마지막 길이까지 입니다.

이를 확인하기 위해 main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  array := [5]int{1, 2, 3, 4, 5}
  slice := array[1:2]

  fmt.Printf("array: len=%d %v\n", len(array), array)
  fmt.Printf("slice: len=%d cap=%d %v\n", len(slice), cap(slice), slice)
}

이를 실행하면 다음과 같은 결과를 얻을 수 있습니다.

# go run main.go
array: len=5 [1 2 3 4 5]
slice: len=1 cap=4 [2]

슬라이스를 슬라이싱하여 슬라이스를 생성할 수 있습니다. 또한, 다음과 같이 startIndex에 0을 지정하면 처음부터 슬라이싱을 할 수 있으며, 0은 생략이 가능합니다.

slice = []int{1, 2, 3, 4, 5}
slice1 := slice[0:3]
slice2 := slice[:3]

다음과 같이 사용하는 경우, 마지막까지 슬라이싱할 수 있으며, 마지막 인덱스를 생략할 수 있습니다.

slice = []int{1, 2, 3, 4, 5}
slice1 = slice[2:len(slice)]
slice2 = slice[2:]

마지막으로, 다음과 같이 전체를 슬라이싱할 수 있으며, 배열을 슬라이스로 바꿀때, 자주 사용됩니다.

arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:]

슬라이싱은 새로운 변수를 만드는 것이 아니라, 단순히 메모리 주소를 가리키기만 함으로 다음과 같이 사용할 수 있습니다.

array1 := [100]int{1: 1, 2: 2, 99: 100}
slice1 = array1[1:10]
slice2 = slice1[2:99]

fmt.Println(slice1)
fmt.Println(slice2)

위의 예제에서 slice1array1의 메모리 주소를 가리키므로, slice1에서 99번째까지의 값을 가져와서 slice2를 만드는 것이 가능합니다.

cap 사이즈 조절 슬라이싱

Array를 슬라이싱하게 되면, 반환된 Slice의 cap은 startIndex부터 Array의 마지막 길이까지가 됩니다. 하지만, 다음과 같이 슬라이싱을 할 때 maxIndex를 추가하여 cap 사이즈를 조절할 수 있습니다.

slice[startIndex:endIndex:maxIndex]
slice1 = []int{1, 2, 3, 4, 5}
slice2 = slice1[1:3:4]

fmt.Printf("slice1: len=%d cap=%d %v\n", len(slice1), cap(slice1), slice1)
fmt.Printf("slice2: len=%d cap=%d %v\n", len(slice2), cap(slice2), sl

append 함수

슬라이스의 끝에 요소를 추가하기 위해서는 append 함수를 사용할 필요가 있습니다. append 함수를 사용하여 슬라이스에 요소를 추가한 경우, 새로운 요소가 추가된 새로운 슬라이스가 반환됩니다.

var slice1 = []int{1, 2, 3}
slice2 := append(slice1, 4)

이때, 새로운 슬라이스는 기존 슬라이스와 동일한 메모리 주소를 사용할 때도 있고, 동일한 메모리 주소를 사용하지 않을 때도 있습니다.

append를 사용하여 새로운 요소를 슬라이스에 추가할 때, 슬라이스에 새로운 요소를 추가할 충분한 공간이 있는 경우, 새로운 슬라이스는 기존 슬라이스의 메모리 주소를 사용하게 됩니다. 하지만, 기존 슬라이스에 충분한 공간이 없는 경우, append는 새로운 메모리 주소에 기존 슬라이스를 복사한 후, 새로운 요소를 추가하게 됩니다.

이를 확인하기 위해, main.go 파일을 다음과 같이 수정합니다.

package main

import "fmt"

func main() {
  slice1 := make([]int, 3)
  slice2 := append(slice1, 4)

  fmt.Println("[New splice]")
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice1 changed ========================================================")
  slice1[0] = 100
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice2 changed ========================================================")
  slice2[0] = 200
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  slice1 = make([]int, 1, 3)
  slice2 = append(slice1, 4)

  fmt.Println("[Same slice]")
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice1 changed ========================================================")
  slice1[0] = 100
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)

  fmt.Println("slice2 changed ========================================================")
  slice2[0] = 200
  fmt.Printf("slice(%p): len=%d cap=%d %v\n", slice1, len(slice1), cap(slice1), slice1)
  fmt.Printf("slice2(%p): len=%d cap=%d %v\n", slice2, len(slice2), cap(slice2), slice2)
}

이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.

# go run main.go
[New splice]
slice(0xc00012a000): len=3 cap=3 [0 0 0]
slice2(0xc00012c000): len=4 cap=6 [0 0 0 4]
slice1 changed ========================================================
slice(0xc00012a000): len=3 cap=3 [100 0 0]
slice2(0xc00012c000): len=4 cap=6 [0 0 0 4]
slice2 changed ========================================================
slice(0xc00012a000): len=3 cap=3 [100 0 0]
slice2(0xc00012c000): len=4 cap=6 [200 0 0 4]
[Same slice]
slice(0xc00012a018): len=1 cap=3 [0]
slice2(0xc00012a018): len=2 cap=3 [0 4]
slice1 changed ========================================================
slice(0xc00012a018): len=1 cap=3 [100]
slice2(0xc00012a018): len=2 cap=3 [100 4]
slice2 changed ========================================================
slice(0xc00012a018): len=1 cap=3 [200]
slice2(0xc00012a018): len=2 cap=3 [200 4]

슬라이스의 cap이 새로운 요소를 추가할 여유가 있는 경우, 동일한 메모리 주소를 사용하는 것을 확인할 수 있습니다. 따라서, 슬라이스의 append를 사용할 때, 기존 슬라이스와 새로운 슬라이스가 동시에 변경될 수 있으므로 주의하여 사용해야 합니다.

슬라이스 복사

다음과 같이 슬라이싱을 통해 슬라이스를 복사할 수 있습니다.

slice1 = []int{1, 2, 3, 4, 5}
slice2 = slice1[:]

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

하지만, 슬라이싱은 새로운 슬라이스를 생성하는 것이 아니라 메모리 주소를 공유하는 것이므로, 위와 같이 slice2의 값을 변경하는 경우 slice1의 값도 변경이 됩니다.

이와 같은 문제를 해결하기 위해서는 다음과 같이 slice1과 동일한 크기의 slice2를 만들고, 루프를 통해 값을 복사할 수 있습니다.

slice1 = []int{1, 2, 3, 4, 5}
slice2 = make([]int, len(slice1))

for i, v := range slice1 {
  slice2[i] = v
}

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

또는 다음과 같이 append 함수를 통해 새로운 슬라이스를 만들고, 모든 내용을 추가하여 복사할 수 있습니다.

slice1 = []int{1, 2, 3, 4, 5}
slice2 = append([]int{}, slice1...)

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

마지막으로, make 함수를 사용하여 동일한 크기의 슬라이스를 생성한 후, copy 함수를 사용하여 모든 값을 복사할 수 있습니다.

slice1 = []int{1, 2, 3, 4, 5}
slice2 = make([]int, len(slice1))

copy(slice2, slice1)

slice2[1] = 100

fmt.Println(slice1)
fmt.Println(slice2)

Golang에서 copy 함수는 다음과 같이 사용할 수 있습니다.

copy(dst, src)

삭제

다음과 같이 슬라이스에서 특정 요소를 삭제할 수 있습니다.

slice := []int{1, 2, 3, 4, 5, 6}
deleteIdx := 2

fmt.Println(slice)
for i := deleteIdx + 1; i < len(slice); i++ {
  slice[i-1] = slice[i]
}
slice = slice[:len(slice)-1]
fmt.Println(slice)

또는 다음과 같이 append를 사용하여 삭제할 수 있습니다.

slice = []int{1, 2, 3, 4, 5, 6}
deleteIdx = 2

fmt.Println(slice)
slice = append(slice[:deleteIdx], slice[deleteIdx+1:]...)
fmt.Println(slice)

마지막으로 copy 함수를 사용하여 삭제할 수 있습니다.

slice = []int{1, 2, 3, 4, 5, 6}
deleteIdx = 2

fmt.Println(slice)
copy(slice[deleteIdx:], slice[deleteIdx+1:])
slice = slice[:len(slice)-1]
fmt.Println(slice)

요소 삽입

Golang에서는 다음과 같이 for 루프를 사용하여 슬라이스에 새로운 요소를 삽입할 수 있습니다.

slice := []int{1, 2, 3, 4, 5, 6}
insertIdx := 2

fmt.Println(slice)
slice = append(slice, 0)
for i := len(slice) - 2; i >= insertIdx; i-- {
  slice[i+1] = slice[i]
}
slice[insertIdx] = 100
fmt.Println(slice)

또는 다음과 같이 append를 사용하여 슬라이스에 새로운 요소를 삽입할 수 있습니다.

slice = []int{1, 2, 3, 4, 5, 6}
insertIdx = 2

fmt.Println(slice)
slice = append(slice[:insertIdx], append([]int{100}, slice[insertIdx:]...)...)
fmt.Println(slice)

마지막으로, copy 함수를 사용하여 슬라이스에 새로운 요소를 삽입할 수 있습니다.

slice = []int{1, 2, 3, 4, 5, 6}
insertIdx = 2

fmt.Println(slice)
slice = append(slice, 0)
copy(slice[insertIdx+1:], slice[insertIdx:])
slice[insertIdx] = 100
fmt.Println(slice)

슬라이스 정렬

Golang에서 기본적으로 제공하는 sort 패키지를 사용하여, 슬라이스를 정렬할 수 있습니다.

package main

import (
  "fmt"
  "sort"
)

func main() {
  slice := []int{6, 3, 1, 5, 4, 2}

  fmt.Println(slice)
  sort.Ints(slice)
  fmt.Println(slice)
}

다음과 같이 구조체 슬라이스도 sort를 사용하여 정렬할 수 있습니다.

package main

import (
  "fmt"
  "sort"
)

type Student struct {
  Name string
  Age  int
}

type Students []Student

func (s Students) Len() int           { return len(s) }
func (s Students) Less(i, j int) bool { return s[i].Age < s[j].Age }
func (s Students) Swap(i, j int)      { s[i], s[j] = s[j], s[i] }

func main() {
  students := []Student{
    {"c", 31},
    {"a", 20},
    {"b", 21},
    {"d", 19},
  }

  fmt.Println(students)
  sort.Sort(Students(students))
  fmt.Println(students)
}

완료

이것으로 Golang에서 슬라이스를 선언하고 사용하는 방법에 대해서 알아보았습니다. 또한 배열과 슬라이스의 차이점, 슬라이싱을 통해 배열에서 슬라이스를 만드는 방법도 알아보았습니다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
Posts