자바스크립트를 활성화 해주세요

Tucker의 Go 언어 프로그래밍 14장 요약

 ·  ☕ 6 min read

Tucker의 Go 언어 프로그래밍스터디 요약 노트입니다.

14장. 포인터

포인터의 Go 언어에서의 특징만 확인해보자.

  1. 빈 포인터의 표기는 null이 아니라 nil로 표현한다.
  2. 구조체를 가리키는 포인터에서 해당 구조체의 필드 접근은 ->이 아니라 .으로 가능하다.
  3. call-by-address를 통해 함수의 인자를 전달, 수정할 수 있다.
  4. new()를 사용하여 인스턴스를 생성할 수 있다.
  5. 인스턴스를 가리키는 포인터가 없으면 가비지 컬렉터가 메모리를 회수한다.
  6. go 컴파일러에서 탈출 분석을 확인하여, 지역변수가 콜 스택이 아니라 힙 영역에 존재할 수 있다.

기본적인 포인터 사용

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import "fmt"

type Data struct {
	value int
	data [10]int
}

func main() {
	var data1 Data
	var pData1 *Data = &data1
	var pData2 *Data = &Data{}	// Create instance with indirect addressing
	var pData3 *Data = new(Data) // Create with new()

	fmt.Printf("&data1: %p\n", &data1)
	fmt.Printf("pData1: %p,\t&pData1: %p\n", pData1, &pData1)
	fmt.Printf("pData2: %p,\t&pData2: %p\n", pData2, &pData2)
	fmt.Printf("pData3: %p,\t&pData3: %p\n", pData3, &pData3)

	CallByValue(data1, 10)
	fmt.Println(data1)
	CallByAddress(&data1, 10)
	fmt.Println(data1)

	CallByValue(*pData1, 20)
	fmt.Println(pData1)
	CallByAddress(pData1, 20)
	fmt.Println(pData1)

	var pData4 *Data = nil
	pData4.value = 40	// Trigger segfault
}

func CallByValue(data Data, n int) {
	data.value = n
	data.data[0] = n / 10
	fmt.Printf("Call-by-value &data: %p\n", &data)
}

func CallByAddress(data *Data, n int) {
	data.value = n
	data.data[0] = n / 10
	fmt.Printf("Call-by-address &data: %p\n", data)
}
$ ./go_pointer_basics
&data1: 0x140000b2000
pData1: 0x140000b2000,  &pData1: 0x140000ac018
pData2: 0x140000b2060,  &pData2: 0x140000ac020
pData3: 0x140000b20c0,  &pData3: 0x140000ac028
Call-by-value &data: 0x140000b2120
{0 [0 0 0 0 0 0 0 0 0 0]}
Call-by-address &data: 0x140000b2000
{10 [1 0 0 0 0 0 0 0 0 0]}
Call-by-value &data: 0x140000b2240
&{10 [1 0 0 0 0 0 0 0 0 0]}
Call-by-address &data: 0x140000b2000
&{20 [2 0 0 0 0 0 0 0 0 0]}
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x10044fe78]

goroutine 1 [running]:
main.main()
	/Workspace/go/src/musthavego/ch14/go_pointer_basics.go:32 +0x408

가비지 컬렉터 순환 참조

각 인스턴스를 가리키는 포인터가 없을때 해당 변수는 가비지 컬렉션에 의해 메모리가 회수될 수 있다고 한다.

그렇다면 순환 참조를 해서 각 인스턴스를 가리키는 포인터는 존재하지만, 해당 순환을 가리키는 변수가 없는 경우에도 가비지 컬렉션이 잘 동작하는지 확인해봤다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

type List struct {
	value int
	next *List
}

func main() {
	var pLists [100]*List

	for i := 0; true; i++{
		var l1 List
		var l2 List
		var l3 List

		l1.next = &l2
		l2.next = &l3
		l3.next = &l1

		l1.value = 1
		l1.next.value = l1.value + 1
		l1.next.next.value = l1.next.value + 1

		pLists[i % 100] = &l1
	}
}

직접 실행해 본 결과, 딱히 메모리 누수가 발생하지 않는 것으로 보였다. 하지만 컴파일러가 최적화 과정에서 해당 요소를 미리 차단했는지는 확실하지 않아서, 이 부분은 컴파일 옵션, 메모리 누수 확인 툴을 좀 더 공부해서 다시 확인해봐야 할 것 같다.

탈출 분석

go 컴파일러는 탈출 분석을 하여 지역 변수의 주소를 return해도 오류가 나지 않는다. (해당 변수를 heap에 할당한다.)

해당 부분을 검증하기 위해 직접 실험해보았다. 실험 결과 모든 변수는 자체적인 goroutine의 heap에 저장되는 것 같다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import "fmt"

type Data struct{
	value int
	data [10]int
}

func main() {
	var data1 Data
	var data2 Data = *new(Data)

	var pData1 *Data = &data1
	var pData2 *Data = &data2
	var pData3 *Data = new(Data)
	var pData4 *Data = newData()
	var pData5 *Data

	n := 10
	if n % 5 == 0 {
		// force variable allocation unpredictable
		pData5 = new(Data)
	}

	fmt.Printf("pData1: %p\n", pData1)
	fmt.Printf("pData2: %p\n", pData2)
	fmt.Printf("pData3: %p\n", pData3)
	fmt.Printf("pData4: %p\n", pData4)
	fmt.Printf("pData5: %p\n", pData5)

	for {
		// Force infinite loop to check in /proc/**/maps
	}
}

func newData() *Data {
	var d Data = Data{}
	return &d
}
$ dlv exec ./go_mem_layout_struct
Type 'help' for list of commands.
(dlv) b go_mem_layout_struct.go:32
Breakpoint 1 (enabled) set at 0x499278 for main.main() ./go_mem_layout_struct.go:32
(dlv) continue
pData1: 0xc000102060
pData2: 0xc0001020c0
pData3: 0xc000102120
pData4: 0xc000102180
pData5: 0xc0001021e0
> main.main() ./go_mem_layout_struct.go:32 (hits goroutine(1):1 total:1) (PC: 0x499278)
Warning: debugging optimized function
    29:	 fmt.Printf("pData4: %p\n", pData4)
    30:	 fmt.Printf("pData5: %p\n", pData5)
    31:
=>  32:	 for {
    33:		 // Force infinite loop to check in /proc/**/map
    34:	 }
(dlv) regs
    Rip = 0x0000000000499278
    Rsp = 0x000000c000108eb0
    Rax = 0x0000000000000000
    Rbx = 0x000000c00011e000
    Rcx = 0x0000000000000000
    Rdx = 0x0000000000000000
    Rsi = 0x0000000000000000
    Rdi = 0x0000000000000000
    Rbp = 0x000000c000108f78
     R8 = 0x0000000000000000
     R9 = 0x0000000000000000
    R10 = 0xfffff80000000001
    R11 = 0x0000000000000212
    R12 = 0x000000c0000161b0
    R13 = 0x000000000000003b
    R14 = 0x0000000000000013
    R15 = 0xffffffffffffffff
 Rflags = 0x0000000000000202    [IF IOPL=0]
     Es = 0x0000000000000000
     Cs = 0x0000000000000033
     Ss = 0x000000000000002b
     Ds = 0x0000000000000000
     Fs = 0x0000000000000000
     Gs = 0x0000000000000000
Fs_base = 0x000000000054e5b0
Gs_base = 0x0000000000000000
$ cat /proc/70871/maps
00400000-0049a000 r-xp 00000000 08:01 4456778                            /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
0049a000-00538000 r--p 0009a000 08:01 4456778                            /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
00538000-0054e000 rw-p 00138000 08:01 4456778                            /Workspace/go/src/musthavego/ch14/go_mem_layout_struct
0054e000-00581000 rw-p 00000000 00:00 0
c000000000-c004000000 rw-p 00000000 00:00 0
7fc392bef000-7fc394fa0000 rw-p 00000000 00:00 0
7fc394fa0000-7fc3a5120000 ---p 00000000 00:00 0
7fc3a5120000-7fc3a5121000 rw-p 00000000 00:00 0
7fc3a5121000-7fc3b6fd0000 ---p 00000000 00:00 0
7fc3b6fd0000-7fc3b6fd1000 rw-p 00000000 00:00 0
7fc3b6fd1000-7fc3b93a6000 ---p 00000000 00:00 0
7fc3b93a6000-7fc3b93a7000 rw-p 00000000 00:00 0
7fc3b93a7000-7fc3b9820000 ---p 00000000 00:00 0
7fc3b9820000-7fc3b9821000 rw-p 00000000 00:00 0
7fc3b9821000-7fc3b98a0000 ---p 00000000 00:00 0
7fc3b98a0000-7fc3b9900000 rw-p 00000000 00:00 0
7fff8d006000-7fff8d027000 rw-p 00000000 00:00 0                          [stack]
7fff8d08f000-7fff8d093000 r--p 00000000 00:00 0                          [vvar]
7fff8d093000-7fff8d095000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]
$ objdump -h go_mem_layout_struct

go_mem_layout_struct:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .text         0009828a  0000000000401000  0000000000401000  00001000  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, CODE
  1 .rodata       00044167  000000000049a000  000000000049a000  0009a000  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .typelink     00000734  00000000004de300  00000000004de300  000de300  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .itablink     00000050  00000000004dea40  00000000004dea40  000dea40  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 .gosymtab     00000000  00000000004dea90  00000000004dea90  000dea90  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .gopclntab    00058ab8  00000000004deaa0  00000000004deaa0  000deaa0  2**5
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  6 .go.buildinfo 00000020  0000000000538000  0000000000538000  00138000  2**4
                  CONTENTS, ALLOC, LOAD, DATA
  7 .noptrdata    0000e2c4  0000000000538020  0000000000538020  00138020  2**5
                  CONTENTS, ALLOC, LOAD, DATA
  8 .data         00007790  0000000000546300  0000000000546300  00146300  2**5
                  CONTENTS, ALLOC, LOAD, DATA
  9 .bss          0002d750  000000000054daa0  000000000054daa0  0014daa0  2**5
                  ALLOC
 10 .noptrbss     00005310  000000000057b200  000000000057b200  0017b200  2**5
                  ALLOC
 11 .zdebug_abbrev 000001e6  0000000000581000  0000000000581000  0014e000  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 12 .zdebug_line  00032ac5  0000000000581119  0000000000581119  0014e119  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 13 .zdebug_frame 00010144  000000000059daa3  000000000059daa3  0016aaa3  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 14 .debug_gdb_scripts 0000002d  00000000005a3643  00000000005a3643  00170643  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 15 .zdebug_info  0007d8a7  00000000005a3670  00000000005a3670  00170670  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 16 .zdebug_loc   00087fc6  00000000005d6e41  00000000005d6e41  001a3e41  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 17 .zdebug_ranges 00032a70  00000000005eeb07  00000000005eeb07  001bbb07  2**0
                  CONTENTS, READONLY, DEBUGGING, OCTETS
 18 .note.go.buildid 00000064  0000000000400f9c  0000000000400f9c  00000f9c  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA

프로세스의 메모리 레이아웃 확인, 스택 포인터 레지스터의 값(Rsp), 각 변수의 주소를 확인해 본 결과 아래와 같은 사실을 알 수 있다.

  1. 현재 go 코드가 실행되고 있는 위치는 전통적인 스택 세그먼트가 아니다.
    아마 go runtime이 스택 세그먼트 위에서 동작하며, 실제 go 코드를 수행하는 goroutine이 다른 세그먼트에서 동작하는 것으로 보인다.
    • procfs의 stack 세그먼트는 0x00007fff8d006000 ~ 0x00007fff8d027000
    • 현재 스택 포인터 레지스터 값은 0x000000c000108eb0
    • 변수들의 주소 값은 0x000000c000102060 ~ 0x000000c0001021e0
    • 즉, 현재 goroutine이 사용중인 세그먼트는 0x000000c000000000 ~ 0x000000c004000000로 볼 수 있다.
  2. 변수들은 지역변수, 동적할당 변수 관계 없이 자체적인 힙 영역에 할당하는 것으로 보인다.
    • 현재 스택 포인터 레지스터 값은 0x000000c000108eb0
    • 변수들의 주소 값은 0x000000c000102060 ~ 0x000000c0001021e0
    • 변수들의 주소값과 스택 포인터의 값이 약 0x6000 이상의 차이를 보이고 있다.
    • 변수들이 지역변수, 동적할당 변수, 탈출분석에 의한 변수 관계없이 모두 같은 간격으로 배치되어있다. (0x60)
    • 일반적으로 콜 스택이 큰 값에서 작은 값으로 자라고, 변수들이 선언 순서에 따라 주소 값이 커지는 것으로 보아
      변수는 내부적으로 콜 스택에 저장된다고 보기 힘들 수 있다.

혹시 접근하는 변수 타입이 struct라서 무조건 heap을 사용하는 것이고, 기본 자료형을 사용하면 stack에 값이 들어가는지 추가로 확인해봤다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import "fmt"

func main() {
	var num1 int
	var num2 int = *new(int)

	var pInt1 *int = &num1
	var pInt2 *int = &num2
	var pInt3 *int = new(int)
	var pInt4 *int = newint()
	var pInt5 *int

	n := 10
	if n % 5 == 0 {
		// force variable allocation unpredictable
		pInt5 = new(int)
	}

	fmt.Printf("pInt1: %p\n", pInt1)
	fmt.Printf("pInt2: %p\n", pInt2)
	fmt.Printf("pInt3: %p\n", pInt3)
	fmt.Printf("pInt4: %p\n", pInt4)
	fmt.Printf("pInt5: %p\n", pInt5)

	for {
		// Force infinite loop to check in /proc/**/maps
	}
}

func newint() *int {
	var d int = 0
	return &d
}
$ dlv exec ./go_mem_layout_int
Type 'help' for list of commands.
(dlv) b go_mem_layout_int.go:27
Breakpoint 1 (enabled) set at 0x499245 for main.main() ./go_mem_layout_int.go:27
(dlv) continue
pInt1: 0xc00001a0f0
pInt2: 0xc00001a0f8
pInt3: 0xc00001a100
pInt4: 0xc00001a108
pInt5: 0xc00001a110
> main.main() ./go_mem_layout_int.go:27 (hits goroutine(1):1 total:1) (PC: 0x499245)
Warning: debugging optimized function
    24:         fmt.Printf("pInt4: %p\n", pInt4)
    25:         fmt.Printf("pInt5: %p\n", pInt5)
    26:
=>  27:         for {
    28:                 // Force infinite loop to check in /proc/**/map
    29:         }
(dlv) regs
    Rip = 0x0000000000499245
    Rsp = 0x000000c000108eb0
    Rax = 0x0000000000000000
    Rbx = 0x000000c00011e000
    Rcx = 0x0000000000000000
    Rdx = 0x0000000000000000
    Rsi = 0x0000000000000000
    Rdi = 0x0000000000000000
    Rbp = 0x000000c000108f78
     R8 = 0x0000000000000000
     R9 = 0x0000000000000000
    R10 = 0xfffff80000000001
    R11 = 0x0000000000000212
    R12 = 0x000000c0000161b0
    R13 = 0x000000000000003b
    R14 = 0x0000000000000013
    R15 = 0xffffffffffffffff
 Rflags = 0x0000000000000202    [IF IOPL=0]
     Es = 0x0000000000000000
     Cs = 0x0000000000000033
     Ss = 0x000000000000002b
     Ds = 0x0000000000000000
     Fs = 0x0000000000000000
     Gs = 0x0000000000000000
Fs_base = 0x000000000054e5b0
Gs_base = 0x0000000000000000

int의 크기인 8 byte 간격으로 떨어진 것을 보아 기본 자료형이던, struct정의 자료형이던 비슷한 증상이 나타나는 것을 확인할 수 있었다.

나와 같은 질문에 대해 Go언어 문서 FAQ에서는 변수가 스택에 저장되는지, 힙에 저장되는지 알 필요가 없다고 한다. 위 코드의 증상에 대한 설명을 추가하자면, 주소를 사용하는 변수는 힙에 할당될 가능성이 높다고 볼 수 있다.

Go의 메모리 할당에 대한 설명을 읽어보면, Go runtime에서는 전통적인 OS의 스택이 아닌, goroutine만의 콜 스택을 생성하고, 콜 스택이 overflow가 일어나기 전에 더 큰 콜 스택으로 복사하는 작업을 수행한다고 한다. 이 상황에서 지역변수를 포인터로 접근하게 되면, 콜 스택이 복사될 때 해당 포인터들이 댕글링 포인터가 될 수도 있기 때문에 해당 변수들을 힙에 할당할 후보가 되는 것 같다.

해당 글에서 알려준 빌드 옵션 -m -l을 추가해서 컴파일러의 탈출 분석 과정을 확인하면, 모든 인스턴스를 탈출로 판단하여 heap에 넣는 것을 확인할 수 있다.

$ go build -gcflags '-m -l' go_mem_layout_struct.go
# command-line-arguments
./go_mem_layout_struct.go:38:6: moved to heap: d
./go_mem_layout_struct.go:11:6: moved to heap: data1
./go_mem_layout_struct.go:12:6: moved to heap: data2
./go_mem_layout_struct.go:12:23: new(Data) does not escape
./go_mem_layout_struct.go:16:24: new(Data) escapes to heap
./go_mem_layout_struct.go:23:15: new(Data) escapes to heap
./go_mem_layout_struct.go:26:12: ... argument does not escape
./go_mem_layout_struct.go:27:12: ... argument does not escape
./go_mem_layout_struct.go:28:12: ... argument does not escape
./go_mem_layout_struct.go:29:12: ... argument does not escape
./go_mem_layout_struct.go:30:12: ... argument does not escape
$ go build -gcflags '-m -l' go_mem_layout_int.go
# command-line-arguments
./go_mem_layout_int.go:33:6: moved to heap: d
./go_mem_layout_int.go:6:6: moved to heap: num1
./go_mem_layout_int.go:7:6: moved to heap: num2
./go_mem_layout_int.go:7:21: new(int) does not escape
./go_mem_layout_int.go:11:22: new(int) escapes to heap
./go_mem_layout_int.go:18:14: new(int) escapes to heap
./go_mem_layout_int.go:21:12: ... argument does not escape
./go_mem_layout_int.go:22:12: ... argument does not escape
./go_mem_layout_int.go:23:12: ... argument does not escape
./go_mem_layout_int.go:24:12: ... argument does not escape
./go_mem_layout_int.go:25:12: ... argument does not escape

저자 강의


JaeSang Yoo
글쓴이
JaeSang Yoo
The Programmer

목차