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