Devin.KR

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 는 같은 배열 다섯 칸을 보고 있으며, front 에 append 하면 line 의 세 번째 칸이 바뀐다.

그림처럼 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) 이 관용적인 모양이다. 새 배열이 만들어졌다면 원래 변수는 여전히 옛 배열을 가리키고 있기 때문이다.

원소를 하나씩 append 하면 용량이 1, 2, 4, 8, 16 으로 커지고, 용량이 찰 때만 새 배열로 옮겨 간다.

비어 있는 슬라이스에 원소를 하나씩 붙이면 용량은 대체로 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, 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 언어 명세의 슬라이스 부분을 참고한다.

연습 문제

  1. 다음 코드의 출력을 예상해 보자.
    a := []int{1, 2, 3, 4}
    b := a[1:3]
    b[0] = 99
    fmt.Println(a, len(b), cap(b))
    
  2. 반납 기록 []string{"강변", "역앞", "강변"} 로 대여소별 횟수를 세어 이름 순서로 출력하고, "시청" 은 기록이 없다고 출력하는 코드 조각을 써 보자.
  3. 다음 코드가 패닉을 일으키는 이유를 설명하고 고쳐 보자.
    var stock map[string]int
    stock["강변"] = 3
    
  4. 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

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

댓글을 남기려면 로그인이 필요합니다.