본문으로 바로가기

1. slice 기초, make을 이용한 slice 선언

 

위 방식과 달리 size가 정해지지 않은 array는 slice라고 한다.

사실 arr와 slice는 메모리 할당 방식에서 다르기 때문에 같은 타입이 아니다.

앞에서 언급했듯 arr는 그냥 값이다. 반면 slice는 레퍼런스 타입이다.

 

그리고 Go에서는 배열을 많이 안쓴다. 거의 슬라이스만 쓴다. 슬라이스에서 배열의 모든 기능을 구현할 수 있어서 배열은 잘 사용이 안된다. 심지어 effective go에서도 array를 만들어 놓기는 했지만 슬라이스를 사용하라고 한다.

 

var num int = 3        // int형 크기 메모리 할당
var arr = [2]int{1, 2} // int형 2개 크기 메모리 할당

// slice
var slice []int        // 슬라이스는 크기를 몰라 배열의 일부를 가리키는 포인터만 만듦
slice[0] = 1           // Error 메모리를 만들지 않아서 존재하지도 않기 때문에 a[0] = 1과 같이 값을 지정할 수 없습니다.
func main() {
	var sl []int = []int{1, 2, 3} // 보통은 이렇게 바로 초기화 함
	sl := []int{1, 2, 3} // shortcut이 선호됨
    
	fmt.Println(sl)
}

 

slice의 struct는 배열의 위치를 가리키는 pointer, 배열 길이 len, 전체 크기인 cap 메모리를 들고 있다.

각각 int 타입으로 슬라이스의 크기는 24바이트이다.

make 함수를 이용하여 len, cap을 지정하는 방식으로 슬라이스를 생성할 수도 있다.

원하는 만큼 용량을 설정한 슬라이스를 만들고 싶다면 make 함수를 이용하면 된다. 

make(슬라이스 타입, 슬라이스 길이, 슬라이스의 용량) 으로 선언할 수 있습니다. 여기서 용량(Capacity)은 생략해서 선언할 수 있습니다.

 

 

func main() {
	slice := make([]int, 3) // []int 슬라이스, len이 3
	fmt.Println(slice) // [0 0 0]

	slice2 := make([]int, 3, 5) // []int 슬라이스 len이 3, cap이 5
	fmt.Println(slice2) // [0 0 0] 이라 출력은 되는데 나머지 2개는 추가될 요소를 위해 비워둠
}

 

len, cap 함수를 통해 슬라이스의 길이와 용량(cap)을 알 수 있다. 

func main() {
	x := []int{4, 6, 8, 9, 11, 23, 46}
	fmt.Println(len(x)) // 4
	fmt.Println(cap(x)) // 4

	for i, v := range x {
		fmt.Println(i, v)
	}
}

 

여기서 왜 용량이 필요한 것인지에 의문이 든다. 여기에는 slice의 동작 방식에 근거한 이유가 있다.

 

slice는 길이의 변경에 대비하여 미리 cap 만큼의 용량을 가진 배열을 할당해두고, 정해진 길이만큼만 사용할 수 있게하여 요소가 추가되어도 재할당할 필요 없이 유동적으로 활용하기 위해서 용량이 존재한다. 

 

여기서 길이와 용량은 다른 것입니다.

 

  • 길이 : 초기화된 슬라이스의 요소 개수 즉, 슬라이스에 5개의 값이 초기화된다면 길이는 5가 됩니다. 그 후에 값을 추가하거나 삭제한다면 그만큼 길이가 바뀌게 됩니다. "len(컬렉션이름)"으로 길이를 알 수 있습니다.
  • 용량 : 슬라이스는 배열의 길이가 동적으로 늘어날 수 있기 때문에 길이와 용량을 구분합니다. 앞서 언급하였듯 slice는 길이의 변경에 대비하여 미리 cap 만큼의 용량을 가진 배열을 할당해두고, 정해진 길이만큼만 사용할 수 있게하여 요소가 추가되어도 재할당할 필요 없이 유동적으로 활용하기 위해서 용량이 존재합니다.

(어떤 분의 비유에 따르면 다음과 같습니다. 헷갈리면 무시하면 됩니다.)

  • 예를 들어, 동호회에서 야유회를 가기위해 버스를 대절한다고 생각해봅시다. 야유회를 가기 위해 모인 인원은 125명이고 버스는 25인승입니다. 125명은 배정이 완료 되어서 버스를 5대를 대절했는데, 16명이 추가로 가고싶다고 합니다. 그래서 추가로 25인승짜리 버스 한 대를 대절했습니다. 여기서 총 승객 136명은 "길이"입니다. 그리고 버스가 한번에 태울 수 있는 승객은 "용량"입니다. 다시 Go언어로 돌아와서 make() 함수를 이용해 슬라이스를 선언한다고 생각해봅시다. 선언한 슬라이스의 용량이 25인데 101개의 값을 초기화하기 위해서는 125의 용량이 필요하게됩니다. 이러한 방식으로 메모리를 관리하는 것입니다. 용량은 "cap(컬렉션이름)"으로 용량을 알 수 있습니다.

 

그런데 이런 cap이 왜 중요한 걸까요? 다음 코드를 봅시다.

func main() {
	slice1 := []int{1, 2, 3} // len 3, cap 3
	slice2 := append(slice1, 4, 5) // 4, 5를 추가할 cap이 부족. 기존 cap의 2배인 새로운 배열을 만듦.

	fmt.Println(slice1, len(slice1), cap(slice1)) // [1 2 3] 3 3
	fmt.Println(slice2, len(slice2), cap(slice2)) // [1 2 3 4 5] 5 6
	
	slice1[1] = 100;
	fmt.Println(slice1) // [1 100 3] 예상대로 바뀜
	fmt.Println(slice2) // [1 2 3 4 5] 안 바뀜. cap이 부족해서 새 배열을 만들었기 때문.
}

 

분명 slice2는 slice1의 포인터 값을 같이 가지고 있어 slice1의 원소를 바꾼 영향이 똑같이 적용될 것이라고 생각했지만, 그렇지 않습니다. cap이 초과하게 되어 새로운 배열을 만들었기 때문입니다.

 

 

2. slice는 reference type이다.

 

슬라이스는 reference type입니다! (struct와 array는 value type이다) 따라서 복사하면 값복사가 아닌 참조 복사를 합니다.

즉, 슬라이스를 정의한 포인터, len, cap은 복사(24바이트 소요)되지만, 포인터가 가리키는 실제 배열은 복사하지 않는다는 거죠.

js를 해 온 저의 경우에는 이게 더 직관적이라고 느끼네요.

package main

import "fmt"

// 배열을 값으로 넘김. (Go에서는 모든 값의 대입은 call by value임)
func changeArray(arr [3]int) {
	// 함수에 인자로 들어간 arr를 함수 스코프 내의 지역변수로 생성. 애초에 원본과 메모리 주소 자체가 다름.
	arr[0] = 100
	// 여기서 함수가 끝나므로 지역 변수는 사라짐
}

// 슬라이드스르 값으로 넘김.
func changeSlice(slice []int) {
	// 이 slice는 참조값이므로 원본 슬라이스를 mutate함.
	slice[0] = 100
}

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

	fmt.Println(arr) // [1 2 3]. 역시 원본이 안 바뀐 것을 확인할 수 있음.
	fmt.Println(slice) // [100 2 3]. 참조값이라 원본이 바뀜.
}

 

 

 

3. slice에 요소 추가하기 (append)

 

초기화 이후 값을 추가하는 방법이 독특하다. 왜 이런 방법이 사용되는고 생각을 해보면, 다음 이유와 같습니다.

func main() {
	names := []string{"foo", "bar", "baz"}
	names[0] = "mama"              // 기존에 존재하는 element 변경
	names = append(names, "robaz") // names.append와 같은 형태가 아님.
	fmt.Println(names)
}

 

아래와 같이 직접 slice에 값을 넣으려고 하면 error가 발생합니다.

이 형태로 기존 element 변경은 가능하지만 새로이 추가하는 push는 불가능하다는 거죠. append 메서드를 사용한 후 기존 슬라이스에 덮어쓰는 방식으로만 가능합니다.

 

이는 생각해보면 당연합니다.

기존에 존재하는 원소들은 메모리 상 어디에 존재하는지 주소를 알기 때문에 변경할 수 있는 반면, 존재하지 않는 인덱스는 할당될 메모리 주소도 없기 때문에 무언가 값을 넣는 것이 불가능하기 때문입니다.

 

func main() {
	names := []string{"foo", "bar", "baz"}
	names[3] = "robaz" // error.
	fmt.Println(names)
}


append에서 주의할 점은 앞에서도 살펴봤지만,

append를 하는 과정에서 cap이 초과하게 되면 새로운 배열을 만들게 된다는 것입니다.

 

 

 

4. Slicing 슬라이싱과 유용한 Slice Tricks

 

python 리스트 슬라이싱하듯 하면 됩니다. 배열에서 슬라이싱할 수도 있고, 슬라이스에서 슬라이싱할 수도 있습니다.

다만, Go에서는 cap을 신경써야 합니다. 슬라이싱한 경우의 cap = 배열의 길이 - 시작인덱스

 

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := slice1[1:2] 
	slice3 := slice1[2:len(slice1)] // 2번부터 끝까지
	slice4 := slice1[:] // 전부

	// 슬라이싱한 경우의 cap = 배열의 길이 - 시작인덱스
	fmt.Println(slice2, len(slice2), cap(slice2)) // [2] 1 4 
	fmt.Println(slice3, len(slice3), cap(slice3)) // [3 4 5] 3 3
	fmt.Println(slice4, len(slice4), cap(slice4)) // [1 2 3 4 5] 5 5
}

 

Slice Tricks

 

슬라이싱 자체는 특별할게 없습니다. 슬라이싱을 활용해서 슬라이스를 CRUD하는 방법이 더 중요합니다.

append, copy, ... 를 잘 활용하는 것이 관건입니다. 외우지 말고, 머리를 쓰면서 사용하다보면 자연스럽게 체득되는 듯합니다...

 

 

(1) 슬라이스 복제 

 

append를 이용하는 방법

개인적으로, 가장 간단한 방법인듯.

func main() {
	slice1 := []int{1, 2, 3}
	slice2 := append([]int{}, slice1...)

	fmt.Println(slice2)
}

 

copy 내장 함수를 이용하는 방법. 
복사하고자 하는 slice의 길이 만큼의 슬라이스를 생성한 후 copy하는 방법입니다.

func main() {
	slice1 := []int{1, 2, 3}
	slice2 := make([]int, len(slice1))
	copy(slice2, slice1) // slice2에 slice1을 복붙

	fmt.Println(slice2)
}

 

(2) 요소 삭제

 

slice는 remove 같은 간단한 메서드가 없습니다

삭제하고자 하는 요소 뒤의 요소들을 앞으로 하나씩 옮겨준 후, 마지막 값을 삭제해줘야 합니다. 그런데 이거 for문으로 구현하면 상당히 귀찮습니다. 아래와 같이 구현하면 간단하게 요소를 삭제한 슬라이드스를 받아볼 수 있습니다.

func main() {
	slice := []int{1, 2, 3, 4, 5}
	idx := 1;
	removedSlice := append(slice[:idx], slice[idx + 1:]...)
	fmt.Println(removedSlice) // [1 3 4 5]
}

 

만약 하나가 아닌 여러 값을 지우고 싶다면요? 똑같은 방법이지만 슬라이싱의 구역을 아래와 같이 해주면 됩니다.

func main() {
	slice := []int{1, 2, 3, 4, 5}
    
    // 1, 2 번째 인덱스를 삭제하자
	removedSlice := append(slice[:1], slice[3:]...) 
	fmt.Println(removedSlice) // [1 4 5]
}

 

(3) 요소 추가 (슬라이스에 슬라이스 추가)

 

중간에 요소를 추가하기 위한 방법입니다. 

우선 넣고 싶은 기준으로 슬라이스를 2개로 쪼개고, 사이에 슬라이스를 넣어서 append하는 방식으로 사용하면 됩니다. 

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

	// 1번 인덱스에 {100, 200}을 추가하고 싶음
	appendedSlice := append(slice[:1], append([]int{100, 200}, slice[1:]...)...)
	fmt.Println(appendedSlice) // [1 100 200 2 3 4 5]
}

 

(4) 정렬

 

stdlib의 sort 패키지를 사용해야 합니다.

int형 슬라이스의 경우 sort.Ints

float형 슬라이스는 sort.Float64s

string형 슬라이스는 sort.Strings

func main() {
	slice := []int{5, 2, 3, 8, 1}
	floatSlice := []float64{3.14, 6.23, 8.76, 5.29}
    s := []string{"banana", "dean", "cynacle", "apple"}

	sort.Ints(slice)
	sort.Float64s(floatSlice)
    sort.Strings(s)

	fmt.Println(slice) // [1 2 3 5 8]
	fmt.Println(floatSlice) // [3.14 5.29 6.23 8.76]
    fmt.Println(s) // [apple banana cynacle dean]
}

 

만약 구조체 슬라이스라면 어떻게 하죠?

package main

import (
	"fmt"
	"sort"
)

type Student struct {
	Name string
	Age int
}

// sort.Interface 메서드를 포함하고 있기 때문에 sort.Sort() 인수로 사용 가능
type ByAge []Student

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

func main() {
	s := []Student {
		{Name: "darren", Age: 100}, {Name: "fauler", Age: 37}, {Name: "psy", Age: 30},
	}

	// 내림 차순 정렬. 올림 차순을 사용하려면 sort.Sort(sort.Reverse(ByAge(s))) 사용할 것
	sort.Sort(ByAge(s))
	fmt.Println(s)
}

 

ref)

https://jacking75.github.io/go_slice_tricks/

jacking75.github.io/go_slice/


darren, dev blog
블로그 이미지 DarrenKwonDev 님의 블로그
VISITOR 오늘 / 전체