golang에서 slice는 특정 배열의 위치를 가르키는 pointer를 가지는 reference 타입의 구조체이다.
그렇기 때문에 대입 연산자 ('=') 를 사용하여 Copy를 하면 값(Elements) 가 Copy가 되는 것이 아니라 reference type의 구조체가 Copy되는 것이다.
즉, 아래 그림과 같이 SliceA를 대입 연산자를 통해 SliceB에 복사한 경우 같은 배열을 가르키는 pointer를 가지는 reference type의 구조체가 만들어져, SliceA에서 배열의 값을 변경하는 경우 SliceB 도 같은 배열을 가르키고 있기 때문에 SliceB 가르키는 값 역시 변경되는 것이다.
이 경우 의도하였다면 큰 문제가 되지 않겠지만, 대부분 이런 경우는 논리적 오류를 일으킬 가능성이 높다.
만약 Slice를 대입 연산자를 이용하여 Copy 하고, Copy된 Slice를 다시 비우는 로직을 이용하는 경우 Copy를 수행한 Slice에 값이 없어 정상적으로 처리를 못하는 경우가 발생하기도 한다.
대입 연산자를 이용한 Slice Copy 예제
func equalCopyExample() {
var src = []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
dst = src
fmt.Println("src:", src)
fmt.Println("dst:", dst)
src[0] = 6
dst[0] = 7
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
src: [1 2 3 4 5]
dst: [1 2 3 4 5]
//dst[0]=7 을 수행하여 src slice 결과 값에도 반영됨
src: [7 2 3 4 5]
dst: [7 2 3 4 5]
이런 논리적 오류를 피하기 위해 slice의 값(value, elements) 를 Copy하고 싶다면 대입 연산자 ('=') 를 사용하는 것이 아니라 golang builtin package에서 제공하는 copy() 함수 등을 사용하는 것이 바람직하다.
본 포스팅에서는 Slice의 Elements를 Copy하는 방법 몇가지를 소개하려고 한다.
#1. copy() 를 이용한 slice copy
먼저 위에서 소개했듯이 golang builtin package에서 제공하는 copy() 를 사용하는 방법이다.
copy() 함수를 사용하는 방법이 오늘 포스팅에서 소개하는 방법 중에서 성능이 가장 좋은 방법이기도 하다.
copy() 는 아래와 같이 dst (복사를 할 곳), src(복사 대상) 두개의 Slice를 인자로 받고, 복사한 Slice의 length를 return 하는 구조로 이루어져 있다.
copy() 구조
func copy(dst, src []Type) int
사용 방법은 아래와 같이 복사 대상 src Slice와 동일한 크기로 dst Slice를 생성해주고, copy함수에 src, dst slices를 인자로 전달하면 된다. Copy 결과를 확인해보면 동일한 값이 두개의 Slice에 각각 들어있는 것을 확인할 수 있다. 또한 각 Slice의 값을 변경하더라도 서로 다른 배열이기 때문에 독립적으로 변경 되는 것을 확인할 수 있다
copy()를 이용한 slice elements 복사 예제:
func copyFuncEaxmple {
var src = []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println("src:", src)
fmt.Println("dst:", dst)
src[0] = 6
dst[0] = 7
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
실행 결과:
// copy 결과
src: [1 2 3 4 5]
dst: [1 2 3 4 5]
// [0] 값 변경 후 출력한 결과
src: [6 2 3 4 5]
dst: [7 2 3 4 5]
#2. append() 를 이용한 slice copy
두 번째 방법은 golang의 builtin package에서 제공하는 append()를 이용하는 방법이다.
append()의 구조는 아래와 같이 복사한 elements를 담을 slice 와 복사하고자 하는 Type의 Elements를 ellipsis (...)로 차례로 넘겨주면 된다.
append() 구조
func append(slice []Type, elems ...Type) []Type
즉, 아래와 같이 인자를 각각 던져주어도 되고, slice 전체를 ellipsis(...)와 함께 전달해주면 된다.
'...' 은 golang에서 variadic parameters라고 표현하는데, 가변길이를 가지는 Type Slice를 표현한다고 이해하시면 편합니다.
자세한 내용은 golang의 spec 문서(link)를 읽어보시길 바랍니다.
append()를 이용한 slice elements 복사 예제:
func appendCopyExample() {
var origin = []int{1, 2, 3, 4, 5}
copy1 := append([]int{}, origin[0], origin[1], origin[2], origin[3], origin[4])\
copy2 := append([]int{}, origin...)
fmt.Println("origin:", origin)
fmt.Println("copy1:", copy1)
fmt.Println("copy2:", copy2)
copy1[0] = 6
copy2[0] = 7
fmt.Println("origin:", origin)
fmt.Println("copy1:", copy1)
fmt.Println("copy2:", copy2)
}
실행 결과:
// copy 결과
origin: [1 2 3 4 5]
copy1: [1 2 3 4 5]
copy2: [1 2 3 4 5]
// [0] 값 변경 후 출력한 결과
origin: [1 2 3 4 5]
copy1: [6 2 3 4 5]
copy2: [7 2 3 4 5]
append()를 사용하는 방법은 copy() 함수를 사용하는 것 보다 성능은 떨어지지만 범용적으로 더 많이 사용된다.
왜 copy()보다 append()를 범용적으로 사용하는 것일까? 그 이유는 복사하기 직전에 복사하고자하는 Slice에 추가된 마지막 값까지 Copy할 수 있기 때문이다.
copy() 함수에 dst로 전달하는 slice는 길이를 지정해주어야하고, dst slice의 길이만큼 복사된다.
때문에 복사 직전에 복사 대상의 slice에 element가 추가되는 경우 마지막에 추가된 만큼 길이를 수정하여 slice를 재정의하지 않으면 마지막에 추가된 element는 복사가 되지 않는다.
즉 copy하기 직전에 mutex를 이용하여 lock을 걸고 slice를 생성하고 copy를 하는 방식이 안전한데, 코드도 늘어나고 매우 귀찮기 때문에 일반적으로 append()를 많이 사용하게 된다.
copy()와 append()의 동작 비교:
func copyVsAppend() {
var src = []int{1, 2, 3, 4, 5}
dst1 := make([]int, len(src))
//element 추가
src = append(src, 6)
copy(dst1, src)
dst2 := append([]int{}, src...)
fmt.Println("src:", src)
fmt.Println("dst1:", dst1)
fmt.Println("dst2:", dst2)
}
실행 결과:
src: [1 2 3 4 5 6]
dst1: [1 2 3 4 5]
dst2: [1 2 3 4 5 6]
#3. range slice 를 이용한 elements 복사
마지막으로 소개하는 방식은 아래와 같이 slice를 range를 이용해 iteration하면서 element 값을 일일히 복사하는 방식이다.
개인적으로는 코드가 길어지고 번거로운 방법으로 잘 사용하지 않는 방식이다.
for range 로 iteration을 수행하며 elements를 copy하는 방식 예제:
func copyItr() {
var src = []int{1, 2, 3, 4, 5}
dst := make([]int, len(origin))
for i, v := range src {
dst[i] = v
}
fmt.Println("src:", src)
fmt.Println("dst:", dst)
dst[0] = 6
fmt.Println("src:", src)
fmt.Println("dst:", dst)
}
실행 결과:
src: [1 2 3 4 5]
dst: [1 2 3 4 5]
src: [1 2 3 4 5]
dst: [6 2 3 4 5]
해당 방식을 사용하기 보다는 copy() 함수나 append()함수를 이용하는 것이 훨씬 편한 것 같다.
참고로 해당 방식은 append()함수 보다는 빠르지만 copy() 방식보다는 느리다.
#4. Slice Copy 방식 별 Benchmark 실행 결과
간단하게 아래와 같이 5개의 integer 값을 가지고 있는 slice를 생성하였고, copy(), append(), for range를 이용하여 slice elements를 copy하는 함수를 구현하였다. 그리고 해당 함수들을 실행시키는 benchmark 코드를 작성하여 각 방법 별 성능을 확인해 보았다.
성능 확인 결과 위에서 각 방식을 설명하면서 언급하였듯이 copy()를 사용하는 것이 가장 성능이 좋았다. 그 다음으로는 for range를 사용하는 방법, 그리고 append()를 사용하는 순으로 성능이 측정되었다.
역시 이번에도 내 손가락이 편하려면 성능은 포기해야한다는 것을 다시한번 알게 되었다.
Benchmark 용 Copy 함수 구현
var origin = []int{1, 2, 3, 4, 5}
func CopyByCopyFunc() []int {
copySlice := make([]int, len(origin))
copy(copySlice, origin)
return copySlice
}
func CopyByAppendFunc() []int {
copySlice := append([]int{}, origin...)
return copySlice
}
func CopyByIterateFunc() []int {
copySlice := make([]int, len(origin))
for i, v := range origin {
copySlice[i] = v
}
return copySlice
}
Benachmark 코드
func BenchmarkCopyByCopyFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
CopyByCopyFunc()
}
}
func BenchmarkCopyByAppendFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
CopyByAppendFunc()
}
}
func BenchmarkCopyByIterateFunc(b *testing.B) {
for i := 0; i < b.N; i++ {
CopyByIterateFunc()
}
}
Benchmark 결과
goos: darwin
goarch: arm64
pkg: github.com/HyunWookKim/golang-slice-copy/example
BenchmarkCopyByCopyFunc
BenchmarkCopyByCopyFunc-10 84110425 13.66 ns/op
BenchmarkCopyByAppendFunc
BenchmarkCopyByAppendFunc-10 63026037 18.48 ns/op
BenchmarkCopyByIterateFunc
BenchmarkCopyByIterateFunc-10 68293216 17.47 ns/op
PASS
ok github.com/takeanoteof/golang-slice-copy/example 3.898s
2023.02.07 - [golang (go)] - golang byte slice (array) compare
2023.12.24 - [golang (go)] - golang map
참고로 위에서 설명한 내용들은 아래 The Go Blog에서 더 자세히 다루고 있으니 꼭 한번씩 읽어보기를 추천한다.
golang slice docs - https://go.dev/blog/slices-intro
'golang (go)' 카테고리의 다른 글
golang sync.Map range (Iteration) (0) | 2024.10.11 |
---|---|
golang map (0) | 2023.12.24 |
golang - const와 iota로 enum(열거)형 구현하기 (1) | 2023.11.18 |
golang byte slice (array) compare (0) | 2023.02.07 |
golang type conversion [string(val)], type assertion [val.(string)] (0) | 2023.02.03 |
댓글