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.)