개요
이번 블로그 포스트에서는 Golang에서 Pointer
(포인터)를 사용하여 변수의 메모리 주소를 다루는 방법에 대해서 살펴보려고 합니다. 이 블로그 포스트에서 소개하는 코드는 다음 링크를 통해 확인하실 수 있습니다.
포인터
포인터(Pointer)는 메모리 주소를 값으로 갖는 타입입니다. Golang에서 포인터는 다음과 같이 선언할 수 있습니다.
var 변수명 *타입
var p *int
이렇게 선언한 포인터 변수는 다른 변수의 주소값을 저장할 수 있으며, 다른 변수의 주소값은 &
연산자를 사용하여 접근할 수 있습니다.
var a int
var p *int
p = &a
*p = 20
이때, 서로 타입이 서로 맞아야하며, 타입이 다른 경우 컴파일 에러가 발생합니다.
Golang에서 포인터(Pointer)를 다루는 방법을 확인하기 위해, main.go
파일을 생성하고 다음과 같이 수정합니다.
package main
import "fmt"
func main() {
var a int = 10
var p *int
fmt.Println(a)
p = &a
fmt.Printf("%v\n", &a)
fmt.Printf("%v\n", p)
*p = 20
fmt.Println(a)
fmt.Println(*p)
}
이를 실행하면 다음과 같은 결과를 볼 수 있습니다.
# go run main.go
10
0xc00001a0e8
0xc00001a0e8
20
20
실행 결과를 보면, 변수 a
와 포인터 변수 p
의 메모리 주소값이 같음을 알 수 있으며, 포인터 변수의 값을 변경하였을 때, 동일한 메모리 주소의 변수인 a
값이 변경되는 것을 알 수 있습니다.
포인터 변수의 기본값
Golang에서 변수를 선언하고 값을 할당하지 않으면, 변수 타입의 기본값이 할당됩니다. 하지만 포인터 변수는 메모리 주소값을 할당하는 변수이므로 값을 할당하지 않으면 nil
이 할당되게 됩니다.
이를 확인하기 위해서 main.go
파일을 다음과 같이 수정합니다.
package main
import "fmt"
func main() {
var p *int
fmt.Println(p)
}
이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
# go run main.go
<nil>
그러므로 포인터 변수를 사용할 때, 다음과 같이 변수가 할당되었는지 확인할 수 있습니다.
var p *int
if p != nil {
fmt.Println("Assigned")
}
함수에서 포인터
Golang에서는 다음과 같이 포인터 변수를 활용할 수 있습니다.
package main
import "fmt"
type Data struct {
value int
data [200]int
}
func ChangeData(arg Data) {
arg.value = 100
arg.data[100] = 999
}
func ChangePData(arg *Data) {
arg.value = 100
arg.data[100] = 999
}
func main() {
var data Data
ChangeData(data)
fmt.Printf("value = %d\n", data.value)
fmt.Printf("data[100] = %d\n", data.data[100])
ChangePData(&data)
fmt.Printf("value = %d\n", data.value)
fmt.Printf("data[100] = %d\n", data.data[100])
}
이를 실행하면 다음과 같은 결과를 확인할 수 있습니다.
# go run main.go
value = 0
data[100] = 0
value = 100
data[100] = 999
함수에 포인터 변수를 사용하면, 전달받은 변수의 값을 함수에서 직접 변경할 수 있습니다.
구조체 포인터 초기화
Golang에서는 구조체(Struct) 포인터는 다음과 같이 초기화를 할 수 있습니다.
package main
import "fmt"
type Data struct {
value int
data [200]int
}
func main() {
var data Data
var p1 *Data = &data
var p2 *Data = &Data{}
fmt.Println(*p1)
fmt.Println(*p2)
}
인스턴스
메모리에 할당된 데이터의 실체를 인스턴스(instance)
라고 합니다.
new 내장 함수
다음과 같이 new
내장 함수를 사용하여 구조체의 인스턴스를 생성할 수 있습니다.
p1 := &Data{}
var p2 = new(Data)
여기서 p1
, p2
는 포인터 변수이며, *Data
타입이다.
인스턴스가 사라지는 시점
Golang에도 가비지 콜렉터(Garbage Collector)가 존재하며, 인스턴스를 참조하는 변수가 모두 사라지면 자동으로 제거됩니다.
- 허상 포인터(Dangling Pointer): 포인터 변수가 더이상 유효하지 않은 메모리를 참조할 때, 발생하는 에러를 말합니다.
package main
import "fmt"
type User struct {
Name string
Age int
}
func NewUser(name string, age int) *User {
var u = User{name, age}
return &u
}
func main() {
userPointer := NewUser("John", 20)
fmt.Println(userPointer)
fmt.Println(userPointer.Age)
fmt.Println(userPointer.Name)
}
C, C++ 관점에서 보면 변수 u는 NewUser함수의 중괄호 안에 있으므로 함수 호출이 끝나는 시점(}
)에서 변수가 사라지게 됩니다. 따라서 사라진 변수의 주소를 넘기고 있으므로 해당 주소를 전달받아 사용하는 userPointer
포인터 변수는 허상 포인터(Dangling Pointer) 에러가 발생하게 됩니다.
func NewUser(name string, age int) *User {
var u = User{name, age}
return &u
}
하지만, Golang에서는 main함수 안에서 userPointer 포인터 변수가 변수 u의 메모리 주소를 참조하고 있기 때문에 u 변수의 인스턴스가 사라지지 않고 계속 유지가 되므로 허상 포인터(Dangling Pointer) 에러가 발생하지 않습니다. Golang은 Escape Analysis
을 사용하여, 함수를 벗어나는 변수들을 검사하여 힙 메모리에 저장하기 때문에 이런 문제가 발생하지 않습니다.
완료
이것으로 Golang에서 포인터(Pointer)를 사용하여 메모리 주소를 참조하고 해당 메모리 주소의 값을 사용하는 방법에 대해서 알아보았습니다. 내용을 조금 요약하면 다음과 같습니다.
- 인스턴스는 메모리에 생성된 데이터의 실체이며, 포인터를 이용해서 인스턴스를 가리키게 할 수 있다.
- 함수 호출시 포인터 인수를 통해서 인스턴스를 입력받고, 함수 안에서 그 값을 수정할 수 있다.
- 쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 삭제한다.
Golang에서는 포인터 변수가 많이 활용되므로, 이 부분을 잘 기억해둡시다.
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku
가 개발한 앱을 한번 사용해보세요.Deku
가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.