Go · 기본
작고 명확한 Go 프로그램
슬라이스와 맵 - 길이와 용량
배열과 슬라이스, append 와 용량 증가, 공유되는 하위 슬라이스 함정, 맵과 존재 확인, slices·maps 패키지
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장에서는 함수가 값을 여러 개 돌려주고 defer 로 뒷정리를 예약하는 방법을 익혔다. 이 장에서는 값을 여러 개 모아 두는 두 가지 그릇, 슬라이스(slice)와 맵(map)을 다룬다. 자전거 대여소에서 오늘 나간 대여 시간 목록을 만들고, 대여소별 대여 횟수를 세는 일은 이 두 그릇 없이는 시작할 수 없다. 특히 슬라이스는 겉보기와 속이 달라서, 길이(length)와 용량(capacity)이 무엇인지 알아야 예상 밖의 결과를 피할 수 있다.
- 배열과 슬라이스의 차이를 설명하고, 길이와 용량을 구분한다.
- append 가 용량을 늘리는 방식을 이해하고, 그 결과를 반드시 변수에 다시 담는 이유를 안다.
- 하위 슬라이스가 같은 배열을 공유해서 생기는 함정을 알아보고 피한다.
- 맵에서 값을 읽고, 없는 키와 값이 0 인 키를 존재 확인으로 구분한다.
- slices 와 maps 패키지의 함수로 정렬, 검색, 복제를 처리한다.
문제 상황
대여소 운영자가 하루 일과를 마치고 두 가지를 알고 싶어 한다. 하나는 오늘 대여 시간(분)을 모두 모아서 가장 긴 시간이 얼마인지 확인하는 것이다. 다른 하나는 강변, 시청, 역앞 대여소 중 어디에서 자전거가 몇 번 나갔는지 세는 것이다.
대여 건수는 미리 알 수 없다. 변수를 rental1, rental2, rental3 처럼 하나씩 만들 수는 없으므로, 개수가 늘어나는 목록이 필요하다. 대여소 이름으로 횟수를 바로 찾는 표도 필요하다. 앞의 것이 슬라이스이고 뒤의 것이 맵이다.
여기에 함정이 하나 더 있다. 반납 대기열 자전거 다섯 대 중 앞의 두 대를 점검 목록으로 떼어 내 "점검 완료" 표시를 하나 덧붙였더니, 원래 대기열의 세 번째 자전거가 사라져 있었다. 목록을 복사한 것이 아니라 같은 메모리를 함께 보고 있었기 때문이다. 이 장의 절반은 이 현상을 이해하는 데 쓴다.
배열과 슬라이스
배열은 길이가 타입의 일부다
배열(array)은 같은 타입의 값을 정해진 개수만큼 이어 붙인 것이다. 길이는 타입에 포함된다. 그래서 [4]string 과 [5]string 은 서로 다른 타입이다. 배열 변수를 다른 변수에 대입하면 원소 전체가 복사되므로, 복사본을 고쳐도 원본은 그대로다.
docks := [4]string{"A1", "A2", "A3", "A4"}
other := docks
other[0] = "B1"
길이를 실행 중에 바꿀 수 없다는 점 때문에 배열을 직접 쓰는 일은 드물다. 대부분의 Go 코드는 슬라이스를 쓴다.
슬라이스는 배열을 가리키는 작은 창이다
슬라이스는 배열을 소유하지 않는다. 슬라이스 값 하나는 세 가지 정보만 담고 있다. 배열의 어느 칸을 시작점으로 보는지(포인터), 몇 칸을 보는지(길이), 시작점부터 배열 끝까지 몇 칸이 남았는지(용량)이다. 이 세 정보를 슬라이스 헤더라고 부르기도 한다. 헤더 자체는 작아서 변수에 대입하거나 함수에 넘길 때 헤더만 복사되고, 가리키는 배열은 복사되지 않는다.
len(s) 는 길이, cap(s) 는 용량을 돌려준다. 슬라이스는 []string{"B01", "B02"} 처럼 리터럴로 만들거나, 기존 슬라이스나 배열을 s[low:high] 로 잘라서 만든다. 자른 결과는 low 번째 칸에서 시작하고, 길이는 high - low 이며, 용량은 원래 용량에서 low 를 뺀 값이다.
그림처럼 line 은 다섯 칸 전체를 보고, front := line[:2] 는 앞의 두 칸만 본다. 길이는 2 이지만 용량은 5 다. 배열 끝까지는 아직 세 칸이 남아 있고, 그 칸들은 line 이 쓰고 있다.
nil 슬라이스와 제로값
앞에서 본 제로값 규칙은 슬라이스에도 적용된다. var minutes []int 로 선언만 한 슬라이스는 nil 이고 길이와 용량이 모두 0 이다. nil 슬라이스에도 append 를 바로 할 수 있으므로, 빈 목록에서 시작할 때 따로 준비할 것이 없다.
| 구분 | 배열 | 슬라이스 | 맵 |
|---|---|---|---|
| 길이 | 타입에 고정 | 실행 중 변함 | 실행 중 변함 |
| 대입할 때 | 원소 전체 복사 | 헤더만 복사, 배열 공유 | 참조만 복사, 내용 공유 |
| 찾는 방법 | 번호(0부터) | 번호(0부터) | 키 |
| 제로값 | 원소마다 제로값 | nil (append 가능) | nil (쓰기는 패닉) |
append 와 용량 증가
내장 함수 append(s, 값...) 는 슬라이스 끝에 값을 붙인 새 슬라이스를 돌려준다. 동작은 두 갈래로 나뉜다.
- 용량이 남아 있으면 같은 배열의 다음 칸에 값을 쓰고, 길이만 늘어난 슬라이스를 돌려준다.
- 용량이 모자라면 더 큰 새 배열을 만들고 기존 원소를 복사한 뒤 값을 붙여서, 새 배열을 가리키는 슬라이스를 돌려준다.
그래서 append 의 결과는 항상 변수에 다시 담아야 한다. minutes = append(minutes, 30) 이 관용적인 모양이다. 새 배열이 만들어졌다면 원래 변수는 여전히 옛 배열을 가리키고 있기 때문이다.
비어 있는 슬라이스에 원소를 하나씩 붙이면 용량은 대체로 1, 2, 4, 8, 16 으로 두 배씩 커진다. 원소가 아주 많아지면 두 배보다 완만하게 늘어난다. 정확한 증가 규칙은 Go 버전과 원소 크기에 따라 달라질 수 있는 구현 세부 사항이므로, 코드가 특정 용량 값에 의존하게 만들지 않는다. 이 장의 예제는 흐름을 보여 주려고 용량을 출력할 뿐이다.
넣을 개수를 대략 알고 있다면 make([]int, 0, 100) 처럼 길이 0, 용량 100 으로 시작할 수 있다. 처음부터 넉넉한 배열을 잡아 두면 복사 횟수가 줄어든다. 하지만 개수가 정확하지 않다면 굳이 맞추려 하지 않아도 된다.
공유되는 하위 슬라이스 함정
앞 그림으로 돌아가 보자. front := line[:2] 의 용량은 5 이므로 front 에 append 하면 용량이 남아 있다고 판단한다. 새 배열을 만들지 않고 같은 배열의 세 번째 칸에 값을 쓴다. 그 칸은 line 의 세 번째 원소이기도 하다. 결과적으로 front 에 값을 붙였을 뿐인데 line 이 바뀐다.
피하는 방법은 두 가지다.
- 용량을 제한한다.
line[:2:2]처럼 세 번째 값까지 쓰면(전체 슬라이스 식, full slice expression) 용량이 2 로 잘린다. 이 슬라이스에 append 하면 용량이 모자라므로 반드시 새 배열이 만들어지고, 원래 배열은 건드리지 않는다. - 복제한다.
slices.Clone(line[:2])는 원소를 새 배열에 복사한 슬라이스를 돌려준다. 이후에는 서로 영향을 주지 않는다.
같은 원리로, 하위 슬라이스의 원소를 고치면 원본에도 반영된다. 이것은 버그가 아니라 슬라이스의 성질이다. 함수에 큰 슬라이스를 넘길 때 복사가 일어나지 않는 것도 이 성질 덕분이다. 다만 "내가 넘긴 것을 저쪽이 고칠 수 있다"는 점을 항상 기억해야 한다.
맵과 존재 확인
맵은 키(key)로 값(value)을 찾는 자료구조다. 타입은 map[string]int 처럼 키 타입과 값 타입을 함께 적는다. 예제에서는 대여소 이름이 키이고, 재고나 횟수가 값이다. 키 타입은 == 로 비교할 수 있어야 하므로 슬라이스는 키가 될 수 없다.
맵은 리터럴이나 make(map[string]int) 로 만든다. 값을 넣을 때는 m[키] = 값, 지울 때는 delete(m, 키) 를 쓴다. len(m) 은 들어 있는 키의 개수다.
없는 키를 읽으면 제로값이 나온다
맵에 없는 키를 읽어도 오류가 나지 않고 값 타입의 제로값이 나온다. map[string]int 에서는 0 이다. 그래서 "재고가 0 대"인 대여소와 "등록되지 않은 대여소"를 값만 보고는 구분할 수 없다. 이때 읽기 결과를 변수 두 개로 받는다.
n, ok := stock["공원"]
두 번째 값 ok 는 키가 실제로 있으면 true 이다. 함수에서 값 두 개를 돌려받던 모양과 같은 형태이며, 관례적으로 이름을 ok 로 짓는다. 값이 필요 없고 존재 여부만 궁금하면 _, ok := m[키] 로 쓴다.
세기와 모으기
없는 키의 제로값을 이용하면 횟수 세기가 짧아진다. counts[name]++ 는 키가 없으면 0 에서 시작해 1 로 만든다. 값이 슬라이스인 맵도 마찬가지다. 없는 키의 값은 nil 슬라이스이고 nil 슬라이스에는 append 할 수 있으므로, m[k] = append(m[k], v) 한 줄이면 된다.
순회 순서는 정해져 있지 않다
맵을 for range 로 돌면 순서가 실행마다 다를 수 있다. Go 가 일부러 그렇게 하므로 순서에 기대는 코드를 쓰면 안 된다. 출력이나 정산처럼 순서가 필요한 곳에서는 키를 꺼내 정렬한 뒤 그 순서로 값을 읽는다. 이 장의 예제도 그렇게 한다.
slices 와 maps 패키지
표준 라이브러리의 slices 와 maps 패키지는 자주 쓰는 작업을 함수로 제공한다. 직접 반복문을 쓸 수도 있지만, 이름이 있는 함수는 의도가 바로 읽힌다. 자세한 목록은 slices 와 maps 문서에서 확인할 수 있다.
| 함수 | 하는 일 | 비고 |
|---|---|---|
slices.Sort(s) | 슬라이스를 제자리에서 오름차순 정렬 | 원본이 바뀜 |
slices.Clone(s) | 새 배열에 복사한 슬라이스 | 공유 함정 방지 |
slices.Contains(s, v) | v 가 들어 있는지 여부 | 처음부터 차례로 검사 |
slices.Index(s, v) | 처음 나오는 번호, 없으면 -1 | 차례로 검사 |
slices.Max(s) | 가장 큰 원소 | 빈 슬라이스면 패닉 |
slices.BinarySearch(s, v) | 정렬된 s 에서 번호와 발견 여부 | 정렬되어 있어야 함 |
maps.Keys(m) | 키를 차례로 내주는 반복자 | 순서 불명 |
slices.Sorted(seq) | 반복자 값을 모아 정렬한 슬라이스 | 키 정렬에 사용 |
maps.Clone(m) | 맵의 얕은 복사본 | 내용이 서로 독립 |
maps.Equal(a, b) | 두 맵의 내용이 같은지 | 값도 비교 가능해야 함 |
maps.Keys 는 슬라이스가 아니라 반복자(iterator)를 돌려준다. 반복자는 값을 하나씩 내주는 함수의 일종이라고만 알아 두면 충분하다. 반복자의 원리는 이 책의 범위 밖이며, slices.Sorted(maps.Keys(m)) 를 키를 정렬해서 얻는 관용구로 외워 두면 된다.
완성 코드
아래 프로그램은 위에서 설명한 내용을 함수 다섯 개로 나누어 차례로 보여 준다. 파일 이름은 main.go 이다.
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
arrayCopy()
growth()
sharing()
stationMap()
helpers()
}
func arrayCopy() {
docks := [4]string{"A1", "A2", "A3", "A4"}
other := docks
other[0] = "B1"
fmt.Println("배열 원본:", docks)
fmt.Println("배열 복사:", other)
fmt.Println("배열 길이:", len(docks))
}
func growth() {
var minutes []int
fmt.Println("nil 슬라이스:", minutes == nil, len(minutes), cap(minutes))
lastCap := cap(minutes)
for i := 1; i <= 9; i++ {
minutes = append(minutes, i*10)
if cap(minutes) != lastCap {
lastCap = cap(minutes)
fmt.Printf("len=%d cap=%d\n", len(minutes), cap(minutes))
}
}
fmt.Println(minutes)
}
func newLine() []string {
return []string{"B01", "B02", "B03", "B04", "B05"}
}
func sharing() {
line := newLine()
front := line[:2]
fmt.Println("front:", front, len(front), cap(front))
front = append(front, "X")
fmt.Println("append 후 front:", front)
fmt.Println("append 후 line:", line)
line = newLine()
capped := line[:2:2]
capped = append(capped, "Y")
fmt.Println("capped:", capped)
fmt.Println("line:", line)
}
func stationMap() {
stock := map[string]int{"강변": 5, "시청": 0, "역앞": 3}
n, ok := stock["공원"]
fmt.Println("공원:", n, ok)
n, ok = stock["시청"]
fmt.Println("시청:", n, ok)
if n, ok := stock["역앞"]; ok {
fmt.Println("역앞 있음:", n)
}
delete(stock, "시청")
fmt.Println("삭제 후 개수:", len(stock))
rentals := []string{"강변", "역앞", "강변", "시청", "강변", "역앞"}
counts := make(map[string]int)
for _, s := range rentals {
counts[s]++
}
for _, name := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(name, counts[name])
}
returned := make(map[string][]string)
returned["강변"] = append(returned["강변"], "B01")
returned["강변"] = append(returned["강변"], "B07")
returned["역앞"] = append(returned["역앞"], "B03")
fmt.Println(returned["강변"], returned["역앞"], returned["시청"], len(returned["시청"]))
}
func helpers() {
minutes := []int{12, 45, 8, 30, 22}
sorted := slices.Clone(minutes)
slices.Sort(sorted)
fmt.Println(minutes, sorted)
fmt.Println(slices.Max(minutes), slices.Contains(minutes, 30), slices.Index(minutes, 8))
pos, found := slices.BinarySearch(sorted, 22)
fmt.Println(pos, found)
fees := map[string]int{"강변": 1000, "역앞": 1500}
feesCopy := maps.Clone(fees)
feesCopy["역앞"] = 2000
fmt.Println(fees["역앞"], feesCopy["역앞"], maps.Equal(fees, feesCopy))
}
줄별 해설
arrayCopy
other := docks 는 배열 네 칸을 통째로 복사한다. other[0] 을 바꿔도 docks 는 그대로이므로 출력의 첫 줄과 둘째 줄이 다르다. 배열의 길이는 4 로 고정이다.
growth
var minutes []int 는 nil 슬라이스를 만든다. minutes == nil 은 true 이고 길이와 용량은 0 이다. 반복문은 10 부터 90 까지 아홉 개를 하나씩 붙이는데, 용량이 바뀐 순간에만 길이와 용량을 출력한다. 그래서 아홉 번의 append 중 다섯 번만 출력이 나온다. lastCap 은 바로 앞에서 본 용량을 기억해 두는 변수다. 이 출력은 Go 1.25 에서 확인한 값이며, 앞에서 말했듯 이 증가 규칙에 기대는 코드를 쓰지는 않는다.
newLine 과 sharing
newLine 은 호출할 때마다 새 슬라이스를 만들어 준다. 실험을 두 번 하려고 원본을 새로 준비하는 용도다. front := line[:2] 의 길이는 2, 용량은 5 다. append(front, "X") 는 용량이 남았으므로 같은 배열의 세 번째 칸을 "X" 로 덮어쓴다. 그래서 line 의 세 번째 원소도 "X" 로 바뀐다.
이어서 line 을 새로 만들고 line[:2:2] 로 자르면 길이 2, 용량 2 가 된다. 여기에 append 하면 용량이 모자라서 새 배열이 만들어진다. 새 배열에 "Y" 가 들어가고 line 은 다섯 원소를 그대로 유지한다.
stationMap
stock["공원"] 은 없는 키이므로 0 과 false 를 돌려준다. stock["시청"] 은 실제로 값 0 이 들어 있으므로 0 과 true 를 돌려준다. 두 경우 값은 같지만 ok 가 다르다. if n, ok := ...; ok 는 조건문 안에서만 쓰는 변수를 선언하는 모양이다. delete 로 "시청" 을 지우면 키가 두 개만 남는다.
이어서 counts[s]++ 로 대여소별 횟수를 센다. 맵 순회 순서는 정해져 있지 않으므로 slices.Sorted(maps.Keys(counts)) 로 키를 정렬한 뒤 그 순서대로 출력한다. 문자열은 바이트 순서로 비교되며 "강변", "시청", "역앞" 이 이 순서로 나온다. 마지막 returned 는 값이 슬라이스인 맵이다. 없는 키 "시청" 을 읽으면 nil 슬라이스가 나오고 Println 은 이것을 빈 대괄호로 출력하며 길이는 0 이다.
helpers
slices.Clone 으로 복사한 뒤 복사본만 정렬하므로 minutes 의 원래 순서는 유지된다. slices.Max 는 45, Contains 는 30 이 있으니 true, Index 는 8 이 세 번째 자리인 번호 2 에 있다고 알려 준다. BinarySearch 는 정렬된 sorted 에서 22 를 찾아 번호 2 와 true 를 돌려준다. 마지막으로 maps.Clone 으로 복제한 맵을 고치면 원본은 그대로이므로 값이 1500 과 2000 으로 갈리고, maps.Equal 은 false 가 된다.
실행 결과
$ go run main.go
배열 원본: [A1 A2 A3 A4]
배열 복사: [B1 A2 A3 A4]
배열 길이: 4
nil 슬라이스: true 0 0
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=5 cap=8
len=9 cap=16
[10 20 30 40 50 60 70 80 90]
front: [B01 B02] 2 5
append 후 front: [B01 B02 X]
append 후 line: [B01 B02 X B04 B05]
capped: [B01 B02 Y]
line: [B01 B02 B03 B04 B05]
공원: 0 false
시청: 0 true
역앞 있음: 3
삭제 후 개수: 2
강변 3
시청 1
역앞 2
[B01 B07] [B03] [] 0
[12 45 8 30 22] [8 12 22 30 45]
45 true 2
2 true
1500 2000 false
실무에서 자주 틀리는 것
함수 안에서 append 하고 결과를 돌려주지 않는다
슬라이스를 함수에 넘기면 헤더가 복사된다. 함수 안의 append 는 함수 안의 헤더만 바꾸므로, 호출한 쪽의 길이는 그대로다.
// 틀린 코드
func addMinute(m []int) {
m = append(m, 10)
}
minutes := []int{5}
addMinute(minutes)
fmt.Println(minutes) // [5]
// 고친 코드
func addMinute(m []int) []int {
return append(m, 10)
}
minutes := []int{5}
minutes = addMinute(minutes)
fmt.Println(minutes) // [5 10]
append 와 같은 규칙을 따라 결과를 돌려주고, 호출한 쪽에서 변수에 다시 담는다.
하위 슬라이스에 append 해서 원본을 덮어쓴다
// 틀린 코드
queue := []string{"B01", "B02", "B03", "B04"}
check := queue[:2]
check = append(check, "점검완료")
fmt.Println(queue) // [B01 B02 점검완료 B04]
// 고친 코드
queue := []string{"B01", "B02", "B03", "B04"}
check := slices.Clone(queue[:2])
check = append(check, "점검완료")
fmt.Println(queue) // [B01 B02 B03 B04]
원본과 완전히 분리하고 싶으면 Clone 을 쓴다. 자르기만 하고 원본을 건드리지 않을 생각이라면 queue[:2:2] 도 방법이지만, 원소를 고칠 때는 여전히 원본이 함께 바뀐다는 점을 잊지 않는다.
nil 맵에 값을 쓴다
nil 슬라이스에는 append 할 수 있지만 nil 맵에는 쓸 수 없다. 읽기는 되지만 쓰기는 실행 중에 패닉(panic, 프로그램이 즉시 멈추는 오류)을 일으킨다.
// 틀린 코드
var stock map[string]int
stock["강변"] = 3 // panic: assignment to entry in nil map
// 고친 코드
stock := make(map[string]int)
stock["강변"] = 3
맵은 선언만으로는 쓸 수 없고 make 나 리터럴로 만들어야 한다.
값이 0 인 것을 없는 것으로 착각한다
// 틀린 코드
if stock["시청"] == 0 {
fmt.Println("등록되지 않은 대여소")
}
// 고친 코드
if _, ok := stock["시청"]; !ok {
fmt.Println("등록되지 않은 대여소")
}
틀린 코드는 시청이 등록되어 있지만 재고가 0 일 때도 "등록되지 않았다"고 출력한다. 존재 여부는 반드시 두 번째 반환값으로 확인한다.
한눈에 보기
| 하고 싶은 일 | 쓰는 것 | 주의할 점 |
|---|---|---|
| 고정 길이 묶음 | [4]string | 대입하면 전체가 복사됨 |
| 늘어나는 목록 | s = append(s, v) | 결과를 반드시 다시 담음 |
| 길이와 용량 | len(s), cap(s) | 용량 값에 의존하지 않음 |
| 부분 잘라내기 | s[low:high] | 원본과 배열을 공유함 |
| 공유 끊기 | s[:n:n], slices.Clone | 원소 수정 공유는 Clone 으로만 끊김 |
| 키로 찾기 | v, ok := m[k] | 없는 키는 제로값 |
| 맵 만들기 | make(map[K]V) | nil 맵에는 쓰지 못함 |
| 순서대로 순회 | slices.Sorted(maps.Keys(m)) | 맵 순회 순서는 불규칙 |
이 장에서 다룬 자료구조의 공식 정의가 궁금하면 Go 언어 명세의 슬라이스 부분을 참고한다.
연습 문제
- 다음 코드의 출력을 예상해 보자.
a := []int{1, 2, 3, 4} b := a[1:3] b[0] = 99 fmt.Println(a, len(b), cap(b)) - 반납 기록
[]string{"강변", "역앞", "강변"}로 대여소별 횟수를 세어 이름 순서로 출력하고, "시청" 은 기록이 없다고 출력하는 코드 조각을 써 보자. - 다음 코드가 패닉을 일으키는 이유를 설명하고 고쳐 보자.
var stock map[string]int stock["강변"] = 3 s := []int{1, 2, 3, 4, 5},head := s[:2]가 있을 때,head = append(head, 100)이s를 바꾸지 않도록 한 줄을 고쳐 보자. 방법은 두 가지 이상 쓴다.
정답과 해설
1. 출력은 [1 99 3 4] 2 3 이다. b 는 a 의 두 번째 칸부터 시작하므로 b[0] 은 a[1] 과 같은 칸이다. 길이는 3 - 1 = 2, 용량은 원래 용량 4 에서 시작 번호 1 을 뺀 3 이다.
2. 예시 답이다.
counts := make(map[string]int)
for _, s := range []string{"강변", "역앞", "강변"} {
counts[s]++
}
for _, name := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(name, counts[name])
}
if _, ok := counts["시청"]; !ok {
fmt.Println("시청 기록 없음")
}
출력은 "강변 2", "역앞 1", "시청 기록 없음" 이다. 없는 키의 값은 0 이라 counts["시청"] == 0 으로도 이번에는 맞지만, 존재 여부는 ok 로 확인하는 습관이 안전하다.
3. 선언만 한 맵은 nil 이어서 쓸 수 있는 저장 공간이 없다. stock := make(map[string]int) 로 만들거나 map[string]int{} 리터럴을 쓰면 된다. 읽기는 nil 맵에서도 가능하지만 쓰기는 패닉이다.
4. 방법 하나는 용량을 제한하는 것이다: head := s[:2:2]. 이러면 append 가 새 배열을 만든다. 다른 방법은 복제이다: head := slices.Clone(s[:2]). 둘 다 append 가 s 의 세 번째 칸을 덮어쓰지 않는다. 다만 이후 head[0] = 7 처럼 원소를 직접 고칠 때, 첫 번째 방법은 (append 전이라면) s 도 함께 바뀌고 Clone 은 바뀌지 않는다.
다음 장에서는 이 슬라이스와 맵에 담기는 값 자체를 더 풍부하게 만든다. 대여소나 자전거처럼 여러 항목을 가진 데이터를 하나로 묶는 구조체와, 그 데이터에 붙는 메서드를 다룬다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.