슬라이스
슬라이스란 Go언어에서 제공하는 동적배열이다. 동적배열이란 자동으로 배열 크기를 증가시키는 자료구조이다. 슬라이싱 기능을 이용해 배열의 일부를 나타내는 슬라이스를 만들 수 있다.
슬라이스 선언
var (변수명) [](자료형)
배열과 비슷하지만, 배열과 달리 배열 요소 최대 개수를 적지 않는다. 슬라이스를 초기화하지 않으면 길이가 0인 슬라이스가 만들어진다. 슬라이스 길이를 초과해 접근하면 런타임 에러가 발생한다. 슬라이스에 대한 인덱싱은 배열과 동일하다.
package main
import "fmt"
func main() {
var slice []int
fmt.Println(slice[1])
}
/*
/private/var/folders/bv/hfxy36vd6_5f_4959zpp0lp40000gn/T/GoLand/___go_build_test_go
panic: runtime error: index out of range [1] with length 0
goroutine 1 [running]:
main.main()
/Users/hoplin/GolandProjects/awesomeProject2/test.go:7 +0x28
*/
슬라이스 초기화
1. {}를 이용한 초기화
package main
func main() {
var slice = []int{1, 2, 3}
slice2 := [1,2,3,4,5]
}
2. make()를 이용한 초기화
make() 내장함수 매개변수 두개를 이용해 슬라이스를 초기화할 수 있다.
- param1 : 만들고자 하는 타입
- param2 : 해당 타입의 길이
예를들어 아래와 같이 사용하면 길이가 3인 int형 슬라이스를 만드는 것이다
var slice = make([]int, 3)
package main
import "fmt"
func main() {
slice := make([]int, 5)
fmt.Println(len(slice))
fmt.Println(cap(slice))
}
슬라이스 순회
슬라이스의 인덱싱은 배열에서의 방법과 동일하다. len()내장함수를 이용해서 인덱싱을 해줄 수 도 있고, range 키워드를 사용해서 순회를할 수 도 있다. range는 index,value를 순차적으로 반환한다.
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
for i := 0; i < len(slice); i++ {
fmt.Printf("%v\t", slice[i])
}
fmt.Println()
for i, _ := range slice {
fmt.Printf("%v\t", slice[i])
}
}
append()
append()를 이용해서 요소를 추가해줄 수 있다.append()첫번째 인수로 추가하고자 하는 슬라이스를 적고, 그 뒤에 요소를 적어주면 슬라이스 맨 뒤에 요소를 추가해 만든 새로운 슬라이스를 반환한다. 뒤에 요소를 적어줄때 꼭 하나만 적는게 아닌, 여러개를 적어도 된다.(가변 인자)
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice = append(slice, 4)
slice = append(slice, 5, 6, 7, 8, 9, 10)
fmt.Println(slice)
//[1 2 3 4 5 4 5 6 7 8 9 10]
}
슬라이스 동작원리
슬라이스는 구조체이다. 슬라이스의 구조체 형태를 보면 아래와 같이 되어있다.
type Slice struct {
Data uintptr
Len int
Cap int
}
https://pkg.go.dev/reflect#SliceHeader
각 필드의 의미는 아래와 같다.
- Data : 실제 배열의 가리키는 포인터
- Len : 요소의 개수
- Cap : 실제 배열의 개수
위에서 make()함수를 이용해서 슬라이스를 초기화하는것을 보았다. 그때 넣었던 인자가 만들고자하는 타입과 길이를 넣었다.
make([]int,3)
과 같이 생성을 하게되면, len : 3, cap : 3인 슬라이스가 만들어 진다. 만약 len : 3, cap : 5인 슬라이스를 만들고 싶은 경우에는
make([]int,3,5)
와 같이 초기화해주면 된다. 이런 경우에는 슬라이스만 만들고, 인수를 초기화하지 않았다. 이런 경우에는 기본 int의 기본값인 '0'이 3개가 들고, 최대 길이가 5인 슬라이스가 만들어지게 되는것이다.
GO언어의 모든 값의 대입은 복사로 일어난다. 예를 들어 아래와 같은 코드가 있다고 가정하자.
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice2 := slice
fmt.Printf("Slice address : %p Len : %v, Cap : %v\n", &slice[0], len(slice), cap(slice))
fmt.Printf("Slice2 address : %p Len : %v, Cap : %v", &slice2[0], len(slice2), cap(slice2))
}
/*
Slice address : 0x14000122060 Len : 5, Cap : 5
Slice2 address : 0x14000122060 Len : 5, Cap : 5
*/
slice2는 slice를 대입한 값이다. Slice구조체 필드를 보면 Data,Len,Cap이 있고, 당연히 이 값들도 모두 slice의 값이 slice2로 복사된다. 다만 주의할 점은 실제 배열을 가리키는 Data필드는 uintptr, 즉 포인터 주소값이므로, 결론적으로 slice, slice2는 동일한 배열을 가리키게 되는것이다.
append로 인해 발생할 수 있는 문제점 1
append함수가 동작되는 원리는, 우선 슬라이스에 값을 추가할 수 있는 공간이 있는지 검사한다. 이 검사는 'cap - len'값으로 검사한다. 만약 넣을 공간이 있다면, 남은 공간에 인자를 넣게된다. 예를 들어 아래 코드가 있다고 가정하자.
package main
import "fmt"
func main() {
slice := make([]int, 3, 5)
for i := 0; i < len(slice); i++ {
slice[i] = i + 1
}
slice2 := append(slice, 4, 5)
slice2[1] = 100 // slice2의 1번째 인덱스만 바꾸고자 한다
fmt.Println(slice)
fmt.Println(slice2)
}
/*
[1 100 3]
[1 100 3 4 5]
*/
slice는 len이 3 cap이 5인 슬라이스이고, slice2는 slice에 4,5를 넣은 슬라이스이다. 결론적으로 slice2는 slice가 가리키는 배열에 값을 넣어 반환받은 슬라이스이므로 slice, slice2는 모두 동일한 배열을 가리키게된다. 그렇기 때문에 위에 배열을 보면 slice2의 1번째 인덱스를 바꾸고자했지만, 정작 바꾼 값을 보면, 동일한 배열을 가리키는 slice의 값까지 바뀌어버리게 된다.
append로 인해 발생할 수 있는 문제 2
슬라이스는 빈공간이 충분하지 않으면(cap - len이 넣고자 하는 값의 개수보다 적은 경우) 더 큰 배열을 마련하고, 이는 기존 배열에 비해 2배 크기로 확장한다. 그 후 기존 배열 요소를 새로운 배열로 모두 복사한다. 아래 코드를 보자
package main
import "fmt"
func main() {
slice := make([]int, 4, 5)
for i := 0; i < len(slice); i++ {
slice[i] = i + 1
}
slice2 := append(slice, 5)
fmt.Printf("Slice address : %p Slice1 : %v\n", &slice[0], slice)
fmt.Printf("Slice2 address : %p Slice2 : %v", &slice2[0], slice2)
fmt.Println()
fmt.Println()
slice = append(slice, 10, 20)
fmt.Printf("Slice address : %p Slice1 : %v\n", &slice[0], slice)
fmt.Printf("Slice2 address : %p Slice2 : %v", &slice2[0], slice2)
}
/*
Slice address : 0x140000161b0 Slice1 : [1 2 3 4]
Slice2 address : 0x140000161b0 Slice2 : [1 2 3 4 5]
Slice address : 0x140000200a0 Slice1 : [1 2 3 4 10 20]
Slice2 address : 0x140000161b0 Slice2 : [1 2 3 4 5]
*/
slice는 len이 4, cap이 5인 슬라이스이고, slice2는 slice에 5를 추가로 넣어 len,cap모두 5인 슬라이스이다. slice2를 만드는 과정에서 slice에 5를 넣었으므로 slice또한 len,cap모두 5인 슬라이스가 된다. 그리고 slice, slice2모두 가리키고 있는 주소도 동일하다. 만약 여기서 slice에 값을 하나 더 넣는다고 가정하면, 배열의 길이를 늘려야 하므로 slice의 Data에는 기존 데이터를 복사한 새로운 배열의 주소가 대입되게 된다.
그렇기 때문에 마지막의 출력문 두개의 결과를 보면, 서로 가리키고 있는 주소값이 다른것을 볼 수 있다.
슬라이싱
슬라이싱은 배열 일부를 집어내는 기능이다. 사용법은 아래와 같다.
array[startindex : endindex]
만약 시작인덱스, 끝인덱스를 생략하고 ':'만 쓴다면 전체 슬라이싱이 된다
array[:] // 전체슬라이싱
조금 더 응용하면 시작인덱스만 생략하면 처음부터 끝인덱스 -1 까지를 의미한다.
array[:endindex] // index 0 ~ endindex - 1
반대로 끝인덱스를 생략하고 시작인덱스를 쓴다면 시작 인덱스부터 끝까지를 의미한다
array[startindex:] // startindex ~ end
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice2 := slice[1:3]
slice3 := slice[3:]
slice4 := slice[:4]
fmt.Println(slice)
fmt.Println(slice2)
fmt.Println(slice3)
fmt.Println(slice4)
}
슬라이싱에서 일어날 수 있는 문제
슬라이싱 범위는 (startindex) ~ (endindex - 1)까지의 슬라이스를 반환한다. 슬라이스를 반환하기 때문에, 슬라이싱 하는 슬라이스의 메모리 주소를 가리키게 된다. 아래 코드를 보면서 이해해 보자.
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice2 := slice[1:3]
fmt.Printf("slice's index 1 address : %p\n", &slice[1])
fmt.Printf("slice2's index 0 address : %p\n", &slice2[0])
slice2[0] = 100
fmt.Println(slice)
fmt.Println(slice2)
}
/*
slice's index 1 address : 0x140000a8038
slice2's index 0 address : 0x140000a8038
[1 100 3 4 5]
[100 3]
*/
slice2는 slice의 index1부터 index2까지 슬라이싱 한 값이다. 슬라이싱은 슬라이싱한 범위의 배열 메모리주소를 그대로 반환하기 때문에 slice2 0번째 index의 주소값과 slice 1번째 index 주소값이 동일한것을 알 수 있다. 이러한 원리로 인해 슬라이싱한 슬라이스 slice2의 값을 바꾸면 원래의 슬라이스인 slice까지 값이 바뀌는것을 알 수 있다.
슬라이스 복제
위의 문제점을 고치는 방법은 슬라이스를 완전히 복제하는것이다('복사' x) 일반적인 슬라이스 순회로 복제하는 방법과, append를 사용해 복제하는 방법이 있다.
package main
import "fmt"
func main() {
slice := []int{1, 2, 3, 4, 5}
slice2 := make([]int, len(slice)) // slice길이만큼의 슬라이스 생성 후 slice의 값을 그대로 넣는경우
for i, v := range slice {
slice[i] = v
}
slice3 := append([]int{}, slice...) // append를 이용해서 복제하는 경우
slice4 := make([]int, len(slice))
copy(slice4, slice)
fmt.Printf("Slice's address : %p\n", &slice[0])
fmt.Printf("Slice2's address : %p\n", &slice2[0])
fmt.Printf("Slice3's address : %p\n", &slice3[0])
fmt.Printf("Slice4's address : %p", &slice4[0])
}
/*
Slice's address : 0x140000161b0
Slice2's address : 0x140000161e0
Slice3's address : 0x14000016210
Slice4's address : 0x14000016240
*/
append를 이용해서 복제하는 경우에서, slice 뒤에 ...을 붙였는데 이는 모든 요솟값을 의미한다. 해석해 보면 빈 int형 슬라이스에 slice에 있는 모든 요소값을 넣는다는 의미이다.
또다른 방법은 'copy'를 사용하는 방법이다. copy는 아래와 같은 형태이다
func copy(dst, src []Type) int
첫번째 인수로는 복사한 결과를 저장하는 슬라이스 변수를 넣고, 두번째 인수로는 복사대상이 되는 슬라이스 변수를 넣는다. 반환값은 복사된 요소이며, 실제 복사되는 요소 개수는 두 슬라이스 중 길이가 더 작은 개수만큼 복사된다.
append를 이용해서 중간에 있는 요소 삭제하기
슬라이스 중간 요소를 삭제하는 방법을 알아보자. 중간 요소를 삭제하기 위해서는 삭제후, 중간 요소 이후의 값을 앞당겨 삭제된 요소를 채운다. append를 이용해서 구현하면 아래와 같이 구현할 수 있다.
package main
import "fmt"
func deleteElement(index int, slice []int) []int {
return append(slice[:index], slice[index+1:]...)
}
func main() {
slice := []int{1, 2, 3, 4, 5}
slice = deleteElement(2, slice)
fmt.Println(slice)
}
append, copy를 이용한 중간에 요소 추가하기
이번에는 요소를 추가해 보자. 요소를 추가하려면 슬라이스 맨 뒤에 공간을 추가해 주고, 맨 뒷값부터 삽입하려는 위치까지 밀어준 뒤 삽입하는 위치의 값을 바꿔주면 된다.
package main
import "fmt"
func insertElementAppend(index int, value int, slice []int) []int {
return append(slice[:index], append([]int{value}, slice[index:]...)...)
}
func insertElementCopy(index int, value int, slice []int) []int {
slice = append(slice, 0)
copy(slice[index+1:], slice[index:])
slice[index] = value
return slice
// Data필드는 포인터값이므로 바뀔지언정, len, cap필드는 int형타입이므로 값이 복사되는 형태이다. 그렇기 때문에 , main함수와 이 함수가 가지고 있는 슬라이스는 서로 다른값을 가지고있다.
// 이를 방지하기위해 반환값을 설정해 두었다
}
func main() {
slice := []int{1, 2, 3, 4, 5}
slice = insertElementAppend(1, 10, slice)
slice = insertElementCopy(2, 20, slice)
fmt.Println(slice)
}
copy를 사용하는방법, append를 사용하는 방법이 모두 있다. 이중 Copy를 사용하는것이 좋은 이유는 append를 사용하면 중첩 append로 인해, 불필요하게 슬라이스가 생성되어 메모리가 사용됐기 때문이다.
'Language > GO' 카테고리의 다른 글
[GO] 구조체와 메소드 (0) | 2022.02.18 |
---|---|
[GO] 배열 (0) | 2022.02.16 |
[GO] 함수 (0) | 2022.02.16 |
[GO] fmt패키지를 이용한 텍스트 입출력하기 (0) | 2022.02.16 |
[Go] 패키지 (0) | 2022.02.16 |