Go 슬라이스 완벽 가이드 | slice 개념·내부 구조·배열 차이·append·copy
들어가며: 슬라이스가 뭐가 다른가요?
”배열이랑 슬라이스가 어떻게 다른가요?”
Go를 처음 배울 때 가장 헷갈리는 개념 중 하나가 슬라이스(slice)입니다. 배열처럼 보이지만 완전히 다른 동작을 하고, 때로는 예상치 못한 결과를 만들어냅니다.
아래 코드는 go를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 배열
arr := [3]int{1, 2, 3}
arr2 := arr // 값 복사
arr2[0] = 100
fmt.Println(arr[0]) // 1 (변경 안 됨)
// 슬라이스
slice := []int{1, 2, 3}
slice2 := slice // 참조 복사
slice2[0] = 100
fmt.Println(slice[0]) // 100 (변경됨!)
이 글을 읽으면:
- 슬라이스의 내부 구조를 이해할 수 있습니다.
- 배열과 슬라이스의 차이를 명확히 알 수 있습니다.
- append, copy의 동작 원리를 이해할 수 있습니다.
- 용량(capacity)과 길이(length)의 차이를 알 수 있습니다.
- 슬라이스 관련 흔한 실수를 피할 수 있습니다.
- 실전에서 슬라이스를 효과적으로 활용할 수 있습니다.
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다.
특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
- 슬라이스 기본 개념
- 슬라이스 내부 구조
- 배열 vs 슬라이스
- 슬라이스 생성 방법
- 길이와 용량
- append 동작 원리
- 슬라이싱 연산
- copy와 메모리 관리
- 완전한 예제
- 자주 발생하는 실수
- 실전 활용 패턴
- 정리
1. 슬라이스 기본 개념
슬라이스란?
슬라이스는 Go의 동적 배열입니다. 크기가 고정되지 않고, 런타임에 크기를 변경할 수 있습니다.
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func main() {
// 슬라이스 선언 및 초기화
var s1 []int // nil 슬라이스
s2 := []int{} // 빈 슬라이스
s3 := []int{1, 2, 3} // 초기값이 있는 슬라이스
s4 := make([]int, 3) // 길이 3인 슬라이스
s5 := make([]int, 3, 5) // 길이 3, 용량 5인 슬라이스
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
fmt.Printf("s2: %v, len=%d, cap=%d\n", s2, len(s2), cap(s2))
fmt.Printf("s3: %v, len=%d, cap=%d\n", s3, len(s3), cap(s3))
fmt.Printf("s4: %v, len=%d, cap=%d\n", s4, len(s4), cap(s4))
fmt.Printf("s5: %v, len=%d, cap=%d\n", s5, len(s5), cap(s5))
}
출력:
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
s1: [], len=0, cap=0
s2: [], len=0, cap=0
s3: [1 2 3], len=3, cap=3
s4: [0 0 0], len=3, cap=3
s5: [0 0 0], len=3, cap=5
2. 슬라이스 내부 구조
슬라이스는 3개의 필드를 가진 구조체
슬라이스는 내부적으로 다음과 같은 구조체입니다:
아래 코드는 go를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
type slice struct {
ptr *[배열] // 실제 데이터를 가리키는 포인터
len int // 현재 길이
cap int // 용량
}
다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart LR
subgraph slice[슬라이스 구조체]
ptr[ptr: 포인터]
len[len: 3]
cap[cap: 5]
end
subgraph array[실제 배열 메모리]
a0[0: 10]
a1[1: 20]
a2[2: 30]
a3[3: ?]
a4[4: ?]
end
ptr -->|가리킴| a0
메모리 레이아웃 예제
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 3, 5)
s[0], s[1], s[2] = 10, 20, 30
// 슬라이스 헤더 크기
fmt.Printf("슬라이스 헤더 크기: %d bytes\n", unsafe.Sizeof(s))
// 슬라이스 정보
fmt.Printf("길이: %d, 용량: %d\n", len(s), cap(s))
fmt.Printf("데이터: %v\n", s)
// 포인터 주소 (내부 배열의 시작 주소)
fmt.Printf("배열 시작 주소: %p\n", &s[0])
}
출력:
다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
슬라이스 헤더 크기: 24 bytes (포인터 8 + len 8 + cap 8)
길이: 3, 용량: 5
데이터: [10 20 30]
배열 시작 주소: 0xc0000b4000
3. 배열 vs 슬라이스
핵심 차이점
| 특징 | 배열 | 슬라이스 |
|---|---|---|
| 크기 | 고정 (타입의 일부) | 가변 |
| 타입 | [N]T | []T |
| 값 복사 | 전체 복사 | 헤더만 복사 (참조) |
| 비교 | == 가능 | == 불가 (nil만 가능) |
| 함수 전달 | 값 복사 | 참조 전달 |
| 메모리 | 스택 또는 힙 | 항상 힙 |
배열 예제
아래 코드는 go를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func modifyArray(arr [3]int) {
arr[0] = 100 // 복사본 수정
}
func main() {
arr := [3]int{1, 2, 3}
modifyArray(arr)
fmt.Println(arr) // [1 2 3] - 변경 안 됨
}
슬라이스 예제
아래 코드는 go를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 100 // 원본 수정
}
func main() {
slice := []int{1, 2, 3}
modifySlice(slice)
fmt.Println(slice) // [100 2 3] - 변경됨!
}
배열을 슬라이스로 변환
아래 코드는 go를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[:] // 배열 전체를 슬라이스로
slice2 := arr[1:4] // [2 3 4]
slice[0] = 100
fmt.Println(arr) // [100 2 3 4 5] - 배열도 변경됨!
4. 슬라이스 생성 방법
방법 1: 리터럴
s := []int{1, 2, 3, 4, 5}
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=5, cap=5
방법 2: make 함수
다음은 간단한 go 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// make([]T, length, capacity)
s1 := make([]int, 5) // len=5, cap=5, [0 0 0 0 0]
s2 := make([]int, 3, 10) // len=3, cap=10, [0 0 0]
s3 := make([]int, 0, 5) // len=0, cap=5, []
방법 3: 슬라이싱
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:4] // [2 3 4], len=3, cap=4 (배열 끝까지)
방법 4: nil 슬라이스
아래 코드는 go를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
var s []int // nil 슬라이스
fmt.Println(s == nil) // true
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
// nil 슬라이스에도 append 가능
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
5. 길이와 용량
길이(Length) vs 용량(Capacity)
- 길이: 슬라이스가 현재 포함하고 있는 요소의 개수
- 용량: 슬라이스가 재할당 없이 담을 수 있는 최대 요소 개수
다음은 go를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
s := make([]int, 3, 5)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // len=3, cap=5
// 길이 범위 내 접근: OK
s[0] = 1
s[1] = 2
s[2] = 3
// 용량 범위 내지만 길이 초과: 패닉!
// s[3] = 4 // panic: index out of range
// append로 추가: OK
s = append(s, 4) // len=4, cap=5
s = append(s, 5) // len=5, cap=5
s = append(s, 6) // len=6, cap=10 (재할당 발생)
용량 증가 규칙
아래 코드는 go를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func main() {
s := make([]int, 0)
for i := 0; i < 20; i++ {
s = append(s, i)
fmt.Printf("len=%d, cap=%d\n", len(s), cap(s))
}
}
출력 (Go 1.18+):
아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
len=1, cap=1
len=2, cap=2
len=3, cap=4
len=4, cap=4
len=5, cap=8
len=6, cap=8
len=7, cap=8
len=8, cap=8
len=9, cap=16
...
규칙:
- 용량이 256 미만: 약 2배씩 증가
- 용량이 256 이상: 약 1.25배씩 증가
6. append 동작 원리
기본 append
아래 코드는 go를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
s := []int{1, 2, 3}
s = append(s, 4) // [1 2 3 4]
s = append(s, 5, 6, 7) // [1 2 3 4 5 6 7]
// 다른 슬라이스 추가
s2 := []int{8, 9}
s = append(s, s2...) // [1 2 3 4 5 6 7 8 9]
append의 내부 동작
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func appendExample() {
s := make([]int, 3, 5)
s[0], s[1], s[2] = 1, 2, 3
fmt.Printf("Before: len=%d, cap=%d, ptr=%p\n",
len(s), cap(s), &s[0])
// 용량 내 추가: 재할당 없음
s = append(s, 4)
fmt.Printf("After 1: len=%d, cap=%d, ptr=%p\n",
len(s), cap(s), &s[0])
s = append(s, 5)
fmt.Printf("After 2: len=%d, cap=%d, ptr=%p\n",
len(s), cap(s), &s[0])
// 용량 초과: 재할당 발생
s = append(s, 6)
fmt.Printf("After 3: len=%d, cap=%d, ptr=%p\n",
len(s), cap(s), &s[0])
}
출력:
다음은 간단한 code 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Before: len=3, cap=5, ptr=0xc0000b4000
After 1: len=4, cap=5, ptr=0xc0000b4000 (같은 주소)
After 2: len=5, cap=5, ptr=0xc0000b4000 (같은 주소)
After 3: len=6, cap=10, ptr=0xc0000b6000 (다른 주소 - 재할당!)
append 시 주의사항
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
s2 := s
// s에만 append
s = append(s, 4)
// s2는 여전히 원래 배열을 가리킴
fmt.Println("s:", s) // [1 2 3 4]
fmt.Println("s2:", s2) // [1 2 3]
// 하지만 원소 수정은 공유됨 (용량 내에서)
s[0] = 100
fmt.Println("s:", s) // [100 2 3 4]
fmt.Println("s2:", s2) // [100 2 3] - 영향받음!
}
7. 슬라이싱 연산
기본 슬라이싱
아래 코드는 go를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5] // [2 3 4]
s2 := s[:5] // [0 1 2 3 4]
s3 := s[5:] // [5 6 7 8 9]
s4 := s[:] // [0 1 2 3 4 5 6 7 8 9] (전체 복사 아님!)
슬라이싱의 용량
아래 코드는 go를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := s[2:5]
fmt.Printf("s1: %v, len=%d, cap=%d\n", s1, len(s1), cap(s1))
// s1: [2 3 4], len=3, cap=8 (원본의 2번 인덱스부터 끝까지)
Full Slice Expression (3-index slice)
아래 코드는 go를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// s[low:high:max]
// low: 시작 인덱스
// high: 끝 인덱스 (포함 안 됨)
// max: 용량 제한
s1 := s[2:5:7] // [2 3 4], len=3, cap=5 (7-2)
fmt.Printf("len=%d, cap=%d\n", len(s1), cap(s1))
// len=3, cap=5
슬라이싱과 메모리 공유
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
s1 := s[1:3] // [2 3]
// s1 수정 → s도 영향받음
s1[0] = 100
fmt.Println("s:", s) // [1 100 3 4 5]
fmt.Println("s1:", s1) // [100 3]
// s 수정 → s1도 영향받음
s[2] = 200
fmt.Println("s:", s) // [1 100 200 4 5]
fmt.Println("s1:", s1) // [100 200]
}
8. copy와 메모리 관리
copy 함수
아래 코드는 go를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// copy(dst, src) int
// 반환값: 복사된 요소 개수 (min(len(dst), len(src)))
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3)
n := copy(dst, src)
fmt.Printf("복사된 개수: %d\n", n) // 3
fmt.Println("dst:", dst) // [1 2 3]
fmt.Println("src:", src) // [1 2 3 4 5] (변경 없음)
// dst 수정해도 src 영향 없음
dst[0] = 100
fmt.Println("src:", src) // [1 2 3 4 5]
슬라이스 복제
아래 코드는 go를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 방법 1: copy 사용
src := []int{1, 2, 3, 4, 5}
dst := make([]int, len(src))
copy(dst, src)
// 방법 2: append 사용
dst2 := append([]int(nil), src...)
// 방법 3: 슬라이싱 + append
dst3 := append(src[:0:0], src...)
메모리 누수 방지
아래 코드는 go를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 나쁜 예: 큰 슬라이스의 일부만 사용
func processData(data []byte) []byte {
// data가 1GB인데 처음 100바이트만 필요
return data[:100] // 1GB 전체가 메모리에 유지됨!
}
// ✅ 좋은 예: 필요한 부분만 복사
func processData(data []byte) []byte {
result := make([]byte, 100)
copy(result, data[:100])
return result // 100바이트만 유지
}
일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.
9. 완전한 예제
예제 1: 동적 배열 구현
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
type DynamicArray struct {
data []int
}
func NewDynamicArray() *DynamicArray {
return &DynamicArray{
data: make([]int, 0, 10),
}
}
func (da *DynamicArray) Append(value int) {
da.data = append(da.data, value)
}
func (da *DynamicArray) Get(index int) (int, bool) {
if index < 0 || index >= len(da.data) {
return 0, false
}
return da.data[index], true
}
func (da *DynamicArray) Size() int {
return len(da.data)
}
func (da *DynamicArray) Capacity() int {
return cap(da.data)
}
func main() {
arr := NewDynamicArray()
for i := 0; i < 15; i++ {
arr.Append(i)
fmt.Printf("Size: %d, Capacity: %d\n",
arr.Size(), arr.Capacity())
}
}
예제 2: 슬라이스 필터링
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func filter(s []int, predicate func(int) bool) []int {
result := make([]int, 0, len(s))
for _, v := range s {
if predicate(v) {
result = append(result, v)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 짝수만 필터링
evens := filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println("짝수:", evens) // [2 4 6 8 10]
// 5보다 큰 수만 필터링
greaterThan5 := filter(numbers, func(n int) bool {
return n > 5
})
fmt.Println("5보다 큰 수:", greaterThan5) // [6 7 8 9 10]
}
예제 3: 슬라이스 역순 정렬
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Println("Before:", s)
reverse(s)
fmt.Println("After:", s) // [5 4 3 2 1]
}
예제 4: 슬라이스에서 요소 제거
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
package main
import "fmt"
// 인덱스로 요소 제거 (순서 유지)
func removeOrdered(s []int, index int) []int {
return append(s[:index], s[index+1:]...)
}
// 인덱스로 요소 제거 (순서 무시, 빠름)
func removeUnordered(s []int, index int) []int {
s[index] = s[len(s)-1]
return s[:len(s)-1]
}
func main() {
s1 := []int{1, 2, 3, 4, 5}
s1 = removeOrdered(s1, 2)
fmt.Println("Ordered:", s1) // [1 2 4 5]
s2 := []int{1, 2, 3, 4, 5}
s2 = removeUnordered(s2, 2)
fmt.Println("Unordered:", s2) // [1 2 5 4]
}
10. 자주 발생하는 실수
실수 1: append 결과를 받지 않음
아래 코드는 go를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 잘못된 예
func wrong() {
s := []int{1, 2, 3}
append(s, 4) // 결과를 받지 않음!
fmt.Println(s) // [1 2 3] - 변경 안 됨
}
// ✅ 올바른 예
func correct() {
s := []int{1, 2, 3}
s = append(s, 4) // 결과를 받아야 함
fmt.Println(s) // [1 2 3 4]
}
실수 2: 슬라이싱 후 원본 수정
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ⚠️ 주의: 메모리 공유
func dangerous() {
s := []int{1, 2, 3, 4, 5}
s1 := s[1:3] // [2 3]
s1[0] = 100
fmt.Println(s) // [1 100 3 4 5] - 원본도 변경됨!
}
// ✅ 안전한 방법: 복사
func safe() {
s := []int{1, 2, 3, 4, 5}
s1 := make([]int, 2)
copy(s1, s[1:3])
s1[0] = 100
fmt.Println(s) // [1 2 3 4 5] - 원본 유지
}
실수 3: 반복문에서 append
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 비효율적
func inefficient() []int {
var result []int
for i := 0; i < 1000; i++ {
result = append(result, i) // 여러 번 재할당
}
return result
}
// ✅ 효율적: 용량 미리 할당
func efficient() []int {
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
result = append(result, i) // 재할당 없음
}
return result
}
실수 4: nil 슬라이스와 빈 슬라이스 혼동
다음은 go를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
var s1 []int // nil 슬라이스
s2 := []int{} // 빈 슬라이스
s3 := make([]int, 0) // 빈 슬라이스
fmt.Println(s1 == nil) // true
fmt.Println(s2 == nil) // false
fmt.Println(s3 == nil) // false
// 하지만 len, cap은 모두 0
fmt.Println(len(s1), cap(s1)) // 0 0
fmt.Println(len(s2), cap(s2)) // 0 0
fmt.Println(len(s3), cap(s3)) // 0 0
// append는 모두 동작
s1 = append(s1, 1)
s2 = append(s2, 1)
s3 = append(s3, 1)
실수 5: 슬라이스를 함수에 전달할 때
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ⚠️ 주의: 요소는 수정되지만 길이/용량은 공유 안 됨
func modify(s []int) {
s[0] = 100 // ✅ 원본 수정됨
s = append(s, 4) // ❌ 원본에 영향 없음
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s) // [100 2 3] - append는 반영 안 됨
}
// ✅ 올바른 방법: 포인터 사용
func modifyCorrect(s *[]int) {
(*s)[0] = 100
*s = append(*s, 4)
}
func main() {
s := []int{1, 2, 3}
modifyCorrect(&s)
fmt.Println(s) // [100 2 3 4]
}
11. 실전 활용 패턴
패턴 1: 슬라이스 사전 할당
아래 코드는 go를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 크기를 알 때
func preAllocate(size int) []int {
return make([]int, 0, size)
}
// 사용 예
result := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
result = append(result, i)
}
패턴 2: 슬라이스 풀링
다음은 go를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import "sync"
var slicePool = sync.Pool{
New: func() interface{} {
return make([]byte, 0, 1024)
},
}
func processData(data []byte) {
buf := slicePool.Get().([]byte)
defer func() {
buf = buf[:0] // 길이를 0으로 리셋
slicePool.Put(buf)
}()
// buf 사용...
}
패턴 3: 슬라이스 중복 제거
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func unique(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
// 사용
numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
fmt.Println(unique(numbers)) // [1 2 3 4 5]
패턴 4: 슬라이스 병합
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func merge(slices ...[]int) []int {
totalLen := 0
for _, s := range slices {
totalLen += len(s)
}
result := make([]int, 0, totalLen)
for _, s := range slices {
result = append(result, s...)
}
return result
}
// 사용
s1 := []int{1, 2, 3}
s2 := []int{4, 5, 6}
s3 := []int{7, 8, 9}
merged := merge(s1, s2, s3)
fmt.Println(merged) // [1 2 3 4 5 6 7 8 9]
패턴 5: 슬라이스 분할
다음은 go를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
func chunk(s []int, size int) [][]int {
var chunks [][]int
for i := 0; i < len(s); i += size {
end := i + size
if end > len(s) {
end = len(s)
}
// 복사본 생성 (메모리 공유 방지)
chunk := make([]int, end-i)
copy(chunk, s[i:end])
chunks = append(chunks, chunk)
}
return chunks
}
// 사용
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
chunks := chunk(numbers, 3)
fmt.Println(chunks) // [[1 2 3] [4 5 6] [7 8 9] [10]]
12. 정리
핵심 요약
| 개념 | 설명 |
|---|---|
| 슬라이스 구조 | 포인터 + 길이 + 용량 (24 bytes) |
| 배열 vs 슬라이스 | 고정 크기 vs 가변 크기, 값 복사 vs 참조 |
| 길이 | 현재 요소 개수 (len) |
| 용량 | 재할당 없이 담을 수 있는 최대 개수 (cap) |
| append | 용량 초과 시 자동 재할당 (약 2배) |
| 슬라이싱 | 원본 배열 공유 (메모리 공유) |
| copy | 독립적인 복사본 생성 |
슬라이스 사용 원칙
- 용량 사전 할당: 크기를 알면
make([]T, 0, capacity)사용 - append 결과 받기:
s = append(s, value)형태로 사용 - 메모리 공유 주의: 슬라이싱 후 원본 수정 시 영향 고려
- 독립 복사: 필요시
copy사용 - nil 체크:
len(s) == 0대신s == nil사용 가능
성능 최적화 팁
다음은 go를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 비효율적
var result []int
for i := 0; i < 10000; i++ {
result = append(result, i) // 여러 번 재할당
}
// ✅ 효율적
result := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
result = append(result, i) // 재할당 없음
}
// ✅ 더 효율적 (길이를 알 때)
result := make([]int, 10000)
for i := 0; i < 10000; i++ {
result[i] = i // append 없이 직접 할당
}
참고 자료
- Go Slices: usage and internals
- Go Slice Tricks
- “The Go Programming Language” by Alan A. A. Donovan and Brian W. Kernighan
자주 묻는 질문 (FAQ)
Q. 슬라이스는 언제 재할당되나요?
A. append 시 현재 용량을 초과하면 자동으로 재할당됩니다. 일반적으로 기존 용량의 약 2배로 증가합니다.
Q. nil 슬라이스와 빈 슬라이스의 차이는?
A. nil 슬라이스는 var s []int로 선언되고 메모리 할당이 없습니다. 빈 슬라이스는 []int{}로 메모리가 할당되어 있습니다. 하지만 len과 cap은 모두 0이고, append는 둘 다 동작합니다.
Q. 슬라이스를 함수에 전달하면 복사되나요?
A. 슬라이스 헤더(24 bytes)만 복사됩니다. 실제 데이터는 공유되므로, 요소 수정은 원본에 영향을 줍니다. 하지만 append로 길이/용량 변경은 원본에 영향 없습니다.
Q. 슬라이스의 최대 크기는?
A. 이론적으로는 int의 최대값이지만, 실제로는 사용 가능한 메모리에 의해 제한됩니다.
Q. 슬라이스를 비교할 수 있나요?
A. == 연산자로는 nil과만 비교 가능합니다. 요소별 비교는 직접 구현하거나 reflect.DeepEqual을 사용해야 합니다.
한 줄 요약: Go 슬라이스는 포인터·길이·용량으로 구성된 동적 배열로, 메모리를 공유하므로 사용 시 주의가 필요합니다.
관련 글
- Go 배열 완벽 가이드
- Go 메모리 관리와 가비지 컬렉션
- Go 맵(Map) 완벽 가이드
- Go 성능 최적화 기법