메뉴

문서정보

GoLang 포인터 101

목차

Pointer

포인터라는 개(멍멍이)가 있다. 이 개는 사냥감의 위치를 가리키는 일을 한다. 위치 정보를 알려주는 녀석이라고 보면 되겠다.

 멍멍이-포인터

소프트웨어 개발에서 포인터도 마찬가지다. 소프트웨어의 가장 중요한 일은 데이터를 읽고 쓰는 것인데, 이 데이터들은 메모리에 위치한다. 데이터가 저장된 메모리의 주소를 우리는 메모리 주소라고 하며, 0x123456 과 같은 16진수로 표현한다. 따라서 이론적으로 메모리 주소를 알고 있으면 모든 데이터를 조작 할 수 있다. 하지만 인간이 16진수 값으로 데이터를 조작하기는 쉽지 않으니, 변수로 한단계 추상화해서 사용한다. 0x12345 에 "yundream@example.com"을 저장하라는 것 보다 email에 "yundream@example.com"을 저장하라는게 훨씬 쉽다.

변수에는 값을 저장하는 일반변수와 값이 저장된 데이터의 주소를 저장하는 포인터가 있다. 아래 그림을 보자.

 변수와 메모리

변수 "a"는 int 형 값인 10 이 저장되어 있다. 값이 저장된 메모리 주소는 0x0001 이다.

포인터 "p"는 메모리 주소 0x0001이 저장되어 있다. 0x0001에는 10이 저장되어 있으므로 포인터 p를 이용해서 값을 읽을 수 있다. 메모리 주소에 있는 값의 타입을 알아야 하기 때문에 포인터도 데이터 타입을 가진다.

선언과 사용

아래 코드를 실행해보자.
package main

import "fmt"

func main() {
	var a int = 56
	var p *int = &a

	fmt.Println(a)
	fmt.Println(p)
	fmt.Println(*p)
}
대략 아래와 같은 결과가 나올 것이다.
56
0xc000016110
56

이제 코드를 분석해보자. 위 코드에서 포인터 p는 a의 주소를 가리킨다. 따라서 a의 값을 변경하면 포인터 p 역시 변경된 값을 읽게 된다.
package main

import "fmt"

func main() {
	var a int = 56
	var p *int = &a

	fmt.Println(a)   // 56
	fmt.Println(*p)  // 56

	a = 100
	fmt.Println(*p) // 100
}

마찬가지로 포인터가 가리키는 주소에 값을 넣을 수도 있다.
func main() {
	var a int = 56
	var p *int = &a

	fmt.Println(a)    // 56
	fmt.Println(*p)   // 56

	*p = 1000
	fmt.Println(a)    // 1000
	fmt.Println(*p)   // 1000
}

내장 함수인 new()를 이용해서 포인터를 선언 할 수 있다.
func main() {
	var p1 *int = new(int)
	p2 := new(int)

	fmt.Println(*p1)  // 0
	fmt.Println(*p2)  // 0
}
초기화 하지 않은 포인터는 각 데이터 타입의 Zero value로 설정된다. int는 0, 스트럭처(struct)는 nil, float는 0 이런 식이다.

스트럭처와 포인터

스트럭처의 포인터도 다른 포인터와 다를 바가 없다. 아래 코드를 보자.
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func setAge(u User, age int) {
	u.Age = age
	fmt.Printf("func> %p: %#v\n", &u, u)
}

func main() {
	yundream := User{
		Name: "yundream",
		Age:  35,
	}

	setAge(yundream, 40)
	fmt.Printf("main> %p: %#v\n", &yundream, yundream)
}
출력을 하면 대략 아래와 같은 결과가 나올 것이다.
func> 0xc00018c018: main.User{Name:"yundream", Age:40}
main> 0xc00018c000: main.User{Name:"yundream", Age:35}

프로그램의 실행시 동적으로 메모리를 확보하는 영역으로 스택과 힙이 있다. 스택 메모리는 함수 호출 스택을 저장하고 로컬변수, 인수, 반환 값이 저장된다. 따라서 포인터가 아닌 값을 함수의 인수로 하게 되면, 스택에 새로운 메모리공간이 만들어지고 여기에 데이터가 복사된다. 이 데이터는 함수안에서만 유효하다. 코드 실명결과를 보면 메모리 주소가 다른 걸 확인 할 수 있다. 메모리 주소가 다르기 때문에 데이터도 서로 다르다.

이걸 포인터로 변경해보자.
package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func setAge(u *User, age int) {
	u.Age = age
	fmt.Printf("func> %p: %#v\n", u, u)
}

func main() {
	yundream := &User{
		Name: "yundream",
		Age:  35,
	}

	setAge(yundream, 40)
	fmt.Printf("main> %p: %#v\n", yundream, yundream)

	setAge(yundream, 100)
	fmt.Printf("main> %p: %#v\n", yundream, yundream)
}
실행결과는 대략 아래와 같다.
func> 0xc00000c030: &main.User{Name:"yundream", Age:40}
main> 0xc00000c030: &main.User{Name:"yundream", Age:40}
func> 0xc00000c030: &main.User{Name:"yundream", Age:100}
main> 0xc00000c030: &main.User{Name:"yundream", Age:100}
main 함수와 setAge 함수 모두 동일한 메모리 영역을 읽고/쓰기 때문에 동일한 결과를 확인 할 수 있다.

포인터와 리시버

Golang에서는 스트럭처가 클래스의 역할을 수행한다. 스트럭처는 클래스와 마찬가지로 메서드를 가지는데, Java, C++ 과는 약간 다르게 메서드를 선언한다.
type User struct {
	Name string
	Age  int
}

func (u User) setAge(age int) {
	u.Age = age
}

func (u User) printAge() {
}
스트럭처안에 메서드를 두는게 아니고 스트럭처 바깥에 메서드를 두고 리시버(receiver)를 이용해서 이 함수가 어떤 스트럭처의 메서드인지를 선언한다. 이 리시버도 포인트 타입이 있다.
type User struct {
	Name string
	Age  int
}

func (u User) setAge(age int) {
	u.Age = age
}

func (u User) printAge() {
}

 리시버

리시버는 값 리시버(Value receiver)과 포인트 리시버(Point receiver)이 있다.

객체지향 언어의 Call by referenceCall by value와 차이가 없다. 포인터 리시버는 Call by reference 하겠다는 거다. 아래 예제를 보자.
type User struct {
	Name string
	Age  int
}

func (u User) setAge(age int) {
	u.Age = age
	fmt.Printf("SET > %#v\n", u)
}

func (u User) printAge() {
	fmt.Printf("PRN > %#v\n", u)
}

func main() {
	yundream := User{
		Name: "yundream",
		Age:  35,
	}
	yundream.printAge()

	yundream.setAge(40)
	yundream.printAge()
}
실행결과다.
PRN > main.User{Name:"yundream", Age:35}
SET > main.User{Name:"yundream", Age:40}
PRN > main.User{Name:"yundream", Age:35}
setAget를 이용해서 40을 할당했으나 값 리시버를 사용했기 때문에, 메서드 안에서만 값이 유효하다. 스트럭처의 값을 변경하고 싶다면 포인터 리시버를 사용해야 한다.

func (u *User) setAge(age int) {
	u.Age = age
	fmt.Printf("SET > %#v\n", u)
}
다시 실행해보자.
PRN > main.User{Name:"yundream", Age:35}
SET > &main.User{Name:"yundream", Age:40}
PRN > main.User{Name:"yundream", Age:40}

인터페이스와 포인터

인터페이스(interface)는 Go에서 제공하는 훌륭한 개며이며, 다형성을 구현하는 유일한 방법이기도 하다.

Go 에서 인터페이스는 스트럭처가 구현 해야 하는 메서드 시그니처의 집합이다. 객체지향에 익숙하다면 인터페이스를 구현하기 위해서 implement키워드를 사용했을 것이다. Go 에서는 명시적으로 implement 키워드를 선언하지 않고 구현할 수 있다. 그냥 해당 구조체가 메서드를 구현했다면 implement 한 것으로 간주한다.

개발자는 인터페이스의 메서드를 구현 할 때, 값 리시버와 포인트 리시버를 사용 할 수 있는데 이때 주의해야 할 사항이 있다. 아래와 같은 인터페이스가 있다고 가정해보자.
type animal interface {
	breathe()
	walk()
}
이제 lion을 구현해 보자.
package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l lion) breathe() {
	fmt.Printf("%d > Lion breathes\n", l.age)
}

func (l lion) walk() {
	fmt.Printf("%d > Lion walk\n", l.age)
}

func main() {
	var a animal

	a = lion{age: 10}
	a.breathe()
	a.walk()

	a = &lion{age: 5}
	a.breathe()
	a.walk()
}
실행 결과
10 > Lion breathes
10 > Lion walk
5 > Lion breathes
5 > Lion walk

이제 포인터 리시버로 lion을 구현해보자.
package main

import "fmt"

type animal interface {
	breathe()
	walk()
}

type lion struct {
	age int
}

func (l *lion) breathe() {
	fmt.Printf("%d > Lion breathes\n", l.age)
}

func (l *lion) walk() {
	fmt.Printf("%d > Lion walk\n", l.age)
}

func main() {
	var a animal

	a = lion{age: 10}
	a.breathe()
	a.walk()

	a = &lion{age: 5}
	a.breathe()
	a.walk()
}
a = lion{age: 10} 에서 아래와 같은 에러가 발생할 것이다.
cannot use (lion literal) (value of type lion) as animal value in assignment: missing method breathe (breathe has pointer receiver)compilerInvalidIfaceAssign

참고