포인터는 메모리의 주소를 저장하는 변수 타입입니다. 아파트의 동호수처럼 변수의 번지수라고 봐도 무방할 것 같습니다.
포인터 선언
포인터 변수는 가리키는 데이터 타입 앞에 * 연산자를 붙여서 선언합니다.
var a *int var b *string var c *float
그리고 어떤 변수의 주솟값을 알고 싶으시면 & 연산자를 붙여서 주솟값을 알아올 수 있습니다.
var a int var p *int p = &a
또한, 포인터가 가리키는 변수의 값을 얻어올 수도 있습니다. 이때는 포인터 앞에 * 연산자를 붙여서 사용합니다.
var a int var p *int p = &a *p = 10
그러면 이것으로 예제를 실행해보겠습니다.
package main import "fmt" func main() { var a int = 10 var pA *int pA = &a fmt.Printf("a값 = %d\n", a) fmt.Printf("pA값 = %p\n", pA) fmt.Printf("pA가 가리키는 값(*pA) = %d\n", *pA) *pA = 100 fmt.Printf("pA가 가리키는 값(*pA) = %d\n", *pA) fmt.Printf("a가 가리키는 값 = %d\n", a) }
- a는 int 형의 변수입니다.
- pA는 int형의 포인터 변수입니다.
- pA에 a의 주소를 저장했습니다.
- pA가 가리키는 값은 10입니다.
- a의 값도 10입니다.
- pA가 가리키는 변수의 값을 100으로 바꿉니다.
- pA가 가리키는 변수의 값이 100으로 바뀌었습니다.
- pA는 a의 번지수가 저장되어 있기 때문에 a도 100으로 바뀌어 있습니다.
포인터의 기본값 nil
포인터 변숫값을 초기화 하지 않는다면 기본값은 nil입니다. nil은 ‘값이 없음’을 의미하고, 어떠한 메모리 주소도 가지고 있지 않다는 뜻입니다.
var p *int if p != nil { // p가 nil이 아니라는 얘기는 p가 유효한 메모리 주소를 가리킨다는 뜻이다. }
포인터를 쓰는 이유
Go언어에서 포인터 개념을 채용한 이유를 곰곰이 생각해보면 결국 설계의 단순화를 위해서인것 같습니다. 포인터는 배우기 까다롭고 어렵지만 개념을 이해하고 나면 프로그래밍을 할때 메모리를 정밀하게 컨트롤 할 수 있게 됩니다. Java나 C#은 정수, 실수 같은 기본 타입이 아니면 모두 참조변수로서, 동적할당으로 메모리 할당이 이루어지고, 포인터와 비슷한 개념의 참조 변수로 데이터를 관리합니다. 메모리 관리는 가상머신이 관리를 해주기 때문에 Managed Code라고 불리기도 합니다. 하지만 Go 언어에서는 구조체, 배열도 모두 값을 가지는 변수로 컨트롤 할 수 있고, 포인터를 이용하면 참조변수처럼 사용할 수도 있습니다.
Go 언어에서 Call by value, Call by reference를 어떻게 활용하는지 알아보겠습니다.
package main import "fmt" func main() { a := [5]int{1, 2, 3, 4, 5} fmt.Println("원본 : ", a) CallByValue(a) fmt.Println("원본 : ", a) fmt.Println() fmt.Println("원본 : ", a) CallByReference(&a) fmt.Println("원본 : ", a) } func CallByValue(b [5]int) { b[0] = 3 fmt.Println("b : ", b) } func CallByReference(pB *[5]int) { (*pB)[0] = 6 fmt.Println("*pB : ", (*pB)) }
C언어를 공부하신 분들은 함수의 인수로 배열을 전달할 때 주의깊게 보셔야 할 것 같습니다. C언어에서 배열의 이름은 그 배열의 시작 주소값이지만 Go 언어에서는 그렇지 않습니다. 다른 타입의 변수들과 동일한 개념으로 사용됩니다.
원본 a 배열을 값에 의한 전달, 참조에 의한 전달을 했을 경우를 보여줍니다. 값에 의한 전달은 배열이 모두 복사가 되어서 전달됩니다. 하지만 참조에 의한 전달은 포인터를 이용해 주소값만 전달을 합니다. 만약 용량이 큰 데이터라면 값에 의한 전달 방식은 메모리 낭비가 일어나게 됩니다.
Go 언어에서는 이것을 세밀하게 컨트롤 가능하기 때문에 프로그램의 성능을 크게 끌어올릴 수 있습니다.
인스턴스
인스턴스란 메모리에 할당된 데이터의 실체를 말합니다. 어떤 객체를 복사해서 똑같은 객체를 만들었을때,
이 두 객체는 “인스턴스가 다르다” 라고 표현할 수 있습니다.
다음 코드는 인스턴스가 하나인 경우를 나타냅니다.
var p1 *Data = &Data{} var p2 *Data = p1 var p3 *Data = p1
p1에서 인스턴스를 생성한 뒤에 p2, p3에 포인터변수에 p1의 주소값을 대입하였기 때문에 인스턴스는 계속 하나입니다.
다음의 경우는 인스턴스가 각각 있는 경우입니다.
var data1 Data var data2 Data = data1 var data3 Data = data1
위 경우에는 값이 복사되어 값이 같고 인스턴스가 다른 경우입니다. 이것은 data1, data2, data3가 각각 독립적이게 됩니다.
인스턴스 생성 방법
아래는 두가지의 인스턴스 생성 방법을 보여줍니다.
p1 := &Data{} // (1) &를 사용하는 초기화 var p2 = new(Data) // (2) new()를 사용하는 초기화
new() 내장 함수는 인수로 타입을 받습니다. 타입을 메모리에 할당하고 기본값으로 채워 그 주소를 반환합니다. (1)번 방식은 초기화 값을 지정해서 인스턴스를 생성할 수 있고, (2)번 방식은 초기화 값을 지정할 수 없습니다.
가비지 컬랙터 (Garbage Collector)
C/C++ 언어와는 다르게 Go 언어에서는 생성된 인스턴스는 사용을 다하면 가비지 컬랙터가 메모리 해제를 시켜줍니다. 보통 메모리가 관리되는 코드 관리되지 않는 코드라고 해서 Managed Code, Unmanaged Code 로 언어들을 나누기도 합니다. “관리가 된다”라는 의미에서 메모리만을 지칭하지는 않겠지만 비슷하게 생각해도 무방할 것 같습니다. 보통 Managed Code라 하면 Java나 C#이 대표적입니다. 메모리를 가상머신이 관리해주기 때문에 메모리 해제를 신경쓰지 않고 구현할 수 있습니다. 대신 성능상에 낭비가 좀 있습니다. 하지만 요즘같이 하드웨어 성능이 좋은 상황에서는 그런 점이 단점이 되지는 않는 것 같습니다.
하지만 Go언어에서 메모리 관리가 된다고 하여 Java나 C#과 같은 Managed Code로 보기엔 좀 힘들겠지만 C/C++과 같이 세밀한 컨트롤을 할 수 있는 상황에서 메모리를 관리해주는 기능이 된다고 하면 꽤 매력적인 언어임에는 틀림 없는 것 같습니다.