ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [golang] Pointer(포인터)를 쓰는 이유
    프로그래밍/golang 2021. 12. 29. 16:31
    반응형
    반응형

    golang에서의 Pointer

    먼저 golang의 컴파일러에 대한 내용을 중심으로 이해를 하고 넘어가야한다.

    설명에 도움이 되는 좋은 문서가 있어 공유하고자 한다.

    https://jacking75.github.io/go_stackheap/

     

    golang - 스택과 힙에 대해 - jacking75

    실행 시 동적으로 메모리를 확보하는 영역으로서 스택과 힙이 있다. 스택 메모리는 함수 호출 스택을 저장하고 로컬 변수, 인수, 반환 값도 여기에 둔다. 스택의 Push와 Pop은 고속이므로 객체를

    jacking75.github.io

     

    1. 포인터 기본

    포인터는 타입이다. 대신 포인터가 가리키는 변수의 메모리 주소를 갖는다. 즉 포인터 변수를 통해 다른 변수의 메모리 주소를 참조해 무언가 가능할 것 같은 느낌이다.

     

    golang에서는 제한된 기준안에서 사용을 할 수 있도록 설정되어 있다. 메모리 주소의 직접 대입이나 포인터의 연산을 허용하지 않는다. 대신 정해진 기준안에서 심플하고 안전하게 메모리의 접근을 허용하는 것이다.

     

    - 메모리의 주소

    - 역참조

     

    이 두가지를 통해 golang의 포인터를 이해할 수 있다.

     

    기본 문법을 통해 확인해보자.

    /* 
    	포인터 선언
    	var 변수명 *타입
    */
    var p *int

    선언한 포인터 변수에 다른 변수의 주소값을 대입한다. 주의할 점은 포인터가 가리키는 대상의 타입과 일치해야한다.

    var example int
    var pointer *int // pointer가 가리키는 example의 int타입과 일치해야한다.
    pointer = &example

    pointer에 example 변수의 주소값을 대입하기 위해선 &를 붙이면 된다.

     

    golang에서 변수의 앞에 &을 붙이면 메모리의 주소를 나타낸다. 예제로 출력해보면 확인해 볼 수 있다.

    package main
    
    import "fmt"
    
    func main() {
    	var example int = 3
    	fmt.Println(&example)
    }

    실행결과

    $ go run main.go
    0xc000000000

    example 변수의 메모리 주소값이 출력된다.

    그럼 이제 포인터가 가르키는 변수의 메모리 주소와 실제 변수의 메모리 주소가 같은지 확인해보자.

    package main
    
    import "fmt"
    
    func main() {
    	var example int
    	var pointer *int
    
    	pointer = &example
    
    	// example 의 메모리 주소값을 출력
    	fmt.Println(&example)
    	// example을 가리키는 포인터의 값을 출력
    	fmt.Println(pointer)
    }

    example 변수의 메모리 주소값를 출력하고 pointer 변수의 값을 출력해보자.

    $ go run main.go
    0xc0000ba000
    0xc0000ba000

    같은 메모리 주소값이 출력되는 걸 확인할 수 있다.

    그럼 메모리의 주소값을 통해 포인터 변수로 example 변수의 value를 조작해본다.

     

    현재 선언된 포인터변수 pointer의 앞에 *를 붙이면 pointer가 가리키는 메모리주소의 value값에 접근할 수 있다. 

    우리는 그것을 역참조라고 한다.

    package main
    
    import "fmt"
    
    func main() {
    	var example int
    	var pointer *int
    	example = 3
    
    	// example의 값을 출력
    	fmt.Println(example)
    	pointer = &example
    
    	// 포인터의 역참조 (*포인터변수명 = 대입값)
    	*pointer = 5
    
    	// example의 값이 바뀌었을까?
    	fmt.Println(example)
    }

    실행결과

    $ go run main.go
    3
    5

    example의 값이 5로 바뀐 것을 확인할 수 있다. 포인터의 역참조를 통해 메모리 주소로 value값 접근이 가능할 수 있었다.

     

    2. 포인터를 사용하는 이유

    일단 예제를 통해 golang의 함수의 파라미터의 원리를 파악해보자.

    package main
    
    import "fmt"
    
    type SomeItem struct {
    	id   int
    	name string
    }
    
    // 구조체의 값을 그대로 인자로 받는 함수
    func EditName(item SomeItem) {
    	item.name = "도자기"
    	fmt.Println(&item.name)
    }
    
    // 포인터를 인자로 받는 함수
    func EditNamePointer(item *SomeItem) {
    	item.name = "호리병"
    	fmt.Println(&item.name)
    }
    
    func main() {
    
    	item := SomeItem{1, "항아리"}
    	fmt.Println("--- 객체이름 변수의 주소와 원래의 값 ---")
    	fmt.Println(&item.name)
    	fmt.Println(item.name)
    
    	// 함수의 파라미터로 item 구조체를 전달
    	fmt.Println("--- 객체이름 변경 함수의 주소와 예상 변경값 ---")
    	EditName(item)
    	fmt.Println(item.name)
    
    	// 포인터를 함수의 파라미터로 전달
    	fmt.Println("--- 객체이름 변경 함수의 주소와 예상 변경값(포인터 사용) ---")
    	EditNamePointer(&item)
    	fmt.Println(item.name)
    }

    실행결과

    $ go run main.go
    --- 객체이름 변수의 주소와 원래의 값 ---
    0xc0000ac020
    항아리
    --- 객체이름 변경 함수의 주소와 예상 변경값 ---
    0xc0000ac038
    항아리
    --- 객체이름 변경 함수의 주소와 예상 변경값(포인터 사용) ---
    0xc0000ac020
    호리병

     

    go언어는 기본적으로 값에 의한 호출(Call by value)이므로 매개변수를 복사해서 함수 내부로 전달하는데 포인터를 통해 본래의 값을 변경할 수 있다.

     

    EditName 함수는 SomeItem{1, "항아리"}로 초기화된 구조체를 통으로 받아 기존 item과 다른 새로운 주소의 item 변수에 복사되어 새로운 메모리주소를 갖고 있다. 즉, 자기의 함수 내부에서 사용된 변수의 값만 바뀌고 함수의 생명주기에 의해 중괄호 닫힘과 쓸모가 없어졌다.

     

    EditNamePointer 함수는 SomeItem{1, "항아리"} 객체 item의 메모리 주소를 통해 포인터 변수가 생성되어 인자로 전해지며 item의 name 필드의 값을 수정할 수 있었으며 동일한 메모리 주소를 확인할 수가 있다.

     

    반응형

    댓글

Designed by Tistory.