안녕하세요, 주니어 개발자 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
를 반환한다.
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'
컴파일 옵션은 아래의 명령어로 확인할 수 있습니다.
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()
함수 외부로 반환하여 스택이 아닌 힙에 메모리가 할당된 것을 확인할 수 있다.
이번 포스트는 포인터에 대한 내용이었습니다.
감사합니다.👍