[Go] slice
Updated:
개요
슬라이스(slice)는 동적 크기를 가진 배열의 유연한 뷰입니다.
주요 특징:
- 동적 크기: 런타임에 크기 변경 가능
- 참조 타입: 내부 배열에 대한 참조 (포인터, 길이, 용량)
- 제로값은 nil: nil 슬라이스도 안전하게 사용 가능
make()또는 슬라이스 리터럴로 생성append()로 요소 추가 (자동 확장)copy()로 복사- 배열보다 훨씬 많이 사용됨
슬라이스의 내부 구조
// 슬라이스의 런타임 표현
type slice struct {
ptr *ElementType // 내부 배열의 포인터
len int // 현재 길이
cap int // 용량 (최대 길이)
}
func sliceInternals() {
// 슬라이스 생성
s := []int{1, 2, 3, 4, 5}
// 길이: 현재 요소 개수
fmt.Println("Length:", len(s)) // 5
// 용량: 내부 배열의 크기
fmt.Println("Capacity:", cap(s)) // 5
// 슬라이싱으로 뷰 생성
s2 := s[1:4] // [2, 3, 4]
fmt.Println("s2:", s2)
fmt.Println("s2 len:", len(s2)) // 3
fmt.Println("s2 cap:", cap(s2)) // 4 (s[1]부터 끝까지)
// 같은 내부 배열 공유
s2[0] = 999
fmt.Println("s:", s) // [1 999 3 4 5]
fmt.Println("s2:", s2) // [999 3 4]
}
슬라이스 생성 방법
1. nil 슬라이스
func nilSlice() {
var s []int
fmt.Println(s) // []
fmt.Println(s == nil) // true
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
// nil 슬라이스도 안전하게 사용 가능
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
}
2. 빈 슬라이스
func emptySlice() {
// 방법 1: 리터럴
s1 := []int{}
fmt.Println(s1 == nil) // false
fmt.Println(len(s1)) // 0
// 방법 2: make
s2 := make([]int, 0)
fmt.Println(s2 == nil) // false
fmt.Println(len(s2)) // 0
// nil vs 빈 슬라이스는 대부분의 경우 동일하게 동작
// JSON 직렬화 등에서 차이 발생 가능
}
3. make로 생성
func makeSlice() {
// make([]Type, length, capacity)
// 길이 5, 용량 5
s1 := make([]int, 5)
fmt.Println(s1) // [0 0 0 0 0]
fmt.Println(len(s1)) // 5
fmt.Println(cap(s1)) // 5
// 길이 3, 용량 5
s2 := make([]int, 3, 5)
fmt.Println(s2) // [0 0 0]
fmt.Println(len(s2)) // 3
fmt.Println(cap(s2)) // 5
// append 시 재할당 없이 확장
s2 = append(s2, 1, 2)
fmt.Println(s2) // [0 0 0 1 2]
fmt.Println(len(s2)) // 5
fmt.Println(cap(s2)) // 5
}
4. 슬라이스 리터럴
func sliceLiteral() {
// 초기값과 함께 생성
s1 := []int{1, 2, 3, 4, 5}
// 인덱스 지정 초기화
s2 := []int{0: 10, 2: 20, 5: 50}
fmt.Println(s2) // [10 0 20 0 0 50]
// 문자열 슬라이스
s3 := []string{"Go", "Python", "Java"}
// 구조체 슬라이스
type Person struct {
Name string
Age int
}
s4 := []Person{
{"Alice", 25},
{"Bob", 30},
}
fmt.Printf("%+v\n", s4)
}
5. 배열에서 슬라이스 생성
func arrayToSlice() {
arr := [5]int{1, 2, 3, 4, 5}
// 전체 슬라이스
s1 := arr[:]
fmt.Println(s1) // [1 2 3 4 5]
// 부분 슬라이스
s2 := arr[1:4]
fmt.Println(s2) // [2 3 4]
// 슬라이스는 배열을 참조
s1[0] = 100
fmt.Println(arr) // [100 2 3 4 5]
}
슬라이싱 연산
1. 기본 슬라이싱
func slicingBasics() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// s[low:high] - low부터 high-1까지
fmt.Println(s[2:5]) // [2 3 4]
// 처음부터
fmt.Println(s[:5]) // [0 1 2 3 4]
// 끝까지
fmt.Println(s[5:]) // [5 6 7 8 9]
// 전체
fmt.Println(s[:]) // [0 1 2 3 4 5 6 7 8 9]
// 빈 슬라이스
fmt.Println(s[5:5]) // []
}
2. Full Slice Expression (3-인덱스)
func fullSliceExpression() {
s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// s[low:high:max] - 용량 제한
s2 := s[2:5:7]
fmt.Println(s2) // [2 3 4]
fmt.Println(len(s2)) // 3
fmt.Println(cap(s2)) // 5 (7-2)
// 용량을 제한하여 원본 슬라이스 보호
s3 := s[2:5:5] // 용량 3
s3 = append(s3, 99)
// s3이 확장되면서 새 배열 할당, s는 영향받지 않음
fmt.Println(s) // [0 1 2 3 4 5 6 7 8 9]
fmt.Println(s3) // [2 3 4 99]
}
Append 함수
1. 기본 Append
func appendBasics() {
var s []int
fmt.Println(s, len(s), cap(s)) // [] 0 0
// 단일 요소 추가
s = append(s, 1)
fmt.Println(s, len(s), cap(s)) // [1] 1 1
// 여러 요소 추가
s = append(s, 2, 3, 4)
fmt.Println(s, len(s), cap(s)) // [1 2 3 4] 4 4
// 다른 슬라이스 추가 (언팩)
s2 := []int{5, 6, 7}
s = append(s, s2...)
fmt.Println(s, len(s), cap(s)) // [1 2 3 4 5 6 7] 7 8
}
2. Append 용량 확장
func appendCapacity() {
s := make([]int, 0, 2)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
s = append(s, 1)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=1 cap=2 [1]
s = append(s, 2)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=2 cap=2 [1 2]
// 용량 초과 시 재할당 (일반적으로 2배)
s = append(s, 3)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=3 cap=4 [1 2 3]
s = append(s, 4, 5, 6)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
// len=6 cap=8 [1 2 3 4 5 6]
}
3. Append 반환값 할당 중요성
func appendReturnValue() {
s1 := []int{1, 2, 3}
s2 := s1
// ❌ 잘못된 사용: 반환값 무시
append(s1, 4)
fmt.Println(s1) // [1 2 3] (변경 안 됨)
// ✅ 올바른 사용: 반환값 할당
s1 = append(s1, 4)
fmt.Println(s1) // [1 2 3 4]
// 주의: s1과 s2가 다른 배열을 가리킬 수 있음
fmt.Println(s2) // [1 2 3] 또는 [1 2 3 4] (용량에 따라 다름)
}
Copy 함수
func copySlice() {
src := []int{1, 2, 3, 4, 5}
// 같은 크기로 복사
dst1 := make([]int, len(src))
n := copy(dst1, src)
fmt.Printf("Copied %d elements: %v\n", n, dst1)
// Copied 5 elements: [1 2 3 4 5]
// 작은 크기로 복사
dst2 := make([]int, 3)
n = copy(dst2, src)
fmt.Printf("Copied %d elements: %v\n", n, dst2)
// Copied 3 elements: [1 2 3]
// 큰 크기로 복사
dst3 := make([]int, 7)
n = copy(dst3, src)
fmt.Printf("Copied %d elements: %v\n", n, dst3)
// Copied 5 elements: [1 2 3 4 5 0 0]
// 원본 수정해도 복사본 영향 없음
src[0] = 999
fmt.Println("src:", src) // [999 2 3 4 5]
fmt.Println("dst1:", dst1) // [1 2 3 4 5]
}
슬라이스 연산
1. 요소 삭제
func deleteElements() {
s := []int{1, 2, 3, 4, 5}
// 인덱스 2 삭제 (순서 유지)
i := 2
s = append(s[:i], s[i+1:]...)
fmt.Println(s) // [1 2 4 5]
// 인덱스 1 삭제 (빠른 방법, 순서 변경)
s = []int{1, 2, 3, 4, 5}
i = 1
s[i] = s[len(s)-1] // 마지막 요소로 대체
s = s[:len(s)-1] // 마지막 제거
fmt.Println(s) // [1 5 3 4]
// 범위 삭제
s = []int{1, 2, 3, 4, 5}
s = append(s[:1], s[3:]...)
fmt.Println(s) // [1 4 5]
}
2. 요소 삽입
func insertElements() {
s := []int{1, 2, 5, 6}
// 인덱스 2에 3, 4 삽입
i := 2
s = append(s[:i], append([]int{3, 4}, s[i:]...)...)
fmt.Println(s) // [1 2 3 4 5 6]
// 더 효율적인 방법
s = []int{1, 2, 5, 6}
i = 2
s = append(s, 0, 0) // 공간 확보
copy(s[i+2:], s[i:]) // 뒤로 이동
s[i], s[i+1] = 3, 4 // 삽입
fmt.Println(s) // [1 2 3 4 5 6]
}
3. 필터링
func filterSlice() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 짝수만 필터
evens := []int{}
for _, n := range numbers {
if n%2 == 0 {
evens = append(evens, n)
}
}
fmt.Println(evens) // [2 4 6 8 10]
// In-place 필터링 (메모리 효율적)
numbers = []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
n := 0
for _, x := range numbers {
if x%2 == 0 {
numbers[n] = x
n++
}
}
numbers = numbers[:n]
fmt.Println(numbers) // [2 4 6 8 10]
}
4. 역순 정렬
func reverseSlice() {
s := []int{1, 2, 3, 4, 5}
// In-place 역순
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
fmt.Println(s) // [5 4 3 2 1]
}
다차원 슬라이스
func multiDimensionalSlice() {
// 2D 슬라이스 생성
rows, cols := 3, 4
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
// 값 할당
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
matrix[i][j] = i*cols + j
}
}
fmt.Println(matrix)
// [[0 1 2 3] [4 5 6 7] [8 9 10 11]]
// 초기값과 함께 생성
matrix2 := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
fmt.Println(matrix2)
}
슬라이스와 메모리
1. 슬라이스 공유 문제
func sliceSharing() {
original := []int{1, 2, 3, 4, 5}
// 슬라이싱은 같은 배열 공유
slice1 := original[1:4]
slice2 := original[2:5]
slice1[1] = 999
fmt.Println("original:", original) // [1 2 999 4 5]
fmt.Println("slice1:", slice1) // [2 999 4]
fmt.Println("slice2:", slice2) // [999 4 5]
}
2. 메모리 누수 방지
func avoidMemoryLeak() {
// ❌ 메모리 누수 가능
func() []int {
data := make([]int, 1000000)
// ... 데이터 초기화 ...
// 작은 부분만 반환하지만 전체 배열이 메모리에 유지됨
return data[:10]
}
// ✅ 복사하여 반환
func() []int {
data := make([]int, 1000000)
// ... 데이터 초기화 ...
result := make([]int, 10)
copy(result, data[:10])
return result // data는 GC 대상이 됨
}
}
3. 용량 사전 할당
func preAllocate() {
// ❌ 비효율적: 매번 재할당
var s []int
for i := 0; i < 1000; i++ {
s = append(s, i)
}
// ✅ 효율적: 용량 사전 할당
s = make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
s = append(s, i)
}
}
실전 활용 패턴
1. 스택 구현
type Stack struct {
items []int
}
func (s *Stack) Push(item int) {
s.items = append(s.items, item)
}
func (s *Stack) Pop() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
index := len(s.items) - 1
item := s.items[index]
s.items = s.items[:index]
return item, true
}
func (s *Stack) Peek() (int, bool) {
if len(s.items) == 0 {
return 0, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack) IsEmpty() bool {
return len(s.items) == 0
}
2. 큐 구현
type Queue struct {
items []int
}
func (q *Queue) Enqueue(item int) {
q.items = append(q.items, item)
}
func (q *Queue) Dequeue() (int, bool) {
if len(q.items) == 0 {
return 0, false
}
item := q.items[0]
q.items = q.items[1:]
return item, true
}
func (q *Queue) Front() (int, bool) {
if len(q.items) == 0 {
return 0, false
}
return q.items[0], true
}
3. 중복 제거
func removeDuplicates(slice []int) []int {
seen := make(map[int]bool)
result := []int{}
for _, value := range slice {
if !seen[value] {
seen[value] = true
result = append(result, value)
}
}
return result
}
func main() {
numbers := []int{1, 2, 2, 3, 3, 3, 4, 5, 5}
unique := removeDuplicates(numbers)
fmt.Println(unique) // [1 2 3 4 5]
}
4. 병합 정렬된 슬라이스
func mergeSorted(a, b []int) []int {
result := make([]int, 0, len(a)+len(b))
i, j := 0, 0
for i < len(a) && j < len(b) {
if a[i] < b[j] {
result = append(result, a[i])
i++
} else {
result = append(result, b[j])
j++
}
}
// 남은 요소 추가
result = append(result, a[i:]...)
result = append(result, b[j:]...)
return result
}
func main() {
a := []int{1, 3, 5, 7}
b := []int{2, 4, 6, 8}
merged := mergeSorted(a, b)
fmt.Println(merged) // [1 2 3 4 5 6 7 8]
}
5. 슬라이스 청크 분할
func chunkSlice(slice []int, chunkSize int) [][]int {
var chunks [][]int
for i := 0; i < len(slice); i += chunkSize {
end := i + chunkSize
if end > len(slice) {
end = len(slice)
}
chunks = append(chunks, slice[i:end])
}
return chunks
}
func main() {
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
chunks := chunkSlice(numbers, 3)
fmt.Println(chunks) // [[1 2 3] [4 5 6] [7 8 9] [10]]
}
6. 제네릭 슬라이스 함수 (Go 1.18+)
// Map 함수
func Map[T, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Filter 함수
func Filter[T any](slice []T, fn func(T) bool) []T {
result := []T{}
for _, v := range slice {
if fn(v) {
result = append(result, v)
}
}
return result
}
// Reduce 함수
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
acc := initial
for _, v := range slice {
acc = fn(acc, v)
}
return acc
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
// Map: 각 요소를 2배로
doubled := Map(numbers, func(n int) int { return n * 2 })
fmt.Println(doubled) // [2 4 6 8 10]
// Filter: 짝수만
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
fmt.Println(evens) // [2 4]
// Reduce: 합계
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })
fmt.Println(sum) // 15
}
성능 최적화
1. 용량 사전 할당 벤치마크
import "testing"
func BenchmarkAppendWithoutCap(b *testing.B) {
for i := 0; i < b.N; i++ {
var s []int
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
}
func BenchmarkAppendWithCap(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 0, 1000)
for j := 0; j < 1000; j++ {
s = append(s, j)
}
}
}
// 결과: WithCap이 약 10배 빠름
2. In-place 연산
// ❌ 새 슬라이스 생성 (메모리 할당)
func transform1(s []int) []int {
result := make([]int, len(s))
for i, v := range s {
result[i] = v * 2
}
return result
}
// ✅ In-place 수정 (메모리 효율적)
func transform2(s []int) {
for i := range s {
s[i] *= 2
}
}
일반적인 실수
1. Append 반환값 무시
func mistake1() {
s := []int{1, 2, 3}
// ❌ 반환값 무시
append(s, 4)
fmt.Println(s) // [1 2 3] (변경 안 됨)
// ✅ 반환값 할당
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
}
2. 슬라이스 순회 중 수정
func mistake2() {
s := []int{1, 2, 3, 4, 5}
// ❌ 순회 중 슬라이스 확장 (예측 불가능)
for _, v := range s {
if v%2 == 0 {
s = append(s, v*10)
}
}
// range는 초기 슬라이스의 복사본을 순회
// ✅ 인덱스 기반 순회
for i := 0; i < len(s); i++ {
if s[i]%2 == 0 {
s = append(s, s[i]*10)
}
}
}
3. 슬라이스 복사 시 참조 공유
func mistake3() {
s1 := []int{1, 2, 3}
// ❌ 참조만 복사
s2 := s1
s2[0] = 999
fmt.Println(s1) // [999 2 3]
// ✅ 값 복사
s3 := make([]int, len(s1))
copy(s3, s1)
s3[0] = 100
fmt.Println(s1) // [999 2 3]
fmt.Println(s3) // [100 2 3]
}
4. nil vs 빈 슬라이스 혼동
func mistake4() {
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
// 대부분의 경우 동일하게 동작
fmt.Println(len(s1), len(s2), len(s3)) // 0 0 0
// JSON 직렬화 차이
// s1 -> null
// s2, s3 -> []
}
5. 슬라이스 비교
func mistake5() {
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
// ❌ 컴파일 에러: 슬라이스는 == 비교 불가
// fmt.Println(s1 == s2)
// ✅ nil 비교만 가능
fmt.Println(s1 == nil) // false
// ✅ 수동 비교
equal := len(s1) == len(s2)
if equal {
for i := range s1 {
if s1[i] != s2[i] {
equal = false
break
}
}
}
fmt.Println(equal) // true
// ✅ reflect 사용 (성능 저하)
fmt.Println(reflect.DeepEqual(s1, s2)) // true
}
nil 슬라이스 처리
func nilSliceHandling() {
var s []int
// ✅ nil 슬라이스도 안전하게 사용 가능
fmt.Println(len(s)) // 0
fmt.Println(cap(s)) // 0
for range s {
// 실행 안 됨
}
s = append(s, 1, 2, 3)
fmt.Println(s) // [1 2 3]
// ❌ nil 체크 불필요
// if s != nil {
// for _, v := range s { ... }
// }
// ✅ 길이 체크로 충분
if len(s) > 0 {
// ...
}
}
정리
- 슬라이스는 동적 배열의 뷰 (포인터, 길이, 용량)
- 참조 타입: 슬라이싱 시 내부 배열 공유
- nil 슬라이스: 안전하게 사용 가능, nil 체크 불필요
make([]T, len, cap)로 용량 사전 할당append()반환값을 반드시 할당copy()로 깊은 복사- 슬라이스는
==비교 불가 (nil 비교만 가능) - 용량 초과 시 자동 확장 (일반적으로 2배)
- Full slice expression으로 용량 제한 가능
- 큰 데이터 처리 시 용량 사전 할당으로 성능 향상
- In-place 연산으로 메모리 효율성 개선
- 슬라이스 공유 문제 주의 (메모리 누수 가능)
- 배열보다 슬라이스 사용 권장