본문 바로가기
Language/Golang

Golang (Go언어) 슬라이스(Slice) 1/2

by HeeWorld 2024. 11. 19.

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

Golang 마스코트 Gopher(고퍼)

 

★슬라이스(Slice)★

- Go에서 제공하는 동적 배열 타입 → Go에서 제공하는 배열을 가리키는 포인터 타입

→ 동적(Dynamic), 프로그램 실행 도중에 결정(실행도중에 바뀔 수 있는 값)

    정적(Static), 코드를 기계어로 바꿀 때 결정(실행도중에 절대 바뀌지 않는 값)

var slice []int

// var <변수명> <타입>

 

- 타입의 [](가운데) 사이즈를 적지 않고 뒤에 요소 타입을 적음(ex. int, str 등)

 

package main

import "fmt"

func main() {
	var slice []int

	if len(slice) == 0 {
		fmt.Println("slice is empty", slice)
	}

	slice[1] = 10
	fmt.Println(slice)
}

// 결과

slice is empty []
panic: runtime error: index out of range [1] with length 0

goroutine 1 [running]:
main.main()
	/tmp/sandbox2626503554/prog.go:12 +0x7b

 

- 실행 도중에 에러가 발생되고, index의 범위를 벗어났다는 에러를 볼 수 있음(두 번째 값에 접근하다 에러)

- 슬라이스의 길이가 0인데(요소가 하나도 없음), slice[1] 두번째 요소 값에 접근하였기 때문에 runtime 에러 발생

 

package main

import "fmt"

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

	if len(slice) == 0 {
		fmt.Println("slice is empty", slice)
	}

	slice[1] = 10
	fmt.Println(slice)
}

// 결과

[1 10 3]

 

- slice에서 int형으로 초기값은 3개의 요소를 가지고 있고, 동적 배열이기 때문에 길이는 줄거나 늘어날 수 있음.

- 길이가 0이 아니고 3이기 때문에 두 번째 값 (slice[1] = 10)이 변경된 것을 확인할 수 있음.

 

 

슬라이스 초기화

- 배열처럼 {}을 사용해 요솟값을 지정하는 방법

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

 

- slice1로 초기화 하면 1, 2, 3을 값으로 갖는 슬라이스가 됨

- slice2는 첫 번째 요소가 1이 되고, 인덱스 5가 2, 인덱스 10이 3이 되어 총 11개의 요소를 갖는 슬라이스가 됨.

   (= [1000200003])

✓ slice1을 초기화 할 때, [...]int{1, 2, 3}으로 하게되면 이는 배열이 되니, 슬라이스를 사용할 땐 []int{1, 2, 3}으로 사용!

 

var slice = make([]int, 3)

 

- Go의 내장 함수인 make()를 사용하여 초기화할 수 있음(Compress 타입을 만들어 주는 함수).

- int 타입 슬라이스를 만드는데 3개의 요소를 가진 슬라이스를 만든다는 의미(기본값 0).

   → slice := []int{0, 0, 0}과 같음

package main

import "fmt"

func main() {
	slice := make([]int, 3)    // make 함수 사용

	if len(slice) == 0 {
		fmt.Println("slice is empty", slice)
	}

	slice[1] = 10
	fmt.Println(slice)
}

// 결과

[0 10 0]

 

 

슬라이스 요소 추가 - append()

- append()도 내장함수이며, slice에 값을 추가할 때 사용

- 슬라이스는 요소를 추가해 길이를 늘릴 수 있음.

package main

import "fmt"

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

	slice2 := append(slice, 4)

	fmt.Println(slice)
	fmt.Println(slice2)
}

// 결과

[1 2 3]
[1 2 3 4]

 

- slice는 요소가 3개인 슬라이스

- slice2는 append()함수를 사용해 slice에 1개의 요소를 추가하여 새로운 슬라이스를 만들어서 반환(요소 4개)

 

✓ append는 슬라이스에 요소를 추가하여 새로운 슬라이스를 반환함(기존의 슬라이스가 바뀌지 않음)

✓ 기존 슬라이스에 추가하고 싶으면, 반환하는 값이 slice2가 아닌 slice = append(slice, 4)를 사용하면 됨

★ (주의) append()는 새로운 슬라이스를 반환 ★

 

 

여러 값 추가

- append()를 사용하여 값을 하나 이상 추가할 수 있음

// 기존 슬라이스
var slice = []int{1, 2, 3}

// 여러 값 추가
slice = append(slice, 3, 4, 5, 6, 7)

// 결과

[1 2 3 3 4 5 6 7]

 

- 첫 번째 인수로 들어온 슬라이스의 값을 변경하는게 아닌, 요소가 추가된 새로운 슬라이스를 반환

 

 

★슬라이스 동작원리★

- python의 슬라이스와 다름.

- 내부를 보면 struct 타입으로 구성되어 있음.

- 슬라이스는 실제 배열에서 현재 내가 사용하고 있는 정도를 잘라낸(슬라이스)한 개념으로 이해

 

→ 1) data 필드는 실제 배열을 나타내는 가르키는 포인터

    2) len 필드는 길이를 나타냄 (내가 현재 몇 개를 사용하고 있는가)

    3) cap 필드는 capacity의 약자로 최대공간 또는 최대길이 (최대 몇 개까지 사용할 수 있는지(=maximum))

type SliceHeader struct {
   Data uintptr   // 실제 배열을 가르키는 포인터
   Len  int       // 요소 개수
   Cap  int       // 실제 배열의 길이
}

슬라이스 동작 구조

 

 

make() 함수를 이용한 선언

- slice는 len이 3이고, cap이 3으로 총 배열 길이가 3, 요소 개수도 3이 됨.

- 각 요소는 기본 값인 0이 됨.

var slice = make([]int, 3)

make 함수 슬라이스 동작 구조

 

✓ 인자를 두 개 기재하여 사용하는 make()

- 첫 번째 숫자가 Len, 두 번째 숫자가 Cap이 됨.

- 5개짜리 배열을 만들어 3개만 사용하고, 나머지 2개는 나중에 추가될 요소를 위해 비워둔 것으로 이해!

var slice2 = make([]int, 3, 5)

make 함수 슬라이스 동작 구조2

 

package main

import "fmt"

func changeArray(array2 [5]int) {
	array2[2] = 200
}

func changeSlice(slice2 []int) {
	slice2[2] = 200
}

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

	changeArray(array)
	changeSlice(slice)

	fmt.Println("array:", array)
	fmt.Println("slice:", slice)
}

// 결과

array: [1 2 3 4 5]
slice: [1 2 200 4 5]

 

- Go의 특징 중인 하나는 일관성으로 어떤 상황에서도 똑같이 작동함.

- Go에서는 모든 값의 대입은 복사로 일어남.

 

✓ 동작 차이의 원인

* Array의 경우

- 함수를 호출하면, 좌측은 r-value로 값으로 동작하며 array와 array2는 다른 변수/인스턴스임

- (array2 [5]int)는 대입연산자를 사용한 것과 같음 (array2 = array)

   = array2는 함수 인자로 들어간 array의 대입연사자를 사용하여 만든 값과 같음

   = array와 array2는 [5]int 로 동일한 타입이라 사용할 수 있음

- array와 array2는 40bit(8bit *5)로 별도의 인스턴스(메모리 공간)를 가지고 있음

- array에 있는 메모리 공간에 있는 값을 array2에 그대로 복사하는 것 = 서로 다른 메모리 공간

 

따라서, array2의 값을 바꾸면 array2의 인스턴스 3번째의 값이 200으로 바뀌고, array는 그대로 있는 것!

array 동작

 

* Slice의 경우

- slice의 내부는 포인터, len, cap 세 개의 필드를 갖는 구조체

- 포인터 메모리 주소는 8bit 길이이며, len과 cap은 각각 int 타입으로 8bit라 해당 슬라이스의 크기는 24bit가 됨.

    = 배열의 길이와 관계 없이 항상 구조체 사이즈로 copy가 됨!!

- 두 개가 동일한 배열을 가리키고 있음.

- 가리키고 있는 배열의 세 번째 값을 200으로 바꿨기 때문에 array랑 다른 값이 출력됨.

 

따라서, slice는 slice의 struct 정보가 복사되는 것이고 같은 배열을 가르키고 있기 때문에 변경된 배열의 값이 출력!

slice 동작

 

 

★append() 동작원리★

- append()의 기본적인 정의는 슬라이스에 요소를 추가한 새로운 슬라이스를 반환이지만,

  실제로는 기존 슬라이스가 바뀔 수도 있고, 아닐 수도 있음!

      = 기존 배열에 4를 추가해서 반환할 수도 있다는 것!

 

1. 처음에 들어오면 빈공간이 충분하다면, 빈공간에 요소를 추가하여 반환

2. 빈공간이 충분하지 않다면, 새로운 배열을 할당하여 기존 슬라이스가 가리키고 있는 배열에서 복사 후 요소 추가후 반환

append() 동작원리

 

 

✓ 빈 공간 확인

* 빈 공간은 cap - len을 한 공간

- cap이 5이고, len이 3인 배열에 요소를 추가할 때, if문으로 cap - len >= 요소 개수를 실행

   true이면 기존 배열에 추가하고, false라면 새로운 공간을 할당하여 기존 len 개수만큼 복사하고 나머지에 요소 추가

 

만약, 기존 슬라이스에 요소를 추가하여 slice2를 새로 만들었는데, slice의 두 번째 값을 바꾼다면?

// slice1 값 변경

slice1[1]=100

 

→ 같은 배열을 가르키고 있기 때문에 slice2의 두 번째 값도 100을 리턴

    slice1을 바꿨으나, slice2가 바뀌는 상황이 생김

 

만약, 공간이 없어 새로운 슬라이스를 만들게 되었을 때, slice1의 값을 변경한다면?

→ slice1의 배열은 바뀌지만, slice2의 값은 변경되지 않음(서로 다른 배열이기 때문)

   append가 빈 공간이 있는지 없는지에 따라 배열을 만들거나 만들지 않기 때문에 기존 배열이 바뀔수도 바뀌지 않을 수도 있음