포인터는 메모리 주소를 값으로 갖는 타입이다. 포인터를 이용하면 동일한 메모리 공간을 여러 변수가 가리킬수 있다.
포인터는 메모리 주소를 값으로 갖는 타입입니다. 예를 들어 int 타입 변수 a가 있을 때 a는 메모리에 저장되어 있고 속성으로 메모리 주소를 가지고 있습니다. 변수 a의 주소가 0x0100번지라고 했을 때 메모리 주솟값 또한 숫자값이기 때문에 다른 변수의 값으로 사용될 수 있습니다. 이렇게 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 포인터 변수라고 합니다.

그림에서 int 타입 변수 a의 메모리 주소는 0x0100번지이고, 값으로 3을 갖습니다.
포인터 변수의 선언
var p * int
포인터의 변수 초기화는 & 연산자로 진행한다.
//ch14/ex14.1/ex14.1.go
package main
import "fmt"
func main() {
var a int = 500
var p *int // 1️⃣ int 포인터 변수 p 선언
p = &a // 2️⃣ a의 메모리 주소를 변수 p의 값으로 대입(복사)
fmt.Printf("p의 값: %p\n", p) // 3️⃣ 메모리 주솟값 출력
fmt.Printf("p가 가리키는 메모리의 값: %d\n", *p) // 4️⃣ p가 가리키는 메모리의 값 출력
*p = 100 // 5️⃣ p가 가리키는 메모리 공간의 값을 변경합니다.
fmt.Printf("a의 값: %d\n", a) // 6️⃣ a값 변화 확인
}
1️⃣ int 타입 포인터 변수 p를 선언합니다.
2️⃣ 변수 a의 주소를 p에 대입합니다.
3️⃣ p값을 출력합니다. 메모리 주솟값은 %p로 출력합니다.
4️⃣ p가 가리키는 메모리 주소에 담긴 값을 출력합니다.
포인터 변수 앞에 *를 붙여서 접근했습니다.
p는 a 변수의 메모리 주소를 값으로 가지고 있기 때문에 *p는 a의 값(500)이 됩니다.
5️⃣ 포인터 변수 p가 가리키는 메모리 공간의 값을 100으로 변경합니다.
p는 a의 메모리 주소를 가리키기 때문에 *p = 100은 곧 a값을 100으로 변경하는 것과 같습니다.
6️⃣ a값이 변경된 것을 알 수 있습니다.
변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 사용
또한, 다른 공간으로 복사되기 때문에 변경 사항이 적용되지 않음
포인트를 사용하면 포인터 변수가 메모리 주소를 값으로 가지고 있어 효율적으로 내부 필드값 변경 가능
//ch14/ex14.3/ex14.3.go
package main
import "fmt"
type Data struct { // ❶ Data형 구조체
value int
data [200]int
}
func ChangeData(arg Data) { // ❷ 파라미터로 Data를 받습니다.
arg.value = 999
arg.data[100] = 999
}
func main() {
var data Data
ChangeData(data) // ❸ 인수로 data를 넣습니다.
fmt.Printf("value = %d\n", data.value)
fmt.Printf("data[100] = %d\n", data.data[100]) // ❹ data 필드 출력
}
❷ ChangeData() 함수는 ❶ Data 타입 구조체를 매개변수로 받습니다.
❸ ChangeData() 함수를 호출하면서 data 변숫값을 인수로 넣습니다.
❷ data 변숫값이 모두 복사되기 때문에 ChangeData( ) 함수의 매개변수 arg와 data는 서로 다른 메모리 공간을 갖는 변수입니다.
❸ arg 매개변숫값을 변경합니다. data 변수와는 다른 메모리 공간을 가지기 때문에 ❷ arg값을 변경해도 data값은 변경되지 않습니다.
❹ data값을 출력하지만 값이 변경되지 않았습니다.

ChangeData( ) 함수 호출 시 data 변숫값이 모두 복사되기 때문에 구조체 크기만큼 복사됩니다. Data 구조체는 int 타입 value와 크기가 200인 int 타입 배열 data로 구성되어 있어 총 1608바이트입니다. ChangeData() 함수를 한 번 호출할 때마다 1608바이트가 복사됩니다. 만약 ChangeData( ) 함수가 짧은 시간에 많이 호출되면 성능 문제가 발생할 수 있습니다.
이 문제를 한방에 해결해주는 해결사가 포인터입니다. 포인터를 이용해서 앞 예제를 다시 만들어 보겠습니다.
//ch14/ex14.4/ex14.4.go
package main
import "fmt"
type Data struct {
value int
data [200]int
}
func ChangeData(arg *Data) { // ❶ 파라미터로 Data 포인터를 받습니다.
arg.value = 999 // ❸ arg 데이터 변경
arg.data[100] = 999
}
func main() {
var data Data
ChangeData(&data) // ❷ 인수로 data의 주소를 넘깁니다.
fmt.Printf("value = %d\n", data.value) // ❹ data 필드값 출력
fmt.Printf("data[100] = %d\n", data.data[100])
}
❶ ChangeData() 함수 매개변수로 Data 구조체의 포인터를 받는 것으로 변경했습니다. ❷ 이제 data 변숫값이 아니라 data의 메모리 주소를 인수로 전달합니다. 메모리 주소는 8바이트 숫자값*이기 때문에 1608바이트의 구조체 전부가 복사되는 게 아닌 8바이트만 복사됩니다.
64비트 컴퓨터에서 메모리 주소는 8바이트이고 32비트 컴퓨터에서는 4바이트 크기를 갖습니다.
❸ arg 포인터 변수가 가리키는 구조체의 값을 변경합니다. arg 포인터의 값은 main() 함수의 data 구조체 주솟값이기 때문에 arg 포인터가 main( ) 함수의 data 변수를 가리키게 됩니다. 그래서 data값이 변경됩니다.
*arg는 포인터 변수이기 때문에 (*arg).value = 999라고 써야 하지만 Go 언어에서는 arg.value라고만 써도 동작합니다.
❹ data의 value와 data 배열의 101번째 값이 변경됐습니다.
포인터를 이용하면 data 변수의 메모리 주소만 복사되기 때문에 메모리 주솟값인 8바이트만 복사됩니다. 또, arg 포인터 변수가 data 변수의 메모리 주소를 값으로 가지고 있어서 Data 구조체의 내부 필드값을 변경할 수 있습니다. 이처럼 포인터를 이용하면 더 효율적으로 데이터를 조작할 수 있습니다.

인스턴스란 메모리에 할당된 데이터의 실체를 말합니다. 예를 들어 다음 코드는 Data 타입값을 저장할 수 있는 메모리 공간을 할당합니다.
var data Data
이렇게 할당된 메모리 공간의 실체를 인스턴스라고 부릅니다.
Data 타입 포인터 변수를 선언하고 data 변수의 주소를 값으로 대입시켜보겠습니다.
var data Data
var p *Data = &data
Data 타입 포인터 변수 p를 선언하고 data의 주소를 대입했습니다. 이때 포인터 변수 p는 data를 가리킨다고 말합니다. 이때 p가 생성될 때 새로운 Data 인스턴스가 만들어진 게 아닙니다. 기존에 있던 data 인스턴스를 가리킨 겁니다. 즉 만들어진 총 Data 인스턴스 개수는 한 개입니다.

인스턴스를 별도로 생성하지 않고, 곧바로 인스턴스를 생성해 그 주소를 포인터 변수에 초깃값으로 대입하는 코드를 살펴보죠.
var p *Data = &Data{}
Data 인스턴스를 만들고 그 메모리 주소를 포인터 변수 p가 가리킵니다. 이번에도 인스턴스는 하나만 생성됩니다.

포인터 변수가 아무리 많아도 인스턴스가 추가로 생성되는 것은 아닙니다.
var p1 Data = &Data{}
var p2 Data = p1
var p3 *Data = p1
Data 인스턴스 하나를 만들고, 포인터 변수 p1, p2, p3가 가리킵니다. 가리키는 포인터 변수 개수는 인스턴스 개수와 무관합니다.

그럼 다음 코드에서 인스턴스는 몇 개일까요?
var data1 Data
var data2 Data = data1
var data3 Data = data1
data1, data2, data3 모두 인스턴스입니다. data1값이 data2, data3에 복사되어서 값만 같을 뿐입니다. 그래서 인스턴스 3개가 생성됩니다.

인스턴스는 메모리에 존재하는 데이터의 실체
포인터를 이용해서 인스턴스에 접근 가능
구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 말과 동일
p1 := &Data{} // 1 &를 사용하는 초기화
var p2 = new(Data) // 2 new()를 사용하는 초기화
new() 내장 함수는 인수로 타입을 받는다.
타입을 메모리에 할당하고 기본값으로 채워 그 주소를 반환
❷ new를 이용해서 내부 필드값을 원하는 값으로 초기화할 수는 없습니다.
반면 ❶ 방식은 p1 := &Data{ 3, 4 }처럼 사용자 초기화가 가능합니다.
&Data{} 과 new(Data) 방식 모두 다 자주 사용하는 방식이기 때문에 잘 알아두셔야 합니다.
Go 언어는 가비지 컬렉터라는 메모리 청소부 기능 제공
가비지 컬렉터가 일정 간격으로 메모리에서 쓸모없어진 데이터를 청소
단, 메모리는 굉장히 크기 때문에 이 메모리 영역을 모두 검사해서 쓸모없는 데이터를 지워주는데 성능 많이씀
func TestFunc() {
u := &User{}. // 1 u 포인터 변수를 선언하고 인스턴스를 생성합니다.
u.Age = 30
fmt.Println(u)
}. // 2 내부 변수 u는 사라집니다. 더불어 인스턴스도 사라집니다.
❶ ****u 포인터 변수를 선언하고 인스턴스를 생성했습니다. 메모리에 User 데이터가 할당됐고 u 포인터 변수가 가리킵니다. 이때 이 인스턴스는 u 포인터 변수로 사용되는 인스턴스이기 때문에 지워지면 안 됩니다.
하지만 TestFunc()이 종료되면 함수 내부 변수 u는 사라져 User 인스턴스를 가리키는 포인터 변수가 없게 됩니다. 이제 User 인스턴스는 쓸모가 없게 됐습니다. 드디어 가비지 컬렉터가 나설 차례입니다. 다음번 청소를 할 때 쓸모 없어진 이 User 인스턴스를 지우게 됩니다.
가비지 컬렉터가 알아서 메모리를 청소해주니 편리합니다. 하지만 세상에 공짜는 없다고 가비지 컬렉터도 공짜가 아닙니다. 메모리는 굉장히 크기 때문에 이 메모리 영역을 모두 검사해서 쓸모없는 데이터를 지워주는 데 성능을 많이 씁니다. 가비지 컬렉터를 사용하면 메모리 관리에서 이득을 보지만 성능에서 손해가 발생하는 거죠. 정리하겠습니다. 다음 네 가지만 기억하세요.
대부분 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용
이론상 스택 메모리 영역이 힙 메모리 영역보다 효율적
스택 메모리는 함수 내부에서만 사용 가능한 영역이기에, 함수 외부로 공개되는 메모리 공간은 힙 메모리 영역에서 할당
Go 언어는 탈출검사(escape analysis)를 해서 어느 메모리에 할당할 지를 결정
하기 코드에서, u 변수의 인스턴스가 함수 외부로 공개되는 것을 분석해내서 u를 스택 메모리가 아닌 힙 메모리에서 할당하게 된다.
즉, 본래 함수 내부에서 선언된 변수는 함수가 종료되어 사라짐으로서, 사라진 메모리를 가리키는 dangling 오류가 발생해야 하지만, 프로그램이 정상 작동한다.
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 // ❶ 탈출 분석으로 u 메모리가 사라지지 않음
}
func main() {
userPointer := NewUser("AAA", 23)
fmt.Println(userPointer)
}
❶ NewUser() 함수에서 선언한 u 변수를 반환했습니다. 함수 내부에서 선언된 변수는 함수가 종료되면 사라집니다. 이 코드는 이미 사라진 메모리를 가리키는 댕글링(dangling) 오류가 발생해야 합니다. 그런데 프로그램이 멀쩡하게 잘 동작하네요?
Go 언어에서는 탈출 검사를 통해서 u 변수의 인스턴스가 함수 외부로 공개되는 것을 분석해내서 u를 스택 메모리가 아닌 힙 메모리에서 할당하게 됩니다. 즉 Go 언어는 어떤 타입이나 메모리 할당 함수에 의해서 스택 메모리를 사용할지 힙 메모리를 사용할지를 결정하는 게 아닙니다. 메모리 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 메모리에 할당할지 힙 메모리에 할당할지 결정합니다.
또 Go 언어에서 스택 메모리는 계속 증가되는 동적 메모리 풀입니다. 일정한 크기를 갖는 C/C++ 언어와 비교해 메모리 효율성이 높고, 재귀 호출 때문에 스택 메모리가 고갈되는 문제도 발생하지 않습니다.