Golang 기초 (13) : 슬라이스에 대하여

Eon Kim·2022년 4월 18일

Golang 기초 

목록 보기
13/14
post-thumbnail

안녕하세요, 주니어 개발자 Eon입니다.
이번 포스트는 슬라이스에 관한 내용입니다.


📝 슬라이스

📌 슬라이스란?

Golang의 동적 배열입니다.
배열과 다르게 배열 길이의 확장과 축소가 가능합니다.


📌 슬라이스 변수의 선언

📍 일반적 선언

var intSlice1 []int
var intSlice2 []int = []int{1, 2, 3, 4, 5}
var intSlice3 = []int{6, 7, 8, 9, 10}
intSlice4 := []int{}
intSlice5 := []int{11, 12, 13, 14, 15}

슬라이스 변수의 요소 타입을 int로 선언하려면 위와 같이 []int로 타입을 지정하면 됩니다.


📍 슬라이스 안의 슬라이스

var slSlice1 [][]int
var slSlice2 [][]int = [][]int{{1, 2, 3, 4, 5}, {6, 7, 8}}
var slSlice3 = [][]int{{6, 7, 8}, {1, 2, 3, 4, 5}}
slSlice4 := [][]int{}
slSlice5 := [][]int{{}} // [][]int{{}, {}} // [][]int{{}, {}, {}}
slSlice6 := [][]int{{6, 7, 8}, {1, 2, 3, 4, 5}}

위와 같이 슬라이스를 이중으로 사용할 수도 있습니다.
이 경우, 동적 이차원 배열이 됩니다.


📍 슬라이스 인덱스 별 지정하여 선언

var intSlice1 []int = []int{0: 1, 3: 4, 5, 6}
fmt.Println(intSlice1)
/* output
[1 0 0 4 5 6]
*/

위와 같이 인덱스를 지정하여 선언할 수 있습니다.
초기화되지 않은 인덱스는 zero-value로 초기화됩니다.

var intSlice1 []int = []int{3: 4, 7, 8, 9, 0: 1, 5, 6}
fmt.Println(intSlice1)
/* output
[1 5 6 4 7 8 9]
*/

이렇게 인덱스 순서를 다르게 하면 마지막에 지정한 인덱스 뒤에, 인덱스를 지정하지 않은 값들을 순서대로 초기화됩니다.
위의 상황에서 5, 6으로 초기화 시킨 값 뒤에 인덱스 하나를 더 추가할 경우, 0번 인덱스 초기화 뒤에 이미 2개의 값이 들어갔고 그 뒤 3번 인덱스로 인식이 되어 duplicated index 에러가 발생합니다.



📝 슬라이스의 구성

📌 slice는 string과 비슷하다?

slice는 string과 비슷합니다.
단순 값 복사를 하면 헤더가 복사돼, 서로 다른 변수라도 같은 slice를 가리키게 됩니다.
string을 복사할 때, StringHeader가 복사된 것처럼slice도 SliceHeader가 복사되기 때문입니다.


📍 SliceHeader

// SliceHeader is the runtime representation of a slice.
// It cannot be used safely or portably and its representation may
// change in a later release.
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}

...
type StringHeader struct {
	Data uintptr
	Len  int
}
...

SliceHeader는 Data, Len, Cap 필드를 가지고 있습니다.
StringHeader는 Data와 Len 필드를 가지고 있습니다.


Data field

실제 데이터의 위치를 가리킵니다.

Len field (Length)

데이터의 길이를 나타냅니다.

Cap field (Capacity)

데이터를 수용할 수 있는 범위를 나타냅니다.



📝 슬라이스 활용


📍 make()

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//	Slice: The size specifies the length. The capacity of the slice is
//	equal to its length. A second integer argument may be provided to
//	specify a different capacity; it must be no smaller than the
//	length. For example, make([]int, 0, 10) allocates an underlying array
//	of size 10 and returns a slice of length 0 and capacity 10 that is
//	backed by this underlying array.
//	Map: An empty map is allocated with enough space to hold the
//	specified number of elements. The size may be omitted, in which case
//	a small starting size is allocated.
//	Channel: The channel's buffer is initialized with the specified
//	buffer capacity. If zero, or the size is omitted, the channel is
//	unbuffered.
func make(t Type, size ...IntegerType) Type

slice, map, chan의 선언 및 초기화하는 방법 중 하나이며, golang 내장함수입니다.
new는 포인터 타입을 반환하지만, make는 지정한 타입으로 반환합니다.

  • make의 인자 (for slice)
    • 첫 번째 인자는 요소의 타입
    • 두 번째 인자는 Len
    • 세 번째 인자는 Cap
var slice []int = make([]int, 10, 12)
fmt.Println(slice)
/* output
[0 0 0 0 0]
*/

길이가 10이고 최대 길이가 12인 []int 슬라이스를 생성합니다.
생성된 슬라이스는 zero-value로 초기화됩니다.


📍 append()

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//	slice = append(slice, elem1, elem2)
//	slice = append(slice, anotherSlice...)
// As a special case, it is legal to append a string to a byte slice, like this:
//	slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

슬라이스의 끝에 요소를 '덧붙이는' golang 내장함수입니다.
capacity가 여유있는 경우엔 새 요소를 그대로 붙이고, 여유가 없는 경우에는 새로운 슬라이스가 할당됩니다.
append() 내장함수는 새로 업데이트된 슬라이스를 반환합니다.

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

위와 같이 사용 가능합니다. 물론 요소의 자료형은 slice 요소의 자료형과 동일해야 합니다.
(slice... 의 ...은 슬라이스를 덧붙일 때 사용합니다.)

append()의 메모리 할당

func main() {
	slice := []int{1, 2, 3, 4, 5}
	slHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Printf("slHeader.Data : %v\n", slHeader.Data)
	fmt.Printf("slHeader.Len : %v\n", slHeader.Len)
	fmt.Printf("slHeader.Cap : %v\n", slHeader.Cap)
	fmt.Println()
	slice = append(slice, 6, 7, 8)
	slHeader = (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Printf("slHeader.Data : %v\n", slHeader.Data)
	fmt.Printf("slHeader.Len : %v\n", slHeader.Len)
	fmt.Printf("slHeader.Cap : %v\n", slHeader.Cap)
}
/* output
slHeader.Data : 824634273488
slHeader.Len : 5
slHeader.Cap : 5

slHeader.Data : 824634433536
slHeader.Len : 8
slHeader.Cap : 10
*/

append()를 사용했을 때, 기존 슬라이스의 공간이 부족하면 두 배 크기의 공간을 할당해서 완전히 새로운 슬라이스를 만들어서 반환합니다.

...
slice2 := []int{6, 7, 8, 9, 10, 11}
slice = append(slice, slice2...)
...
/* output
slHeader.Data : 824634318848
slHeader.Len : 11
slHeader.Cap : 12
*/

만약 위와 같이 새로 덧붙여지는 요소 / 슬라이스의 크기가 기존의 슬라이스보다 큰 경우, 더 큰 크기의 슬라이스를 기준으로 두 배의 공간을 할당해서 새로운 슬라이스를 만들고 반환합니다.


📍 copy()

// The copy built-in function copies elements from a source slice into a
// destination slice. (As a special case, it also will copy bytes from a
// string to a slice of bytes.) The source and destination may overlap. Copy
// returns the number of elements copied, which will be the minimum of
// len(src) and len(dst).
func copy(dst, src []Type) int

src 슬라이스를 dst 슬라이스로 복사합니다. string의 byte 값을 []byte 자료형으로 하여 복사하기도 합니다.
copy() 내장함수는 복사된 요소의 개수를 반환합니다.


📌 슬라이스의 사용

📍 슬라이스 대입

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := slice1
	fmt.Println(slice2)
}
/* output
[1 2 3 4 5]
*/

위와 같이 슬라이스 대입을 수행하면, 같은 값을 가지는 걸 볼 수 있습니다.

slice1[0] = 100
fmt.Println(slice2)
/* output
[100 2 3 4 5]
*/

이렇게 slice1을 수정하면 slice2까지 영향을 받는 걸 볼 수 있습니다.
슬라이스를 대입 연산을 하면 슬라이스 헤더가 대입되어 두 변수 모두 하나의 인스턴스를 가리키게 됩니다.

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := slice1
	fmt.Println(slice2)
	slice1[0] = 100
	fmt.Println(slice2)
	slHeader1 := (*reflect.SliceHeader)(unsafe.Pointer(&slice1))
	slHeader2 := (*reflect.SliceHeader)(unsafe.Pointer(&slice2))
	fmt.Println()
	fmt.Println("*-*-*-*-* slice1 *-*-*-*-*")
	fmt.Printf("slHeader1.Data : %v\n", slHeader1.Data)
	fmt.Printf("slHeader1.Len : %v\n", slHeader1.Len)
	fmt.Printf("slHeader1.Cap : %v\n", slHeader1.Cap)
	fmt.Println()
	fmt.Println("*-*-*-*-* slice2 *-*-*-*-*")
	fmt.Printf("slHeader2.Data : %v\n", slHeader2.Data)
	fmt.Printf("slHeader2.Len : %v\n", slHeader2.Len)
	fmt.Printf("slHeader2.Cap : %v\n", slHeader2.Cap)
}
/* output
[1 2 3 4 5]
[100 2 3 4 5]

*-*-*-*-* slice1 *-*-*-*-*
slHeader1.Data : 824634376240
slHeader1.Len : 5
slHeader1.Cap : 5

*-*-*-*-* slice2 *-*-*-*-*
slHeader2.Data : 824634376240
slHeader2.Len : 5
slHeader2.Cap : 5
*/

같은 Data 필드를 가진 것을 볼 수 있습니다.


📍 슬라이스 복사

슬라이스 순회를 이용한 복사

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := make([]int, len(slice1))
	for i, v := range slice1 {
		slice2[i] = v
	}
	fmt.Println(slice2)
}
/* output
[1 2 3 4 5]
*/

이 방법은 slice1와 같은 길이를 가지는 slice2를 빈 값으로 선언 및 초기화를 먼저 진행합니다.
그리고 빈 슬라이스인 slice2에 slice1의 요소를 순회하며 대입하는 방식으로, 값은 같으나 서로 다른 인스턴스를 가리키게 합니다.
(서로 같은 인스턴스를 가리키는지 확인하려면 위의 SliceHeader를 출력하는 구문을 사용하면 됩니다.)


append() 내장함수를 이용한 복사

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := append([]int{}, slice1...)
	fmt.Println(slice2)
}
/* output
[1 2 3 4 5]
*/

append() 내장함수를 사용하여 새로운 빈 슬라이스에 기존 슬라이스를 덧붙여, 같은 값을 가지지만 서로 다른 인스턴스인 슬라이스를 생성할 수 있습니다.


copy() 내장함수를 이용한 복사

func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := make([]int, 3)
	copy(slice2, slice1)
	fmt.Println(slice2)
}
/*	output
[1 2 3]
*/

func copy(dst, src []Type) int 의 dst의 크기 내에서 src를 복사합니다.
dst의 범위를 벗어나는 요소는 복사해오지 않습니다.


📍 슬라이스 요소 제거

특정 인덱스를 제거한 새로운 슬라이스

func SubElement(index int, slice []int) (subSlice []int) {
	subSlice = []int{}
	for i, v := range slice {
		if index == i {
			continue
		}
		subSlice = append(subSlice, v)
	}
	return
}
func main() {
	slice1 := []int{1, 2, 3, 4, 5}
	slice2 := SubElement(3, slice1)
	slice2Header := (*reflect.SliceHeader)(unsafe.Pointer(&slice2))
	fmt.Println("slice2 :", slice2)
	fmt.Println("*-*-*-*-* slice2 *-*-*-*-*")
	fmt.Printf("slice2Header.Data : %v\n", slice2Header.Data)
	fmt.Printf("slice2Header.Len : %v\n", slice2Header.Len)
	fmt.Printf("slice2Header.Cap : %v\n", slice2Header.Cap)
}
/*	output
slice2 : [1 2 3 5]
*-*-*-*-* slice2 *-*-*-*-*
slice2Header.Data : 824634425344
slice2Header.Len : 4
slice2Header.Cap : 4
*/

기존 슬라이스를 보존하면서 특정 인덱스를 뺀 슬라이스를 새로 만듭니다.


기존 슬라이스에서 특정 인덱스를 제거

func main() {
	slice := []int{1, 2, 3, 4, 5}
	index := 3
	beforeHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(beforeHeader)
	for i := index + 1; i < len(slice); i++ {
		slice[i-1] = slice[i]
	}
	slice = slice[:len(slice)-1]
	fmt.Println()
	resultHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(resultHeader)
}
/* output
slice : [1 2 3 4 5]
*-*-*-*-* slice *-*-*-*-*
&{824634376240 5 5}

slice : [1 2 3 5]
*-*-*-*-* slice *-*-*-*-*
&{824634376240 4 5}
*/

슬라이스를 처음 만들었을 때의 capacity가 유지되는 것을 볼 수 있습니다.


슬라이싱

func main() {
	slice1 := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	fmt.Println(slice1[:3])
	fmt.Println(slice1[5:])
	fmt.Println(slice1[:])
	fmt.Println(slice1[2:5])
}
/* output
[1 2 3]
[6 7 8 9 10]
[1 2 3 4 5 6 7 8 9 10]
[3 4 5]
*/

slice[포함부터 : 전까지]
슬라이스를 위와 같은 방법으로 절단할 수 있습니다.

슬라이싱으로 특정 인덱스 제거

func main() {
	slice := []int{1, 2, 3, 4, 5}
	index := 3
	beforeHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(beforeHeader)
	slice = append(slice[:index], slice[index+1:]...)
	fmt.Println()
	resultHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(resultHeader)
}
/* output
slice : [1 2 3 4 5]
*-*-*-*-* slice *-*-*-*-*
&{824634384432 5 5}

slice : [1 2 3 5]
*-*-*-*-* slice *-*-*-*-*
&{824634384432 4 5}
*/

슬라이스를 처음 만들었을 때의 capacity가 유지되는 것을 볼 수 있습니다.


copy() 내장함수로 특정 인덱스 제거

func main() {
	slice := []int{1, 2, 3, 4, 5}
	index := 3
	beforeHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(beforeHeader)
	copy(slice[index:], slice[index+1:])
	slice = slice[:len(slice)-1]
	fmt.Println()
	resultHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
	fmt.Println("slice :", slice)
	fmt.Println("*-*-*-*-* slice *-*-*-*-*")
	fmt.Println(resultHeader)
}
/* output
slice : [1 2 3 4 5]
*-*-*-*-* slice *-*-*-*-*
&{824634376240 5 5}

slice : [1 2 3 5]
*-*-*-*-* slice *-*-*-*-*
&{824634376240 4 5}
*/

슬라이스를 처음 만들었을 때의 capacity가 유지되는 것을 볼 수 있습니다.


📍 슬라이스 요소 삽입

슬라이스 요소 밀어내어 삽입

func main() {
	slice := []int{1, 2, 3, 4, 5}
	slice = append(slice, 0)
	index := 3
	const location = 2
	for i := len(slice) - location; i >= index; i-- {
		slice[i+1] = slice[i]
	}
	slice[index] = 100
	fmt.Println(slice)
}
/* output
[1 2 3 100 4 5]
*/

길이를 하나 늘리면, 기존 슬라이스의 마지막 인덱스는 len(바뀐 슬라이스)-2에 위치합니다.
슬라이스의 요소들을 index 기준으로 하나씩 뒤로 미루고, index 위치에 새로운 요소를 삽입합니다.


append() 내장함수와 슬라이싱으로 요소 삽입

func main() {
	slice := []int{1, 2, 3, 4, 5}
	index := 3
	slice = append(slice[:index], append([]int{100}, slice[index:]...)...)
	fmt.Println(slice)
}
/* output
[1 2 3 100 4 5]
*/

(미포함)까지인 슬라이스에, append(추가하려는 요소 + (포함)슬라이스)를 덧붙입니다.
단, append() 내에서 임시로 만든 새 슬라이스에 (포함)슬라이스를 덧붙이는 과정에 임시 버퍼를 사용한다는 단점이 존재합니다.


copy() 내장함수와 슬라이싱으로 요소 삽입

func main() {
	slice := []int{1, 2, 3, 4, 5}
	index := 3
	slice = append(slice, 0)
	copy(slice[index+1:], slice[index:])
	slice[index] = 100
	fmt.Println(slice)
}
/* output
[1 2 3 100 4 5]
*/

zero-value 요소를 추가하고, 특정 인덱스부터의 요소를 한 칸씩 밀어냅니다.
이 과정에서 slice[index]의 값은 유지가 되어, slice[index+1]과 동일한 값을 가집니다.



이번 포스트는 슬라이스에 관한 내용이었습니다.
감사합니다.👍

profile
주니어 개발자

0개의 댓글