이 글은 Must Have Tucker의 Go 언어 프로그래밍 책을 참고하여 작성하였으며, 개인적인 학습 내용을 정리한 글입니다.
포인터
포인터는 메모리 주소를 값으로 갖는 타입입니다.
포인터를 이용하면 동일한 메모리 공간을 여러 변수가 가리킬 수 있습니다.
예를 들어 int 타입 변수 a를 선언하면,
- 변수 a는 메모리에 저장되어 있고
- 속성으로 메모리 주소를 가지고 있습니다.
var a int = 3
---
- 변수 a는 값 10으로 선언
- 메모리 어딘가에 저장됨
- 그 메모리 주소를 가리키는 것이 포인터이 때 변수 a의 주소가 0x0100 번지라고 가정을 했을 때, 메모리 주솟값(0x0100) 또한 숫자값이기 때문에 다른 변수의 값으로 사용될 수 있습니다.
이렇게 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 포인터 변수라고 합니다.

위 그림에서 정의한대로 나타내면
p = &a
---
- p는 포인터 변수 p 라는 의미
- 그리고 포인터 변수 p는 변수 a의 값이 저장된 위치를 가리킵니다.
- 즉, p = 0x0100 번지
이렇게 메모리 주소를 값으로 가져 메모리 공간을 가리키는 타입을 포인터 라고 합니다.
포인터 변수 선언
포인터 변수는 가리키는 데이터 타입 앞에 * 를 붙여서 선언 합니다.
1. var p *int
2. var a int
3. p = &a
---
1. p는 int 타입 데이터의 메모리 주소를 가리키는 포인터 변수라는 의미
2. int 타입의 변수 a 선언
3. 변수 a의 메모리 주소를 포인터 변수 p에 대입여기서 보면 &과 * 은 무엇인가 궁금하실 텐데요
- &은 주소 연산자 입니다. 변수의 메모리 주소를 가져옵니다.
- *는 역참조 연산자 입니다. 포인터가 가리키는 실제 값을 가져옵니다.
var a int = 10
var p *int
p = &a
fmt.Println(p) // 변수 a가 가진 10 값에 대한 메모리 주솟값 출력
fmt.Println(*p) // 10 출력예시를 통해 포인터 변수 사용법에 대해 알아보겠습니다.
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의 값: 0x1400000e088
p가 가리키는 메모리의 값: 500
a의 값: 100❷ 포인터 변수 p에 a의 메모리 주소 값을 넣어줍니다.
❸ 그리고 p 값을 찍어보면, 변수 a의 메모리 주솟값을 출력합니다.
❹ 그리고 p가 가리키는 메모리 주소에 담긴 값을 출력하면 변수 a에 담겨있던 500이 출력됩니다.
➎ 포인터 변수 p가 가리키는 메모리 공간 값을 100으로 변경해주면,
➏ a의 값이 500에서 100으로 변경되는 것을 확인할 수 있습니다.
포인터의 기본값 nil
포인터 변숫값은 초기화해주지 않으면 기본값 nil 입니다.
이 값은 0이지만 정확한 의미는 유효하지 않는 메모리 주솟값
즉, 어떤 메모리 공간도 가리키고 있지 않다는 걸 나타냅니다.
이 상태에서 역참조(*)를 하여 메모리 공간 값을 꺼내려고 하면 프로그램이 즉시 panic이 됩니다.
따라서 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 필드 출력
}
---
value = 0
data[100] = 0❸ ChangeData(data)를 호출할 때 data 전체가 복사되어 함수로 전달이 됩니다.
❷ 그래서 함수 안에서 바꾼 건 복사본이고,
❹ main에 있는 원본 data를 출력해보면 값이 전혀 바뀌지 않은 것을 확인할 수 있습니다.
해당 예제의 문제점은 ChangeData() 함수 호출 시 data 변숫값이 모두 복사되기 때문에 구조체 크기만큼 복사됩니다.
즉, ChangeData() 함수를 짧은 시간에 많이 호출하면 성능 문제가 발생 할 수 있습니다.
이러한 문제를 해결해주는 것이 포인터 입니다.
- 포인터를 사용할 때
//ch12/ex12.4/ex12.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])
}
---
value = 999
data[100] = 999앞선 내용과 비슷하지만, ❶ 여기서 파라미터로 Data 구조체의 포인터를 받은 걸로 변경합니다.
그리고 ❷ 에서 Data 구조체의 data 변수의 포인터 주소값을 넘겨주면,
data 변수의 주소의 공간에 ChangeData() 함수의 데이터들이 들어가게되어
❹ 에서 보면 data 필드의 값들이 변경되어 출력되는 것을 확인할 수 있습니다.
이게 포인터를 안썻을 때와 어떤 차이점이 있냐면,
❷ 여기서 data 변숫값이 아니라 메모리 주소를 인수로 전달을 한거여서, 구조체 전부가 복사되는 것이 아닌 메모리 주소 값만 복사가 되어, 효율적으로 데이터를 조작할 수 있습니다.
구조체를 생성해 포인터 변수 초기화하기
앞선 예제에서는 구조체 변수를 별도로 생성을 하고, 포인터 변수에 구조체 변수의 메모리 주솟값을 넣어주었습니다.
ex)
var data Data // 구조체 변수 선언
var p *Data = &data // 포인터 변수 p에 구조체 변수(data)의 메모리 주솟값 복사이렇게 하면 번거로우니 아래의 방식으로 하면 코드 한줄로 정의할 수 있습니다.
ex)
var p *Data = &Data{} // 포인터 변수 p에 구조체의 메모리 주솟값을 복사이렇게 하면 따로 구조체 변수를 선언하지 않고 바로 포인터 변수 p에 구조체의 메모리 주솟값을 복사해줄 수 있습니다.
인스턴스
인스턴스란 메모리에 할당된 데이터의 실체를 말합니다.
예시를 들자면,
var data Data위와 방법은 Data 구조체 타입의 data 변수를 만드는 방법인데,
이렇게 하면 data 변수가 사용할 수 있는 메모리 공간이 생기는데, 해당 공간의 실체를 인스턴스 라고 부릅니다.
그러면 인스턴스를 만들어보겠습니다.
var data Data // data 변수가 사용할 수 있는 인스턴스 생성
var p1 *Data = &data // 포인터 변수 p가 인스턴스를 가리킴이렇게 인스턴스가 생성이 됩니다.
그리고 포인터 변수로 해당 인스턴스를 가리켜도 인스턴스는 추가로 생성되지 않습니다.
var p2 *Data = p1
var p3 *Data = p1
그러나 포인터와 달리 하나의 구조체 변수를 복사한 여러 개의 구조체 변수들의 인스턴스들은 다릅니다.
var data1 Data
var data2 Data = data1
var data3 Data = data1이렇게 되면 data1, data2, data3 모두 인스턴스 입니다.
포인터와 달리 서로 같은 값을 가지고 있을 뿐 이지 각각 별개의 인스턴스가 생성이 됩니다.
인스턴스에 대해 좀 더 설명을 하자면,
인스턴스는 데이터의 실체 입니다.
앞서 설명을 했던 것 처럼 인스턴스는 데이터의 실체인데,
포인터를 이용해서 인스턴스에 접근할 수 있습니다.
구조체 포인터를 함수 매개변수로 받는다는 말은 구조체 인스턴스로 입력을 받겠다는 이야기와 같습니다.
앞서 포인터값을 별도의 변수를 선언하지 않고 초기화하는 방법을 보았습니다.
그런데, new() 내장 함수를 이용하면 더 간단히 표현할 수 있습니다.
p := new(int)위와 같이 사용을 하는데, new(Type) new 함수에는 꼭 타입이 들어가야합니다.
즉, 타입의 "제로값으로 초기화된 인스턴스"를 하나 만들고 그 인스턴스의 주소를 반환하는 내장 함수 입니다.
인스턴스는 언제 사라지나
인스턴스는 메모리에 할당된 데이터의 실체인데, 메모리는 무한한 자원이 아닙니다.
만약 메모리에 데이터가 할당만 되고 사라지지 않는다면 프로그램은 금세 메모리가 고갈되어 비정상 종료가 됩니다.
그래서 쓸모없는 데이터를 메모리에서 해제하는 기능이 필요합니다.
Go 언어는 가비지 컬렉터 라는 메모리 청소부 기능을 제공합니다.
그래서 우린 따로 메모리를 정리하지 않아도 가비지 컬렉터가 알아서 메모리를 청소를 해줍니다.
다만, 가비지 컬렉터가 쓸모 없는 데이터를 지워주는 데 성능이 많이 씁니다.
따라서 메모리 관리 면에서는 이득이 있지만 성능면에서는 손해가 발생합니다.
정리하자면,
- 인스턴스는 메모리에 생성된 데이터의 실체
- 포인터를 이용해서 인스턴스를 가리키게 할 수 있음
- 함수 호출 시 포인터 인수를 통해서 인스턴스를 입력받고 그 값을 변경 가능
- 쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 지워줌
스택 메모리와 힙 메모리
대부분 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용합니다.
이론상 스택 메모리가 힙 메모리보다 효율적이지만, 스택 메모리는 함수 내부에서만 사용가능한 영역 입니다.
그래서 함수 외부로 공개되는 메모리 공간은 힙 메모리 영역에서 할당 합니다.
Go 언어는 탈출 검사(escape analysis)를 통해 어느 메모리에 할당할지 결정 합니다.
예제 코드를 보겟습니다.
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)
}
---
&{AAA 23}원래라면 NewUser() 함수에서 선언한 변수 u는 함수 내부에서 선언된 변수여서 함수가 종료되면 사라져야 합니다.
그런데 userPointer 변수를 출력 해보면 정상적으로 값이 나옵니다.
이 말은, Go 언어에서는 탈출 검사를 통해서 u 변수의 인스턴스가 함수 외부로 공개되는 것을
분석해내서 u를 스택메모리가 아닌 힙 메모리에 할당을 하게된 것 입니다.
즉 Go 언어는 메모리 공간이 함수 외부로 공개되는지 여부를 자동으로 검사해서 스택 or 힙 메모리에 할당할지 결정 합니다.
이상 입니다.
감사합니다.
'개발 > Go' 카테고리의 다른 글
| [Go 언어 학습기 #8] Go 언어(Golang) 의 슬라이스 (0) | 2025.12.23 |
|---|---|
| [Go 언어 학습기 #7] Go 언어(Golang) 의 패키지 (0) | 2025.12.22 |
| [Go 언어 학습기 #6] Go 언어(Golang) 의 구조체 (0) | 2025.12.18 |
| [Go 언어 학습기 #5] Go 언어(Golang) 의 배열 (0) | 2025.12.17 |
| [Go 언어 학습기 #4] Go 언어(Golang) 의 함수 (0) | 2025.12.02 |
