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

Go 배열, slice, map, range

by JLearn 2026. 7. 13.
반응형

Go에서 여러 값을 다룰 때는 배열, slice, map을 주로 사용합니다.

배열은 길이가 고정된 값 타입이고, slice는 배열의 연속된 영역을 가리키는 자료구조입니다. map은 key와 value를 연결해 저장합니다. range는 배열, slice, map 등의 요소를 순회할 때 사용합니다.


디렉터리 구조는 다음과 같이 구성합니다.

sources/
└── collections/
    └── main.go

sources 디렉터리에서 실행합니다.

cd sources
go run ./collections

배열과 slice의 차이

배열은 선언할 때 길이가 정해지며, 길이도 배열 타입의 일부입니다.

var first [3]int
var second [4]int

first의 타입은 [3]int, second의 타입은 [4]int입니다. 요소 타입이 모두 int여도 길이가 다르므로 서로 다른 타입입니다.

배열을 다른 변수에 대입하면 모든 요소가 복사됩니다.

package main

import "fmt"

func main() {
    original := [3]string{"keyboard", "mouse", "monitor"}
    copied := original

    copied[0] = "notebook"

    fmt.Println("original:", original)
    fmt.Println("copied:", copied)
}

실행 결과:

original: [keyboard mouse monitor]
copied: [notebook mouse monitor]

copied를 변경해도 original은 바뀌지 않습니다. 배열 대입 시 배열 전체 값이 복사되기 때문입니다.

slice는 배열처럼 길이가 타입에 포함되지 않습니다.

var names []string

[]string은 문자열 slice 타입입니다. slice는 내부적으로 backing array의 연속된 영역을 설명하며, 길이와 용량 정보를 함께 가집니다.

backing array란 무엇인가?

backing array란 slice의 실제 요소를 저장하고 있는 원본 배열을 말합니다. slice 자체는 요소를 직접 저장하지 않고, backing array의 어느 위치부터 몇 개의 요소를 사용할지를 나타냅니다.보통 내부 배열이라고 표현합니다.

Go의 slice는 요소 자체를 보관하는 독립된 배열이 아닙니다. slice는 backing array의 어느 위치부터 몇 개의 요소를 사용할지를 나타내는 descriptor입니다. 따라서 slice를 생성하면 실제 값은 backing array에 저장되고, slice는 그 배열의 일부 또는 전체를 바라봅니다.

배열이나 다른 slice를 범위 표현식으로 자르면 새로운 요소 저장 공간이 복사되어 만들어지는 것이 아니라, 기존 backing array를 공유하는 새로운 slice가 만들어집니다. 이 때문에 같은 영역을 공유하는 배열이나 slice 중 하나에서 값을 변경하면 다른 쪽에서도 변경된 값이 보일 수 있습니다.

slice는 개념적으로 다음 정보를 이용해 backing array의 일부 영역을 나타냅니다.

정보 의미
시작 위치 backing array에서 slice의 첫 번째 요소가 시작되는 위치
길이 len 현재 slice에서 사용할 수 있는 요소 수
용량 cap 시작 위치부터 backing array 끝까지 확장할 수 있는 최대 요소 수

예를 들어 다음 배열에서 slice를 생성할 수 있습니다.

devices := [5]string{"keyboard", "mouse", "monitor", "speaker", "camera"}
selected := devices[1:4]

이때 관계는 다음과 같습니다.

backing array: [keyboard mouse monitor speaker camera]
                         └──── selected ────┘
selected:      [mouse monitor speaker]
len(selected): 3
cap(selected): 4

selecteddevices[1]부터 devices[3]까지를 사용하므로 길이는 3입니다. 시작 위치인 devices[1]부터 배열의 마지막 요소까지는 4개이므로 용량은 4입니다.

fmt.Println(len(selected)) // 3
fmt.Println(cap(selected)) // 4

배열과 slice가 같은 backing array를 사용하면 요소도 공유합니다.

selected[0] = "trackball"
fmt.Println(devices)

출력:

[keyboard trackball monitor speaker camera]

selected[0]은 backing array의 devices[1]과 같은 요소이므로, slice에서 값을 변경하면 원래 배열에서도 변경된 값이 보입니다. 같은 backing array에서 만든 여러 slice도 서로 영향을 줄 수 있습니다.

first := devices[1:3]
second := devices[2:5]

first[1] = "display"

fmt.Println(second)

출력:

[display speaker camera]

first[1]second[0]은 backing array의 같은 요소를 가리킵니다.

backing array는 slice를 이해하기 위한 핵심 개념입니다. 다만 프로그램에서는 backing array에 직접 접근하는 것이 아니라 slice를 통해 요소를 읽고 변경합니다. append로 용량이 부족해지면 새로운 backing array가 만들어질 수 있으므로, 이전 slice와 새 slice가 계속 같은 배열을 공유한다고 가정하면 안 됩니다.

package main

import "fmt"

func main() {
    devices := [4]string{"keyboard", "mouse", "monitor", "speaker"}
    selected := devices[1:3]

    fmt.Println("selected:", selected)
    fmt.Println("len:", len(selected))
    fmt.Println("cap:", cap(selected))

    selected[0] = "trackball"

    fmt.Println("devices:", devices)
}

실행 결과:

selected: [mouse monitor]
len: 2
cap: 3
devices: [keyboard trackball monitor speaker]

selecteddevices 배열의 일부를 가리킵니다. 따라서 slice 요소를 변경하면 같은 backing array를 공유하는 배열이나 다른 slice의 값도 바뀔 수 있습니다.

slice를 다른 변수에 대입하면 slice가 가리키는 요소 전체가 복사되는 것이 아니라, backing array를 참조하는 정보가 복사됩니다. 서로 독립된 데이터를 만들려면 copy 또는 slices.Clone 등을 사용해야 합니다.


slice 생성과 append

slice는 리터럴이나 make로 생성할 수 있습니다.

items := []string{"pen", "notebook"}
counts := make([]int, 3)
queue := make([]string, 0, 5)
표현 의미
[]string{"pen", "notebook"} 값이 들어 있는 slice 생성
make([]int, 3) 길이 3, 용량 3인 slice 생성
make([]string, 0, 5) 길이 0, 용량 5인 slice 생성

slice에 요소를 추가할 때는 append를 사용합니다.

package main

import "fmt"

func main() {
    tasks := []string{"review"}

    tasks = append(tasks, "test")
    tasks = append(tasks, "deploy", "monitor")

    fmt.Println(tasks)
}

실행 결과:

[review test deploy monitor]

append는 기존 backing array의 용량이 충분하면 같은 배열을 재사용하고, 용량이 부족하면 더 큰 새로운 backing array를 할당합니다. 어느 경우든 길이와 backing array 정보가 달라질 수 있으므로 반환된 slice를 반드시 다시 받아야 합니다.

일반적으로는 원래 slice 변수에 반환값을 다시 대입합니다.

tasks = append(tasks, "archive")

이 형태를 사용하면 기존 backing array가 재사용되든 새로운 backing array가 만들어지든 tasks가 항상 최신 slice를 가리킵니다.

아래처럼 반환값을 사용하지 않으면 컴파일 오류가 발생합니다.

append(tasks, "archive")

같은 backing array를 사용하는 경우

배열이나 slice를 범위 표현식으로 다시 자르면 원본과 같은 backing array를 사용합니다.

numbers := [5]int{10, 20, 30, 40, 50}
part := numbers[1:4]

numbers[2] = 300

fmt.Println(numbers) // [10 20 300 40 50]
fmt.Println(part)    // [20 300 40]

원본 배열의 numbers[2]와 slice의 part[1]은 같은 요소입니다. 따라서 원본 배열을 변경하면 slice에서도 변경된 값이 보이고, slice를 변경하면 원본 배열에도 반영됩니다.

slice에서 다시 만든 slice도 같은 backing array를 공유합니다.

values := []int{10, 20, 30, 40, 50}
first := values[1:4]
second := values[2:5]

first[1] = 300

fmt.Println(values) // [10 20 300 40 50]
fmt.Println(second) // [300 40 50]

다른 backing array를 사용하는 경우

새 slice를 만들고 copy하면 원본과 독립된 backing array를 사용할 수 있습니다.

source := []int{10, 20, 30}
cloned := make([]int, len(source))
copy(cloned, source)

cloned[0] = 100

fmt.Println(source) // [10 20 30]
fmt.Println(cloned) // [100 20 30]

backing array의 공유 여부는 생성 방식과 append 시점의 용량으로 판단할 수 있습니다.

코드 backing array 관계
part := array[1:3] 원본 배열과 동일
second := first[1:3] first와 동일
makecopy 원본과 다름
append할 때 기존 용량이 충분함 기존 backing array 재사용
append할 때 기존 용량이 부족함 새로운 backing array 할당

append와 backing array 공유

기존 용량이 충분하면 append는 같은 backing array의 다음 위치에 값을 저장합니다.

numbers := make([]int, 3, 5)
copy(numbers, []int{10, 20, 30})

part := numbers[:2]
part = append(part, 99)

fmt.Println(numbers) // [10 20 99]
fmt.Println(part)    // [10 20 99]

part의 길이는 2이지만 용량이 남아 있으므로 append한 값이 같은 backing array의 세 번째 요소에 저장됩니다. 이 때문에 numbers[2]99로 바뀝니다.

반대로 용량이 부족하면 새로운 backing array가 만들어집니다.

numbers := make([]int, 3, 3)
copy(numbers, []int{10, 20, 30})

extended := append(numbers, 40)
extended[0] = 100

fmt.Println(numbers)  // [10 20 30]
fmt.Println(extended) // [100 20 30 40]

numbers의 길이와 용량이 모두 3이므로 요소를 하나 더 추가하려면 새로운 backing array가 필요합니다. 이후 extended의 요소를 변경해도 numbers에는 영향을 주지 않습니다.

다른 변수에 append 결과를 받으면 원본과 확장된 slice가 같은 backing array를 공유할 수도 있고, 공유하지 않을 수도 있습니다. 단순히 목록을 계속 확장하는 목적이라면 slice = append(slice, value)처럼 원래 변수에 다시 대입하는 방식이 가장 명확합니다.

원본과 독립된 slice에 값을 추가하려면 먼저 복사할 수 있습니다.

numbers := []int{10, 20, 30}

extended := append([]int(nil), numbers...)
extended = append(extended, 40)

fmt.Println(numbers)  // [10 20 30]
fmt.Println(extended) // [10 20 30 40]

nil slice에도 append할 수 있습니다.

var numbers []int
numbers = append(numbers, 10)

nil slice는 길이와 용량이 0이지만 append, len, cap, range를 사용할 수 있습니다.

slice의 내용을 독립적으로 복사하려면 새 slice를 만들고 copy를 사용합니다.

source := []string{"alpha", "beta", "gamma"}
cloned := make([]string, len(source))
copy(cloned, source)

cloned[0] = "changed"

이 경우 cloned을 변경해도 source에는 영향을 주지 않습니다.


map 생성과 조회

map은 key를 이용해 value를 찾는 연관 자료구조입니다.

scores := map[string]int{
    "kim":  90,
    "lee":  85,
}

make로 빈 map을 생성할 수도 있습니다.

scores := make(map[string]int)
scores["kim"] = 90

map에서 값을 읽을 때 key가 없으면 value 타입의 zero value가 반환됩니다.

package main

import "fmt"

func main() {
    stock := map[string]int{
        "keyboard": 10,
        "mouse":    0,
    }

    fmt.Println(stock["keyboard"])
    fmt.Println(stock["mouse"])
    fmt.Println(stock["monitor"])
}

실행 결과:

10
0
0

mouse는 실제로 저장된 값이 0이고, monitor는 key 자체가 없습니다. 값만 조회하면 둘을 구분할 수 없습니다.

key 존재 여부가 중요하면 두 번째 반환값을 사용합니다.

quantity, ok := stock["monitor"]
if !ok {
    fmt.Println("등록되지 않은 상품입니다.")
} else {
    fmt.Println("재고:", quantity)
}

map 요소는 delete로 삭제합니다.

delete(stock, "keyboard")

존재하지 않는 key를 삭제해도 오류가 발생하지 않습니다.

nil map

선언만 한 map의 zero value는 nil입니다.

var settings map[string]string

nil map은 읽을 수 있습니다.

value := settings["theme"]
fmt.Println(value)

존재하는 key가 없으므로 문자열의 zero value인 빈 문자열이 반환됩니다.

하지만 nil map에 값을 저장하면 panic이 발생합니다.

var settings map[string]string
settings["theme"] = "dark"

실행 시 다음과 같은 오류가 발생합니다.

panic: assignment to entry in nil map

값을 저장하려면 map 리터럴이나 make로 초기화해야 합니다.

settings := make(map[string]string)
settings["theme"] = "dark"

range로 순회하기

range는 배열, slice, map 등의 요소를 반복해서 처리할 때 사용합니다.

배열이나 slice를 순회하면 index와 value를 받을 수 있습니다.

package main

import "fmt"

func main() {
    languages := []string{"Go", "Java", "Python"}

    for index, language := range languages {
        fmt.Printf("%d: %s\n", index, language)
    }
}

실행 결과:

0: Go
1: Java
2: Python

index가 필요 없으면 _로 생략합니다.

for _, language := range languages {
    fmt.Println(language)
}

value가 필요 없으면 index만 받을 수 있습니다.

for index := range languages {
    fmt.Println(index)
}

map을 순회하면 key와 value를 받을 수 있습니다.

for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

map의 순회 순서는 지정되어 있지 않습니다. 실행할 때마다 같은 순서로 나온다고 가정하면 안 됩니다.

출력이나 테스트에서 일정한 순서가 필요하면 key를 slice에 모아 정렬한 뒤 조회합니다.

package main

import (
    "fmt"
    "slices"
)

func main() {
    scores := map[string]int{
        "kim":  90,
        "lee":  85,
        "park": 95,
    }

    names := make([]string, 0, len(scores))
    for name := range scores {
        names = append(names, name)
    }

    slices.Sort(names)

    for _, name := range names {
        fmt.Printf("%s: %d\n", name, scores[name])
    }
}

실행 결과:

kim: 90
lee: 85
park: 95

range의 value는 복사본

배열이나 slice를 range로 순회할 때 value 변수에는 각 요소의 값이 복사됩니다.

numbers := []int{10, 20, 30}

for _, number := range numbers {
    number *= 2
}

fmt.Println(numbers)

출력:

[10 20 30]

원본 요소를 변경하려면 index를 사용해야 합니다.

for index := range numbers {
    numbers[index] *= 2
}

출력:

[20 40 60]

전체 예제

아래 예제는 배열 복사, slice의 backing array 공유, append, map 조회, 정렬된 map 출력, range를 한 번에 확인합니다.

sources/collections/main.go:

package main

import (
    "fmt"
    "slices"
)

func main() {
    categories := [3]string{"book", "office", "digital"}
    copiedCategories := categories
    copiedCategories[0] = "home"

    fmt.Println("categories:", categories)
    fmt.Println("copied categories:", copiedCategories)

    products := [4]string{"keyboard", "mouse", "monitor", "speaker"}
    selectedProducts := products[1:3]
    selectedProducts[0] = "trackball"

    fmt.Println("products:", products)
    fmt.Println("selected products:", selectedProducts)
    fmt.Println("selected len:", len(selectedProducts))
    fmt.Println("selected cap:", cap(selectedProducts))

    tasks := []string{"review"}
    tasks = append(tasks, "test", "deploy")

    fmt.Println("tasks:")
    for index, task := range tasks {
        fmt.Printf("  %d: %s\n", index, task)
    }

    stock := map[string]int{
        "keyboard": 10,
        "mouse":    0,
        "monitor":  5,
    }

    quantity, ok := stock["speaker"]
    if !ok {
        fmt.Println("speaker: not found")
    } else {
        fmt.Println("speaker:", quantity)
    }

    names := make([]string, 0, len(stock))
    for name := range stock {
        names = append(names, name)
    }

    slices.Sort(names)

    fmt.Println("stock:")
    for _, name := range names {
        fmt.Printf("  %s: %d\n", name, stock[name])
    }
}

실행:

cd sources
go run ./collections

실행 결과:

categories: [book office digital]
copied categories: [home office digital]
products: [keyboard trackball monitor speaker]
selected products: [trackball monitor]
selected len: 2
selected cap: 3
tasks:
  0: review
  1: test
  2: deploy
speaker: not found
stock:
  keyboard: 10
  monitor: 5
  mouse: 0

정리

배열은 길이가 타입에 포함되는 고정 크기 값이며, 배열을 대입하면 전체 요소가 복사됩니다.

slice는 backing array의 연속된 영역을 나타냅니다. 같은 backing array의 같은 요소를 공유하면 원본 배열이나 다른 slice의 변경이 서로에게 보입니다. 범위 표현식으로 만든 slice는 backing array를 공유하고, copy로 새 slice에 복사하면 독립된 데이터를 만들 수 있습니다. append는 용량이 충분하면 기존 backing array를 재사용하고 부족하면 새 배열을 할당하므로, 일반적으로 slice = append(slice, value) 형태로 원래 변수에 반환값을 다시 대입합니다.

map은 key와 value를 연결해 저장합니다. 존재하지 않는 key를 읽으면 value 타입의 zero value가 반환되므로, key 존재 여부가 중요할 때는 value, ok := m[key] 형태를 사용합니다. nil map은 읽을 수 있지만 값을 저장하면 panic이 발생합니다.

range는 배열, slice, map을 간결하게 순회할 수 있게 해줍니다. map의 순회 순서는 지정되어 있지 않으며, 일정한 출력 순서가 필요하면 key를 별도로 정렬해야 합니다.


참고 자료

반응형

'프로그래밍 > Go' 카테고리의 다른 글

Go 포인터, string, rune  (0) 2026.07.14
Go 함수, closure, recursion  (1) 2026.07.14
Go if, for, switch와 break, continue  (0) 2026.07.11
Go fmt의 %v와 %q  (0) 2026.07.11
Go 리터럴, 변수, 상수와 Zero Value  (0) 2026.07.10

댓글