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

Eon Kim·2022년 4월 18일
0

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과 비슷하다?

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


📍 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
}
...

SliceHeaderData, Len, Cap 필드를 가지고 있습니다.
StringHeaderDataLen 필드를 가지고 있습니다.


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 슬라이스로 복사합니다. stringbyte 값을 []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를 빈 값으로 선언 및 초기화를 먼저 진행합니다.
그리고 빈 슬라이스인 slice2slice1의 요소를 순회하며 대입하는 방식으로, 값은 같으나 서로 다른 인스턴스를 가리키게 합니다.
(서로 같은 인스턴스를 가리키는지 확인하려면 위의 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) intdst의 크기 내에서 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개의 댓글