다른 많은 언어들처럼 Go에도 포인터가 있어요. 포인터가 처음이라면 조금 어렵게 느껴질 수도 있지만, 익숙해지면 아주 단순해요. 포인터는 Go에서 아주 중요한 부분이니, 시간을 들여 제대로 이해해 봐요.
자세한 내용을 파고들기 전에, 포인터가 어디에 쓰이는지 이해해 두면 좋아요. 포인터는 프로그램의 다른 부분과 메모리를 공유하는 방법인데, 크게 두 가지 이유에서 유용해요:
일반적인 정수 변수 a가 있다고 해봐요:
var a int
변수를 선언하면 Go는 값을 저장할 메모리 공간을 찾아야 해요. 이 과정은 대부분 우리에게 감춰져 있어요. 그 메모리 공간에 저장된 값을 가져와야 할 때는 그냥 변수 이름으로 가리키면 돼요.
예를 들어 a + 2라고 쓰면, 변수 a와 연결된 메모리에 저장된 값을 가져와서 2를 더하는 것과 같아요.
마찬가지로 a의 메모리 공간에 있는 값을 바꾸고 싶을 때는 변수 이름을 사용해서 값을 할당할 수 있어요:
a = 3
이제 a와 연결된 메모리 공간에는 값 3이 저장돼요.
변수를 사용하면 메모리에 있는 값들을 가리킬 수 있지만, 때로는 변수가 가리키는 메모리 주소를 아는 것이 유용해요. 포인터는 그 값들의 메모리 주소를 담고 있어요. 포인터 타입의 변수는 기본 타입 앞에 별표를 붙여서 선언해요:
var p *int // 'p' contains the memory address of an integer
여기서는 "int에 대한 포인터"(*int) 타입의 변수 p를 선언해요.
즉, p에는 정수의 메모리 주소가 담겨요.
포인터의 제로 값은 nil인데, nil 포인터는 메모리 주소를 담고 있지 않기 때문이에요.
변수 값의 메모리 주소를 찾으려면 & 연산자를 사용하면 돼요.
예를 들어 변수 a의 메모리 주소를 찾아서 포인터 p에 저장하고 싶다면 다음과 같이 하면 돼요:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
포인터가 있을 때, 그 포인터가 나타내는 메모리 주소에 저장된 값을 알고 싶을 수 있어요.
이때는 * 연산자를 사용하면 돼요:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
*p 연산은 p에 저장된 메모리 주소에 있는 값을 가져와요.
이 연산을 흔히 "역참조"라고 불러요.
역참조 연산자를 사용해서 포인터가 가리키는 메모리 주소에 새로운 값을 할당할 수도 있어요:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
*pa에 값을 할당하면 pa가 담고 있는 메모리 주소에 저장된 값이 바뀌어요.
pa가 a의 메모리 주소를 담고 있으니, *pa에 값을 할당하는 것은 곧 a의 값을 바꾸는 것과 같아요!
다만 주의할 점이 있어요. 역참조하기 전에 포인터가 nil이 아닌지 항상 확인해요.
nil 포인터를 역참조하면 프로그램이 실행 중에 충돌해요!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
지금까지는 기본 값에 대한 포인터만 봤어요. 구조체에 대한 포인터도 만들 수 있어요:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
새로운 Person을 만들면서 바로 포인터를 저장할 수도 있어요:
var p *Person
p = &Person{Name: "Peter", Age: 22}
구조체에 대한 포인터가 있을 때는 필드에 접근하기 전에 포인터를 역참조할 필요가 없어요:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
슬라이스와 맵은 구현 안에 이미 포인터를 가지고 있어서 특별한 타입이에요. 그래서 이런 타입의 값에 대한 메모리 주소를 공유하려고 포인터를 만들 필요가 없는 경우가 대부분이에요. 맵의 키 값을 증가시키는 함수가 있다고 해봐요:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
맵을 만들고 이 함수를 호출하면, 함수가 맵에 가한 변경 사항은 함수가 끝난 뒤에도 그대로 남아 있어요. 포인터를 사용했을 때와 비슷한 동작이지만, 이 예시에서는 참조나 역참조, 또는 어떤 포인터 문법도 사용하지 않았다는 점을 눈여겨봐요:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
슬라이스에 이미 있는 요소를 바꿀 때도 마찬가지예요.
하지만 append처럼 새로운 슬라이스를 반환하는 동작은 특별한 경우라서, 함수 바깥의 슬라이스를 바꾸지 않을 수도 있어요.
이는 슬라이스가 내부적으로 동작하는 방식 때문인데, 좀 더 심화된 주제라서 이 연습 문제에서는 자세히 다루지 않아요.
정말 궁금하다면 Go 블로그: 'append'의 동작 원리에서 더 읽어볼 수 있어요.
여러분 근처의 한 지역 학교에는 아주 활발한 학생회가 있어요. 학생회는 회장이 이끌고 있으며, 2년마다 새로운 회장을 뽑는 선거가 열려요.
올해 선거에서는 표를 세는 새로운 디지털 시스템이 필요하기로 했어요. 학교는 이 새로운 시스템을 만드는 데 여러분의 도움이 필요해요.
새 투표 시스템에 가장 먼저 필요한 것 중 하나는 득표 카운터예요. 이 카운터로 특정 후보가 받은 표를 관리할 수 있어요.
후보의 초기 득표 수를 받아, 주어진 초기 득표 수로 초기화된 int를 가리키는 포인터를 반환하는 함수 NewVoteCounter를 만들어요.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
이제 새 카운터를 만들 수 있게 됐어요! 그런데 새 시스템에는 카운터에서 득표 수를 가져오는 방법도 필요해요.
카운터(*int)를 인자로 받아 카운터의 득표 수를 반환하는 함수 VoteCount를 만들어요.
카운터가 nil이면 득표 수가 없는 것으로 간주해요:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
드디어 표를 셀 시간이에요! 이제 카운터의 득표 수를 늘리는 방법이 필요해요.
카운터(*int)와 득표 수를 인자로 받아, 그만큼 카운터를 증가시키는 함수 IncrementVoteCount를 만들어요.
전달되는 포인터는 절대 nil이 아니라고 가정해도 돼요.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
이제 모든 표를 세었으니, 학교 전체에 결과를 발표할 준비를 할 시간이에요. 그런데 득표용 카운터만으로는 부족하다는 걸 알게 돼요. 득표 수를 특정 후보와 연결할 방법이 필요해요.
후보의 이름과 득표 수를 받아 새로운 선거 결과를 반환하는 함수 NewElectionResult를 만들어요.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
선거 결과 struct는 이미 만들어져 있고, 다음과 같이 정의되어 있어요:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
학교에 새로운 회장을 발표할 시간이에요!
회장은 학교에 있는 작은 디지털 전광판을 통해 발표돼요.
메시지는 새 회장의 이름과 그가 받은 득표 수를 다음 형식으로 보여줘야 해요: <candidate_name> (<votes>).
이런 메시지의 예시는 "Peter (51)"예요.
*ElectionResult를 인자로 받아 표시할 메시지를 담은 문자열을 반환하는 함수 DisplayResult를 만들어요.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
최종 결과가 정확한지 확인하기 위해 표를 다시 셌어요. 다시 세는 과정에서 일부 후보의 득표 수가 하나씩 어긋나 있다는 사실이 밝혀졌어요.
최종 결과와 득표 수를 줄여야 할 후보의 이름을 받아, 그 후보의 득표 수를 감소시키는 함수 DecrementVotesOfCandidate를 만들어요.
최종 결과는 map[string]int 형태로 주어지고, 키는 후보의 이름, 값은 해당 후보의 총 득표 수예요.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9
Exercism에 가입하고 Go 트랙을 개념 34개연습 문제 165개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.