본문 바로가기
프로그래밍/Golang

Golang Slice(슬라이스) : make, copy, append

by slowin 2024. 7. 31.

개요

슬라이스를 생성하고 조작하는 데 사용되는 세 가지 중요한 내장 함수인 make, copy, 그리고 append에 대해 살펴보겠습니다.

1. make 함수

`make` 함수는 타입, 길이, 용량 이렇게 세 가지 인수를 받습니다.

 

내장함수 주석은 다음과 같습니다.

// The make built-in function allocates and initializes an object of type
// slice, map, or chan (only). Like new, the first argument is a type, not a
// value. Unlike new, make's return type is the same as the type of its
// argument, not a pointer to it. The specification of the result depends on
// the type:
//
//	Slice: The size specifies the length. The capacity of the slice is
//	equal to its length. A second integer argument may be provided to
//	specify a different capacity; it must be no smaller than the
//	length. For example, make([]int, 0, 10) allocates an underlying array
//	of size 10 and returns a slice of length 0 and capacity 10 that is
//	backed by this underlying array.
//	Map: An empty map is allocated with enough space to hold the
//	specified number of elements. The size may be omitted, in which case
//	a small starting size is allocated.
//	Channel: The channel's buffer is initialized with the specified
//	buffer capacity. If zero, or the size is omitted, the channel is
//	unbuffered.
func make(t Type, size ...IntegerType) Type

정리하면,

  • make 함수는 슬라이스(slice), 맵(map), 채널(chan) 타입의 객체만 할당하고 초기화합니다.
  • 반환되는 결과의 구체적인 사항은 타입에 따라 다릅니다:
    • 슬라이스(Slice): 인수로 전달된 크기는 슬라이스의 길이를 지정합니다. 슬라이스의 용량은 길이와 동일하며, 두 번째 정수 인수를 사용하여 다른 용량을 지정할 수 있습니다. 이때, 용량은 길이보다 작을 수 없습니다. 예를 들어, make([]int, 0, 10)은 크기가 10인 기본 배열을 할당하고, 길이가 0이고 용량이 10인 슬라이스를 반환합니다.
    • 맵(Map): 지정된 요소 수를 담을 수 있는 충분한 공간을 갖춘 빈 맵이 할당됩니다. 크기를 생략할 수 있으며, 이 경우 작은 초기 크기로 할당됩니다.
    • 채널(Channel): 채널의 버퍼는 지정된 버퍼 용량으로 초기화됩니다. 크기가 0이거나 생략된 경우, 채널은 버퍼가 없는 채널이 됩니다.

그렇다면, 실제 예제를 통해 알아보겠습니다.

package main

import "fmt"

func main() {
    // 길이가 5, 용량이 10인 문자열 슬라이스 생성
    fruits := make([]string, 5, 10)
    
    fmt.Printf("fruits 길이: %d, 용량: %d\n", len(fruits), cap(fruits))
    
    // 길이와 용량이 모두 3인 정수 슬라이스 생성
    numbers := make([]int, 3)
    
    fmt.Printf("numbers 길이: %d, 용량: %d\n", len(numbers), cap(numbers))
}

// 출력
// fruits 길이: 5, 용량: 10
// numbers 길이: 3, 용량: 3

 

배열 용량 늘리기

package main

import "fmt"

func main() {
    // 초기 슬라이스 생성: 길이 10, 용량 15
    slice := make([]int, 10, 15)

    // 슬라이스에 초기 값 설정
    for i := range slice {
        slice[i] = i + 1
    }

    // 새로운 슬라이스 생성: 길이는 유지, 용량 2배로 지정
    newSlice := make([]int, len(slice), 2*cap(slice))

    // 기존 슬라이스의 데이터를 새 슬라이스로 복사
    for i := range slice {
        newSlice[i] = slice[i]
    }

    // 원래 슬라이스 변수에 새 슬라이스 할당
    slice = newSlice

    // 슬라이스 확장 후 데이터 추가 
    slice = append(slice, 11, 12, 13, 14, 15)
}

이렇게 용량을 늘리는 로직을 실행하게 되면, 일시적으로 메모리가 기존 슬라이스와 새로운슬라이스 메모리가 합쳐져서 메모리 사용량이 증가되게 됩니다.

단축선언 방법

단축형으로 사용할때 아래와 같이 표현 할 수 있습니다.

slice := make([]slice, 5) // 슬라이스의 길이와 용량이 모두 5

2. copy 함수

copy 함수는 한 슬라이스의 요소를 다른 슬라이스로 복사합니다. 이 함수는 두 슬라이스의 길이 중 더 작은 값만큼의 요소를 복사합니다.

copy(dst, src)

 

  • dst: 대상 슬라이스
  • src: 원본 슬라이스
package main

import "fmt"

func main() {
    // 원본 슬라이스
    src := []int{1, 2, 3, 4, 5}
    
    // 대상 슬라이스 (길이가 더 긴 경우)
    dst1 := make([]int, 7)
    copied1 := copy(dst1, src)
    
    fmt.Printf("복사된 요소 수: %d\n", copied1)
    fmt.Printf("dst1: %v\n", dst1)
    
    // 대상 슬라이스 (길이가 더 짧은 경우)
    dst2 := make([]int, 3)
    copied2 := copy(dst2, src)
    
    fmt.Printf("복사된 요소 수: %d\n", copied2)
    fmt.Printf("dst2: %v\n", dst2)
}

// 출력
// 복사된 요소 수: 5
// dst1: [1 2 3 4 5 0 0]
// 복사된 요소 수: 3
// dst2: [1 2 3]

슬라이스 중간 index에 데이터 삽입

// Insert는 지정된 인덱스에 값을 삽입합니다. 인덱스는 범위 내에 있어야 하며,
// 슬라이스는 새 요소를 위한 공간이 있어야 합니다.
func Insert(slice []int, index, value int) []int {
    // 슬라이스를 하나의 요소로 확장합니다.
    slice = slice[0 : len(slice)+1]
    // copy를 사용하여 슬라이스의 상위 부분을 이동시키고 빈 공간을 만듭니다.
    copy(slice[index+1:], slice[index:])
    // 새 값을 저장합니다.
    slice[index] = value
    // 결과를 반환합니다.
    return slice
}

 

실행 중 데이터 상태를 살펴보겠습니다.

1. 초기 함수 전달된 상태의 슬라이스 상태입니다.

초기 상태

2. copy 함수를 수행 후 insert index 부터 배열이 복제되어 추가된것을 알수 있습니다.

`len`이 10 -> 11로 늘어 났습니다.

copy 후 상태

3. 추가하고자 하는 index에 데이터를 넣은 후 상태입니다.

index:5 의 데이터가 99로 변경된것을 확인할 수 있습니다.

삽입 후 상태

슬라이스에 요소를 삽입하는 로직을 살펴보았습니다.

하지만, 위 코드는 용량이 가득찼을때는 에러가 발생 합니다.

 

용량을 벗어 났을때의 상황을 처리하는 코드를 알아 보겠습니다.

func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // 슬라이스가 가득 찼습니다; 확장해야 합니다.
        // 크기를 두 배로 늘리고 1을 추가합니다. 크기가 0일 때도 여전히 확장합니다.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}

이렇게 한다면, 용량을 넘어가게 되면 배열을 2배늘려서 용량을 확보하고 데이터를 삽입할 수 있습니다.

 

이처럼 Extend 함수 매커니즘을 사용하여 Go언어에서 내장함수로 지원하는 함수가 있습니다.

바로 `append` 함수 입니다. 다음은 `append`함수에 대해 알아 보겠습니다.

3. append 함수

Append 함수 구조

go팀에서 append를 설명하기위한 `Append` 예시함수부터 살펴 보겠습니다.

func Append(slice []int, items ...int) []int
...int 는 0개 이상의 인자를 받겠다는 의미 입니다.

 

구현체는 아래 예시코드입니다.

// Append는 항목을 슬라이스에 추가합니다.
// 첫 번째 버전: 단순히 Extend를 반복 호출합니다.
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

 

효율을 높이기 위해, 용량을 2배가 아닌 사용자 정의로 변경하고 싶다면 다음 코드를 참고 해주세요

// Append는 요소를 슬라이스에 추가합니다.
// 효율적인 버전입니다.
func Append(slice []int, elements ...int) []int {
    n := len(slice)
    total := len(slice) + len(elements)
    if total > cap(slice) {
        // 재할당합니다. 새 크기의 1.5배로 성장하여 계속 확장할 수 있습니다.
        newSize := total*3/2 + 1
        newSlice := make([]int, total, newSize)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[:total]
    copy(slice[n:], elements)
    return slice
}

 

golang 에서 내장함수로 지원하는 append 함수!

append 함수는 슬라이스에 하나 이상의 요소를 추가합니다. 필요한 경우 자동으로 슬라이스의 용량을 늘립니다. (위 Extend 함수에서 살펴 보았습니다.)

slice = append(slice, elements...)
  • slice: 요소를 추가할 슬라이스
  • elements: 추가할 요소들 (가변 인자)

1. 단일 항목 추가하기

// 두 개의 시작 슬라이스 생성
slice := []int{1, 2, 3}
fmt.Println("Start slice: ", slice)

// 슬라이스에 항목 추가하기
slice = append(slice, 4)
fmt.Println("Add one item:", slice)

2. 하나의 슬라이스를 다른 슬라이스에 추가하기

slice2 := []int{55, 66, 77}
fmt.Println("Start slice2:", slice2)

// 하나의 슬라이스를 다른 슬라이스에 추가하기
slice = append(slice, slice2...)
fmt.Println("Add one slice:", slice)

3. 슬라이스 복사하기

// 슬라이스 복사하기 (int 타입)
slice3 := append([]int(nil), slice...)
fmt.Println("Copy a slice:", slice3)

`[]int(nil)`은 `slice`와 같은 타입의 새로운 빈 슬라이스를 만들고, `append`를 사용하여 `slice`의 요소를 `slice3`로 복사합니다.

4. 슬라이스를 자기 자신에 복사하기

// 슬라이스를 자기 자신에 복사하기
fmt.Println("Before append to self:", slice)
slice = append(slice, slice...)
fmt.Println("After append to self:", slice)

// 출력
// Before append to self: [1 1111 3 4 55 66 77]
// After append to self: [1 1111 3 4 55 66 77 1 1111 3 4 55 66 77]

 

정리

go.dev 참고하여 make, copy, append 함수에 대해서 알아 보았습니다.

 

시리즈

Golang Arrays(배열), Slice(슬라이스) - 선언, 초기화 방법

Golang Slice(슬라이스) 동작원리와 사용법

Golang Slice(슬라이스) : make, copy, append

Golang Slice(슬라이스) 요소 제거하기

참고

go.dev