Golang 기초 (10) : 포인터에 대하여
안녕하세요, 주니어 개발자 Eon입니다.
이번 포스트는 포인터에 대한 내용입니다.
📝 포인터란?
'메모리 주소를 가리키는 것' 을 의미합니다.
변수를 선언하면 메모리 영역에 공간이 할당되는데, 그 메모리 영역의 주소를 가리키는 것을 포인터라고 합니다.
포인터 변수는 메모리 주소를 값으로 가집니다.

📝 인스턴스란?
메모리의 실체를 말합니다.
우리가 변수를 선언하면 변수의 자료형 크기만큼 메모리가 할당됩니다.
인스턴스는, 그 할당된 메모리의 실체를 가리키는 말입니다.

📝 포인터는 왜 사용할까?
포인터를 사용하면 메모리 영역에 직접 접근하여 인스턴스를 조작할 수 있습니다.
어디서든 인스턴스에 접근할 수 있어, 메모리를 중복으로 할당하는 경우를 줄이고 불필요한 메모리 낭비를 막을 수 있습니다.
- 퍼포먼스를 신경쓸 때
포인터는 퍼포먼스를 고려하지 않을 땐 사용할 이유가 없다.
물론,포인터타입을 반환하는 함수를 사용할 땐 사용할 수 밖에 없다.인스턴스를 메모리에 통째로 복사해서 사용할 때,인스턴스의 주소만 넘김으로써 메모리 낭비를 줄일 때 사용한다.
📝 포인터
📌 포인터의 초기화
var p1 *int
// var a int
var p1 *int = &a
p1 := &a
// type Sth struct {}
p1 := &Sth{}
p1 := new(int)
위의 선언은 각기 다른 포인터 변수의 선언 방법을 나타낸다.
var p1 *int : pointer int형의 변수 p1을 선언한다.
이때, p1의 값은 nil이다. (아래 내용 참조)
var p1 *int = &a, p1 := &a : pointer int형의 변수 p1을 선언하고 &a로 초기화한다.
&a는 변수 a의 주소값이다.
p1 := &Sth{} : pointer Sth{}형의 변수 p1을 선언한다.
p1 := new(int) : pointer int형의 변수 p1을 선언한다.
new(Type)은 pointer Type의 zero value를 반환한다.
📌 포인터 변수의 기본값은 nil
var pt *int
fmt.Println(pt)
// <nil>
- 초기화를 하지 않고
zero value 상태의 포인터 변수를 출력하면 <nil>이 출력된다.
nil은 출력할 때 항상 <, >을 포함한다.
포인터 변수의 값이 nil이라는 것은 가리키고 있는 인스턴스가 없음을 말한다.
📍 nil이란?
builtin.go
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
nil은 pointer, channel, func, interface, map, or slice type의 zero value(기본값)이다.
(예 : int의 zero value는 0이다.)
📌 포인터의 사용
📍 예시 1
var a int = 10
var b int = 20
var p1 *int = &a
var p2 *int = &b
fmt.Printf("variable a : %d, address : %p\n", a, p1)
fmt.Printf("variable b : %d, address : %p\n", b, p2)
fmt.Printf("variable p1 is pointing a : %d\n", *p1)
// ***** result of the output *****
// variable a : 10, address : 0xc0000a8000
// variable b : 20, address : 0xc0000a8008
// variable p1 is pointing a : 10
- 변수
a와 b를 int형으로 선언하고, 각각 10, 20으로 초기화한다.
- 변수
p1과 p2를 *int형(pointer int형)으로 선언하고, 각각 a와 b의 주소값으로 초기화한다.
- 초기화한 값들을 출력하여 확인한다.
여기서 a와 b의 주소값의 차이가 8만큼 나는 것을 확인할 수 있다.
둘 다 int형 변수이고, 64비트 OS라서 int형이 8byte의 사이즈를 가진다.
a의 메모리를 할당하고 가장 빠른 메모리 영역이 8만큼 뒤의 영역이고, b는 a의 바로 뒤에 메모리가 할당되었다는 것을 알 수 있다.
포인터 자료형의 출력은 %p로 할 수 있다.
*p1으로 p1이 가리키는 인스턴스의 값을 나타내고, 출력하여 값을 확인할 수 있다.
여기서 p1은 a의 주소값을 가지고 있고 해당 주소값을 * (pointer)로 가리켜, 인스턴스의 값을 나타낸다.
📍 예시 2
package main
import (
"fmt"
"unsafe"
)
type pTest struct {
pStr string
pInt int
}
func setStr(getStruct *pTest) {
getStruct.pStr = "struct pTest's byte size :"
}
func setInt(getStruct *pTest) {
getStruct.pInt = int(unsafe.Sizeof(pTest{}))
}
func main() {
var example pTest
setStr(&example)
setInt(&example)
fmt.Println(example)
}
// {struct pTest's byte size : 24}
위와 같이 다른 서로 다른 함수에서 같은 구조체 인스턴스인 example에 접근하여 요소를 조작했다.
출력 결과를 통해, 같은 인스턴스에 대하여 조작했다는 것을 확인할 수 있다.
📍 예시 3
package main
import (
"fmt"
"sync"
)
type pTest struct {
pStr string
pInt int
}
func addStr(wg *sync.WaitGroup, getStruct *pTest, str rune) {
getStruct.pStr = getStruct.pStr + string(97+str)
defer wg.Done()
}
func addInt(wg *sync.WaitGroup, getStruct *pTest, num int) {
getStruct.pInt += num
defer wg.Done()
}
func printStruct(wg *sync.WaitGroup, getStruct *pTest, num int) {
fmt.Println(getStruct)
defer wg.Done()
}
func main() {
var example pTest
defer fmt.Println("===== Finished =====")
wg := sync.WaitGroup{}
defer wg.Wait()
for i := 0; i <= 10; i++ {
wg.Add(1)
go addStr(&wg, &example, rune(i))
wg.Add(1)
go addInt(&wg, &example, i)
wg.Add(1)
go printStruct(&wg, &example, i)
}
}
위의 코드를 실행하면 실행할 때마다 결과가 바뀐다.
goroutine(golang의 멀티 쓰레드)을 사용한 것인데, golang에서 동시성 프로그래밍을 할 때 사용한다.
(os 쓰레드보다 훨씬 가볍게 동작하는 가상 쓰레드이며, 비동기(asynchronously) 실행을 한다.)
아무튼 위와 같이 사용하면 인스턴스를 사용자가 예측 불가하게 무작위로 조작하기 때문에, 동시에 같은 인스턴스에 접근 및 조작하는 로직을 구현할 때는 주의해야 한다.
📝 스택 & 힙 메모리
📌 스택 메모리
var p1 *int
// var a int
var p1 *int = &a
p1 := &a
// type Sth struct {}
p1 := &Sth{}
p1 := new(int)
위의 선언은 각기 다른 포인터 변수의 선언 방법을 나타낸다.
var p1 *int:pointer int형의 변수p1을 선언한다.
이때,p1의 값은nil이다. (아래 내용 참조)var p1 *int = &a,p1 := &a:pointer int형의 변수p1을 선언하고&a로 초기화한다.
&a는 변수a의 주소값이다.p1 := &Sth{}:pointer Sth{}형의 변수p1을 선언한다.p1 := new(int):pointer int형의 변수p1을 선언한다.
new(Type)은pointer Type의zero value를 반환한다.
var pt *int
fmt.Println(pt)
// <nil>
- 초기화를 하지 않고
zero value상태의포인터 변수를 출력하면<nil>이 출력된다. nil은 출력할 때 항상<,>을 포함한다.포인터 변수의 값이nil이라는 것은 가리키고 있는인스턴스가 없음을 말한다.
builtin.go
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
nil은 pointer, channel, func, interface, map, or slice type의 zero value(기본값)이다.
(예 : int의 zero value는 0이다.)
var a int = 10
var b int = 20
var p1 *int = &a
var p2 *int = &b
fmt.Printf("variable a : %d, address : %p\n", a, p1)
fmt.Printf("variable b : %d, address : %p\n", b, p2)
fmt.Printf("variable p1 is pointing a : %d\n", *p1)
// ***** result of the output *****
// variable a : 10, address : 0xc0000a8000
// variable b : 20, address : 0xc0000a8008
// variable p1 is pointing a : 10
- 변수
a와b를int형으로 선언하고, 각각10,20으로 초기화한다. - 변수
p1과p2를*int형(pointer int형)으로 선언하고, 각각a와b의 주소값으로 초기화한다. - 초기화한 값들을 출력하여 확인한다.
여기서a와b의 주소값의 차이가8만큼 나는 것을 확인할 수 있다.
둘 다int형 변수이고, 64비트 OS라서int형이8byte의 사이즈를 가진다.
a의 메모리를 할당하고 가장 빠른 메모리 영역이8만큼 뒤의 영역이고,b는a의 바로 뒤에 메모리가 할당되었다는 것을 알 수 있다. 포인터자료형의 출력은%p로 할 수 있다.*p1으로p1이 가리키는인스턴스의 값을 나타내고, 출력하여 값을 확인할 수 있다.
여기서p1은a의 주소값을 가지고 있고 해당 주소값을*(pointer)로 가리켜,인스턴스의 값을 나타낸다.
package main
import (
"fmt"
"unsafe"
)
type pTest struct {
pStr string
pInt int
}
func setStr(getStruct *pTest) {
getStruct.pStr = "struct pTest's byte size :"
}
func setInt(getStruct *pTest) {
getStruct.pInt = int(unsafe.Sizeof(pTest{}))
}
func main() {
var example pTest
setStr(&example)
setInt(&example)
fmt.Println(example)
}
// {struct pTest's byte size : 24}
위와 같이 다른 서로 다른 함수에서 같은 구조체 인스턴스인 example에 접근하여 요소를 조작했다.
출력 결과를 통해, 같은 인스턴스에 대하여 조작했다는 것을 확인할 수 있다.
package main
import (
"fmt"
"sync"
)
type pTest struct {
pStr string
pInt int
}
func addStr(wg *sync.WaitGroup, getStruct *pTest, str rune) {
getStruct.pStr = getStruct.pStr + string(97+str)
defer wg.Done()
}
func addInt(wg *sync.WaitGroup, getStruct *pTest, num int) {
getStruct.pInt += num
defer wg.Done()
}
func printStruct(wg *sync.WaitGroup, getStruct *pTest, num int) {
fmt.Println(getStruct)
defer wg.Done()
}
func main() {
var example pTest
defer fmt.Println("===== Finished =====")
wg := sync.WaitGroup{}
defer wg.Wait()
for i := 0; i <= 10; i++ {
wg.Add(1)
go addStr(&wg, &example, rune(i))
wg.Add(1)
go addInt(&wg, &example, i)
wg.Add(1)
go printStruct(&wg, &example, i)
}
}
위의 코드를 실행하면 실행할 때마다 결과가 바뀐다.
goroutine(golang의 멀티 쓰레드)을 사용한 것인데, golang에서 동시성 프로그래밍을 할 때 사용한다.
(os 쓰레드보다 훨씬 가볍게 동작하는 가상 쓰레드이며, 비동기(asynchronously) 실행을 한다.)
아무튼 위와 같이 사용하면 인스턴스를 사용자가 예측 불가하게 무작위로 조작하기 때문에, 동시에 같은 인스턴스에 접근 및 조작하는 로직을 구현할 때는 주의해야 한다.
📌 스택 메모리
스택 메모리는 함수 호출 시에 함수에 자동으로 할당되는 메모리를 말합니다.
함수가 끝날 때 자동으로 정리되는 메모리입니다.
📌 힙 메모리
힙 메모리는 프로그래머가 수동으로 할당하는 메모리를 말합니다.
수동으로 할당하기 때문에 프로그래머가 수동으로 해제해야 하는 메모리입니다.
그렇지 않으면 메모리에서 자동으로 해제되지 않기 때문에 메모리 낭비로 이어집니다.
📌 메모리 관리는 어떻게 해야 할까?
Golang은 스택이든 힙이든 가비지 컬렉터가 알아서 메모리를 정리해줍니다.
때문에 사용자가 고심하며 메모리 할당에 대해 고민할 필요가 없습니다.
물론, 나중에 무거운 기능을 수행하는 코드를 작성할 때에는 고려해야만 합니다.
예를 들어, file을 읽어오는데 그 file의 크기를 이미 알고 있다면 그에 맞게 메모리를 할당해, 힙 영역에 메모리가 생기지 않게 하는 것이 좋습니다.
📍 힙은 비용이 비싸다?
힙은 스택에 비해 비용이 비쌉니다.
비용이 비싸다는 것은, "힙은 엑세스와 메모리 해제가 스택에 비해 느리다" 라는 것을 말합니다.
여러 관점에서, 힙에 메모리가 할당되는 것은 지양하는 것이 좋습니다.
📌 탈출 분석
탈출 분석은 함수에서 함수로 인스턴스가 탈출하는지 여부를 분석하는 것입니다.
탈출 분석을 통해 탈출하는 인스턴스의 메모리를 스택이 아닌 힙에 할당합니다.
인스턴스가 함수 외부에서도 사용이 된다면 스택 메모리에 할당하는 것은 적절하지 않기 때문입니다.
📍 힙 메모리 할당 확인
아래의 명령어로 .go 파일을 컴파일하면 컴파일 과정에서 힙 메모리의 사용 여부를 확인할 수 있습니다.
go build -gcflags '-m -l'
go build -gcflags '-m'
go build -gcflags '-m=2'
- '-m -l' : 옵티마이징 레벨을 1로 하고, 인라이닝을 없앱니다.
- '-m' : 옵티마이징 레벨을 1로 합니다.
- '-m=2' : 옵티마이징 레벨을 2로 합니다. (보다 상세하게 나옵니다.)
컴파일 옵션은 아래의 명령어로 확인할 수 있습니다.
go tool compile -help
📍 예시
package main func get(p *int) *int { example := *p return &example } func main() { var a int var p *int = &a a = 20 _ = get(p) }
eon@vamos-eon:~/goprojects/pointer$ go build -gcflags '-m -l' # main ./pointer.go:5:10: p does not escape ./pointer.go:6:2: moved to heap: example ./pointer.go:16:13: ... argument does not escape ./pointer.go:16:14: "" escapes to heap위와 같이
get()함수에서 변수example을 할당했으나,example의인스턴스를get()함수 외부로 반환하여 스택이 아닌 힙에 메모리가 할당된 것을 확인할 수 있다.
이번 포스트는 포인터에 대한 내용이었습니다.
감사합니다.👍
Author And Source
이 문제에 관하여(Golang 기초 (10) : 포인터에 대하여), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@vamos_eon/go-pointer저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)