요약
- 포인터는 메모리 주소를 값으로 갖는 타입이다.
- &를 이용하여 데이터의 메모리 주소를 알 수 있다.
- 포인터를 이용하면 메모리의 주솟값으로 메모리를 조작할 수 있다.
- 인스턴스는 메모리에 있는 데이터 실체이고 포인터로 조작할 수 있다.
포인터란?
포인터는 메모리 주소를 값으로 갖는 타입이다.
- ex) int 타입의 변수 a가 있을 때 a는 메모리에 저장되어 있고 속성으로 메모리 주소를 가지고 있다.
변수 a의 주소가 0x0000번지라고 치면 0x0000번지를 다른 변수의 값으로 사용할 수 있다.
이렇게 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 포인터 변수라고 한다.
- 여러 포인터가 같은 메모리 공간을 가리킬 수 있다.

포인터 변수 선언
선언 형식
var a *int
var b *float64
var c *User
- 포인터 변수는 가리키는 데이터 타입 앞에 * 을 붙여서 선언한다.
대입 형식
var a int
// int 타입의 포인터 변수 p 선언
var p *int
// 포인터 변수 p가 a를 가리키도록 대입한다.
p = &a
- 포인터 변수 p에 a의 메모리 주소를 대입하는 구문이다.
- 이것을 ‘포인터 변수 p가 변수 a를 가리킨다.’ 라고 말한다.
이제 p를 이용하여 변수 a의 값을 변경할 수 있다.
*p = 20
- 포인터 변수 앞에 *를 붙이면 그 포인터 변수가 가리키는 메모리 공간에 접근할 수 있다.
예제
package main
import "fmt"
func main() {
var a int = 500
var p *int // ❶ int 포인터 변수 p 선언
p = &a // ❷ a의 메모리 주소를 변수 p의 값으로 대입(복사)
fmt.Printf("p의 값: %p\\n", p) // ❸ 메모리 주솟값 출력
fmt.Printf("p가 가리키는 메모리의 값: %d\\n", *p) // ❹ p가 가리키는 메모리의 값 출력
*p = 100 // ➎ p가 가리키는 메모리 공간의 값을 변경합니다.
fmt.Printf("a의 값: %d\\n", a) // ➏ a값 변화 확인
}
---
p의 값: 0x1400011c008 // p
p가 가리키는 메모리의 값: 500 // *p
a의 값: 100 // a
💡 포인터는 리눅스의 하드링크 & 심볼릭 링크와 매우 유사하다.
(차이점은 포인터는 다른 변수를 가리키는 데 사용된다는 점)
2️⃣ 포인터 변숫값 비교하기
예제
//ch14/ex14.2/ex14.2.go
package main
import "fmt"
func main() {
var a int = 10
var b int = 20
var p1 *int = &a // ❶ p1은 a의 메모리 공간을 가리킵니다.
var p2 *int = &a // ❷ p2는 a의 메모리 공간을 가리킵니다.
var p3 *int = &b // ❸ p3는 b의 메모리 공간을 가리킵니다.
fmt.Printf("p1 == p2 : %v\\n", p1 == p2)
fmt.Printf("p2 == p3 : %v\\n", p2 == p3)
}
---
p1 == p2 : true
p2 == p3 : false
- == 연산을 사용하여 포인터 변수가 같은 메모리 공간을 가리키는지 확인한다.
- p1과 p2는 같은 메모리 공간을 가리키기 때문에 참이되고 p2와 p3는 같은 메모리 공간을 가리키지 않기 때문에 거짓이된다.
3️⃣ 포인터의 기본값 nil
포인터 변숫값을 초기화 하지 않으면 기본값은 nil이다.
- 이 값은 0이지만 정확한 의미는 유효하지 않는 메모리 주솟값을 가리키는 것을 의미한다. (어떠한 메모리 공간도 가리키고 있지 않음)
예제
var p *int
if p != nil {
// p가 nil이 아니다.
// => p가 유효한 메모리 주소를 가리킨다는 뜻임.
}
그래서 포인트 왜씀?
변수 대입이나 함수의 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 차지한다.
- 메모리 공간을 사용하는 문제, 큰 메모리 공간을 복사할 때 발생하는 성능 문제가 있다.
- 또한 다른 공간으로 복사되기 때문에 변경사항이 적용되지도 않음.
좋지 않은 코드 예제
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는 서로 다른 메모리 공간을 가지는 변수이다.
- 호출된 ChangeData() 에서 arg의 매개변숫값을 변경한다.
- 메모리 공간이 서로 다르기 때문에 data 값은 변경되지 않는다.
위 예제 코드에서 Data 구조체는 int 타입 value와 크기가 200인 int 타입 배열 data로 구성되어 있어서 총 1608 바이트이다.
- ChangeData() 함수를 한 번 호출할 때마다 1608바이트가 복사된다.
짧은 시간에 많이 호출되면 성능 문제가 발생할 수 있다.
포인터를 사용한 좋은 코드 예제
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])
}

Data 구조체를 생성해 포인터 변수 초기화하기
구조체 변수를 별도로 생성하지 않는다.
AS-IS
var data Data
var p *Data = &data
TO-BE
var p *Data = &Data{}
인스턴스
인스턴스란 메모리에 할당된 데이터의 실체를 말한다.
var data Data
- data변수는 Data 타입값을 저장할 수 있는 메모리 공간을 뜻한다.
- 이렇게 할당된 메모리 공간의 실체를 인스턴스라고 부른다.
💡 인스턴스 개념을 잘 이해해야 메서드나 인터페이스 개념도 쉽게 이해할 수 있다.
예제 1
var data Data
var p *Data = &data
- Data 타입 포인터 변수 p를 선언 후 data의 주소를 대입한다.
- Data 인스턴스 개수는 하나이다. (p가 생성될 때 새로운 Data인스턴스가 만들어진게 아니다.)
예제 2
var p *Data = &Data{}
- 예제 1과 동일하게 Data 인스턴스를 만들고 그 메모리 주소를 포인터 변수 p가 가리킨다.
- 이번에도 인스턴스는 하나만 생성된다.
포인터 변수가 아무리 많아도 인스턴스가 추가로 생성되는 것은 아니다.
- 가리키는 포인터 변수 개수는 인스턴스 개수와 무관하다.
1️⃣ 인스턴스는 데이터의 실체다.
포인터를 이용해서 인스턴스에 접근할 수 있다.
- 구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 얘기와 동일하다.
2️⃣ new() 내장 함수
포인터 값을 별도의 변수를 선언하지 않고 초기화 하는 방법보다 더 간단하게 표현할 수 있는게 바로 new() 내장함수를 이용하는 것이다.
var p *Data = &Data{} // 1. &를 사용하는 초기화
p1 := &Data{} // 2. &를 사용하는 초기화
var p2 = new(Data) // 3. new() 내장함수 이용하여 초기화
- new() 내장 함수는 인수로 타입을 받는다. 타입을 메모리에 할당하고 기본값으로 채워서 그 주소를 반환한다.
- 그러나 new()를 이용하여 내부 필드값을 원하는 값으로 초기화 하지는 못한다.
- 초기화 하려면 1, 2번째 방식을 이용해야한다.
💡 &Data{}와 new(Data) 방식 모두 다 자주 사용하는 방식이므로 둘 다 잘 알아야한다.
3️⃣ 그러면 인스턴스 언제 사라짐?
만약, 메모리에 데이터가 할당만 되고 사라지지 않는다면 프로그램은 금세 메모리가 고갈될 것임.
- 그래서 이런 쓸모없는 데이터를 메모리로부터 해제해주는 기능이 필요하다.
Go언어는 가비지 컬렉터 (Garbage Colletor)라는 기능을 제공한다. 가비지 컬렉터는 일정한 간격으로 메모리에서 쓸모없어진 데이터를 청소한다.
그러면 쓸모없는 데이터의 판별은 어떻게 하는가?
func TestFunc() {
u := &User{} // u 포인터 변수를 선언하고 인스턴스를 생성한다.
u.Age = 30
fmt.Println(u)
}
- 위 함수에서 생성된 User 인스턴스는 u 포인터 변수로 사용되는 인스턴스라서 사라지면 안된다.
- 하지만 TestFunc()이 종료되면 함수 내부 변수 u는 사라지기 때문에 User 인스턴스를 가리키는 포인터 변수가 없게된다.
- 이제 User 인스턴스를 쓸모가 없어지게 되었다. 가비지 컬렉터는 특정 간격에 이 User 인스턴스를 지우게 된다.
💡 메모리는 크기 때문에 가비지 컬렉터가 메모리 영역을 모두 검사하는데 성능을 많이 쓰게된다. 세상에 공짜는 없다.