[Golang] Pointer

2021-10-25 hit count image

Golang에서 포인터(Pointer)를 사용하여 변수의 메모리 주소값을 다루는 방법을 알아보도록 하겠습니다.

개요

이번 블로그 포스트에서는 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에서는 포인터 변수가 많이 활용되므로, 이 부분을 잘 기억해둡시다.

제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!

책 홍보

스무디 한 잔 마시며 끝내는 React Native 책을 출판한지 벌써 2년이 다되었네요.
이번에도 좋은 기회가 있어서 스무디 한 잔 마시며 끝내는 리액트 + TDD 책을 출판하게 되었습니다.

아래 링크를 통해 제가 쓴 책을 구매하실 수 있습니다.
많은 분들에게 도움이 되면 좋겠네요.

스무디 한 잔 마시며 끝내는 React Native, 비제이퍼블릭
스무디 한 잔 마시며 끝내는 리액트 + TDD, 비제이퍼블릭
Posts