Package unsafe

Package unsafe

Go에는 컴파일러가 알고 있고, "unsafe"라는 import path로 접근할 수 있는 내장 패키지 unsafe가 있어요. 이 패키지는 타입 시스템을 위반하는 연산을 포함한 저수준 프로그래밍(low-level programming) 기능을 제공해요. unsafe를 쓰는 패키지는 타입 안전성(type safety)을 반드시 수동으로 검증해야 하고, 이식성(portable)을 보장하지 못해요. 이 패키지는 다음과 같은 인터페이스를 제공합니다.

package unsafe

type ArbitraryType int  // shorthand for an arbitrary Go type; it is not a real type
type Pointer *ArbitraryType

func Alignof(variable ArbitraryType) uintptr
func Offsetof(selector ArbitraryType) uintptr
func Sizeof(variable ArbitraryType) uintptr

type IntegerType int  // shorthand for an integer type; it is not a real type
func Add(ptr Pointer, len IntegerType) Pointer
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
func SliceData(slice []ArbitraryType) *ArbitraryType
func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte

Pointer포인터 타입이지만, Pointer 값은 역참조(dereference)할 수 없어요. 기저 타입(underlying type)uintptr인 어떤 포인터나 값도 변환을 통해 기저 타입이 Pointer인 타입이 될 수 있고, 그 반대도 마찬가지예요. 해당 타입들이 타입 매개변수라면, 각각의 타입 집합에 속한 모든 타입이 같은 기저 타입을 가져야 하며, 그 기저 타입은 각각 uintptrPointer여야 해요. Pointeruintptr 사이의 변환이 어떤 효과를 내는지는 구현에 따라 달라요(implementation-defined).

var f float64
bits = *(*uint64)(unsafe.Pointer(&f))

type ptr unsafe.Pointer
bits = *(*uint64)(ptr(&f))

func f[P ~*B, B any](p P) uintptr {
	return uintptr(unsafe.Pointer(p))
}

var p ptr = nil

AlignofSizeof 함수는 어떤 타입의 표현식 x를 받아서, var v = xv를 선언했다고 가정했을 때 가상의 변수 v정렬(alignment) 또는 **크기(size)**를 각각 돌려줘요.

Offsetof 함수는 (괄호로 감쌀 수도 있는) 셀렉터 s.f를 받아요. 여기서 s 또는 *s가 나타내는 구조체의 필드 f옵셋(offset) 을 구조체 주소 기준 바이트 단위로 돌려줘요. f임베디드 필드(embedded field)라면, 구조체의 필드들을 통한 포인터 간접 참조 없이 도달 가능해야 해요. 필드 f를 가진 구조체 s에 대해 다음 식이 성립해요.

uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))

컴퓨터 아키텍처에 따라 메모리 주소가 정렬(align) 되어야 할 수 있어요. 즉, 변수의 주소가 어떤 인자(factor)의 배수가 되어야 하는데, 그 인자가 바로 변수 타입의 정렬이에요. Alignof 함수는 어떤 타입의 변수를 나타내는 표현식을 받아서, 그 (타입의) 변수의 정렬을 바이트 단위로 돌려줘요. 변수 x에 대해 다음이 성립해요.

uintptr(unsafe.Pointer(&x)) % unsafe.Alignof(x) == 0

타입 T타입 매개변수이거나, 변수 크기를 가진 요소나 필드를 포함하는 배열 또는 구조체 타입이라면, (그 타입의 변수는) 변수 크기(variable size) 를 가져요. 그 외의 경우 크기는 상수(constant) 예요. Alignof, Offsetof, Sizeof에 대한 호출은, 그 인자들이(또는 Offsetof의 셀렉터 식 s.f에서 구조체 s가) 상수 크기의 타입이라면, 타입 uintptr컴파일 타임 상수 표현식 이에요.

Add 함수는 ptrlen을 더하고 갱신된 포인터 unsafe.Pointer(uintptr(ptr) + uintptr(len))를 돌려줘요 [Go 1.17]. len 인자는 정수 타입이거나 타입이 정해지지 않은(untyped) 상수여야 해요. 상수 len 인자는 int 타입 값으로 표현 가능해야 하고, 타입이 정해지지 않은 상수라면 int 타입이 부여돼요. Pointer유효한 사용 규칙은 여전히 적용돼요.

Slice 함수는 기저 배열(underlying array)이 ptr에서 시작하고 길이와 용량이 len인 슬라이스를 돌려줘요. Slice(ptr, len)은 다음 식과 동일해요.

(*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]

다만 특수한 경우로, ptrnil이고 len이 0이면 Slicenil을 돌려줘요 [Go 1.17].

len 인자는 정수 타입이거나 타입이 정해지지 않은 상수여야 해요. 상수 len 인자는 음수가 아니어야 하고 int 타입 값으로 표현 가능해야 하며, 타입이 정해지지 않은 상수라면 int 타입이 부여돼요. 실행 시점(run time)에 len이 음수이거나, ptrnil인데 len이 0이 아니라면 런타임 패닉(run-time panic)이 발생해요 [Go 1.17].

SliceData 함수는 slice 인자의 기저 배열을 가리키는 포인터를 돌려줘요. 슬라이스의 용량 cap(slice)가 0이 아니라면 그 포인터는 &slice[:1][0]이에요. slicenil이면 결과는 nil이에요. 그 외의 경우에는 지정되지 않은 메모리 주소를 가리키는 nil이 아닌 포인터예요 [Go 1.20].

String 함수는 기저 바이트가 ptr에서 시작하고 길이가 lenstring 값을 돌려줘요. ptrlen 인자에는 Slice 함수와 같은 요구사항이 적용돼요. len이 0이면 결과는 빈 문자열 ""이에요. Go 문자열은 불변(immutable)이므로 String에 전달된 바이트는 그 이후에 수정하면 안 돼요 [Go 1.20].

StringData 함수는 str 인자의 기저 바이트를 가리키는 포인터를 돌려줘요. 빈 문자열에 대해서는 반환 값이 지정되지 않으며 nil일 수도 있어요. Go 문자열은 불변이므로 StringData가 반환한 바이트를 수정하면 안 돼요 [Go 1.20].

더 알아보기

  • unsafe 패키지의 실제 함수 사용법과 Pointer 규칙은 Go 문서 /pkg/unsafe에서 확인해요.
  • 이번 섹션은 Go Specification의 System considerations 절에 딸린 Package unsafe 단락이에요.

출처: Go Specification