Learning Go - Chapter 6. 포인터

choko's avatar
Jun 29, 2024
Learning Go - Chapter 6. 포인터
 
 

포인터

  • 값이 저장된 메모리의 위치 값을 가지고 있는 변수이다.
  • 서로 다른 타입은 서로 다른 수의 메모리를 차지하지만, 모든 포인터는 어떤 타입을 가지든 같은 크기를 가진다.
    • Int64 는 8바이트, bool은 1바이트(최소 주소 저장 공간은 1바이트)를 가진다.
    • 하지만 Int64bool 을 가리키는 포인터는 모두 같은 크기를 가진다. 둘 다 메모리의 주소를 가리키는 것은 똑같기 때문이다.
  • & → 주소 연산자, 변수 앞에 붙히면 메모리 주소를 반환
  • * → 간접 연산자, 포인터 앞에 붙히면 가리키는 값을 반환(역참조)
  • new → 포인터 변수를 생성하는 내장 함수이다.
 
 

포인터는 변경 가능한 파라미터를 가리킨다

  • Go는 값에 의한 호출을 사용하는 언어이다.
    • 포인터 전달로 call by reference가 있다 생각할 수 있는데, 실제론 포인터의 이 전달하는 것이기 때문에, go는 call by value 밖에 없다.
  • 포인터를 사용하지 않을 때, 함수로 전달된 값은 복사된다.
    • 원본이 아니기 때문에, 원본의 불변성을 보장한다.
  • 하지만 포인터가 함수로 전달되면 함수는 포인터의 복사를 얻게 된다.
    • 해당 포인터는 원본 데이터를 가리키고 있기 때문에, 원본을 수정할 수 있다.
 
 

포인터는 최후의 수단이다.

  • 포인터는 데이터 흐름을 이해하기 어렵게 만들고, 가비지 컬렉터에게 추가적인 작업을 준다.
  • 함수로 구조체 전달을 포인터로 하는 것보다, 함수 내에서 구조체를 초기화하고 반환하는 것이 좋다.
    • // 이렇게 말고 func MakeFoo(f *Foo) error { f.Field1 = "val" f.Field2 = 20 return nil } // 이렇게 하자 func MakeFoo() (Foo, error) { f := Foo{ Field1: "val", Field2: 20, } return f, nil }
 
  • 하지만 Jsonunmarshal과 같은, 타입을 알 수 없는 경우 interface{} 포인터를 사용하는 경우는 어쩔수 없이 사용한다.
    • 변수를 수정하기 위해 포인터 파라미터를 사용하는 유일한 경우는 함수가 해당 포인터를 인터페이스로 예상할 때이다.
 
 
 

MapSlice의 차이

  • Map은 구조체를 가리키는 포인터로 구현되어 있다. → 함수로 Map을 넘기는건 포인터를 복사한다는 의미
    • 특히 공용 API에서 파라미터나 반환값으로 map의 사용은 지양해야 한다.
    • 또 map을 그렇게 사용하면, API 자체가 문서화 되는것을 방해한다.
      • map의 key가 무엇인지는 직접 확인하는 수밖에 없다.
    • 이러한 이유로 map으로 넘기기보단 구조체를 사용하기를 지향하자.
 
  • Slice는 3개의 항목을 가지는 구조체로 구현되어 있다
    • 슬라이스의 내용을 수정하는 것은 원본 변수에 반영이 된다.
    • 하지만 append를 통해 길이를 변경하는 것은 원본 변수에 반영되지 않는다.
    • 이는 슬라이스가 다음 세 가지 항목을 가지는 구조체로 구현되어 있기 때문이다.
      • len를 위한 정수 항목
      • cap을 위한 정수 항목
      • 메모리 블록
        • append를 하면, 복사본 slice는 len이 증가하지만, 원본 slice의 len은 그대로다. 따라서, append된 데이터는 슬라이스에 포함되어 리턴되지 않는다.
        •  
           

가비지 컬렉터 작업량 줄이기

  • 가비지(Garbage)는 더 이상 어떤 포인터도 가리키지 않는 데이터를 의미한다.
  • 스택
    • 불연속적인 블록의 메모리로, 스레드 실행 내에 있는 모든 함수의 호출은 같은 스택을 공유한다.
    • 스택 포인터
      • 메모리가 할당된 마지막 위치를 추적
      • 추가적인 메모리 할당은 스택 포인터를 간단히 이동시킴으로 처리
    • 스택 프레임
      • 함수가 실행될 때 함수 데이터들을 위해 생성됨
      • 지역변수, 파라미터들이 저장됨
    • 함수 종료시 함수의 반환값은 스택을 통해 호출 함수로 복사되고, 스택 포인터는 스택 프레임의 초기 위치로 이동시켜 지역변수와 파라미터는 스택 메모리에서 해제된다.
    • 힙은 가비지 컬랙터에 의해 관리되는 메모리이다.
    • 변수가 포인터를 통해 동적인 메모리를 가지고, 컴파일러가 데이터가 스택에 저장될 수 없다고 판단했을 때, 포인터가 가리키는 데이터는 스택을 벗어났고, 해당 데이터는 힙에 저장된다.
    • 힙에 저장된 데이터에서 더 이상 해당 데이터로 가리키는 포인터가 없다면, 그 데이터는 가비지가 되어 가비지 컬랙터의 작업으로 정리된다.
 
  • 가비지 컬랙터의 작업에는 시간이 든다. 가능한, 포인터 사용을 줄이고 스택에 많이 저장하도록 하여 가비지 컬랙터 작업량을 줄이도록 하자.
 
  • 참고 - Go 에서 가비지 컬랙터의 동작 과정(Mark-Sweep)
      1. Marking(표시) - 모든 도달 가능한 객체를 표시한다.
      1. Sweeping(쓸기) - 표시되지 않은 객체는 가비지로 판단되고, 해당 객체의 메모리를 해제한다.
       
       
       
Share article

Tom의 TIL 정리방