Skip to content

unsafe

공식 문서: unsafe package - unsafe - Go Packages

unsafe 표준 라이브러리는 공식에서 제공하는 저수준 프로그래밍을 위한 라이브러리로, 해당 패키지가 제공하는 연산을 통해 Go 타입 시스템을 우회하여 메모리를 직접 읽고 쓸 수 있습니다. 이 패키지는 이식성이 없을 수 있으며, 공식에서는 이 패키지가 Go 1 호환성 기준의 보호를 받지 못한다고 명시합니다. 그럼에도 불구하고 unsafe 는 많은 프로젝트에서 사용되며, 공식 표준 라이브러리에서도 사용됩니다.

TIP

이식성이 없는 이유는 일부 연산의 결과가 운영체제 구현에 따라 달라지며, 다른 시스템에서는 다른 결과가 나올 수 있기 때문입니다.

ArbitraryType

go
type ArbitraryType int

Arbitrary 는 임의의라고 번역할 수 있으며, 여기서는 임의의 유형을 나타냅니다. any 와 동일하지 않으며, 실제로 이 유형은 unsafe 패키지에 속하지 않습니다. 여기에 나타나는 것은 단순히 문서 목적을 위한 것입니다.

IntegerType

go
type IntegerType int

IntegerType 은 임의의 정수 유형을 나타냅니다. 실제로 이 유형은 unsafe 패키지에 속하지 않으며, 여기에 나타나는 것은 단순히 문서 목적을 위한 것입니다.

위 두 유형은 너무 신경 쓸 필요가 없습니다. 이들은 단지 대표일 뿐이며, unsafe 패키지 함수를 사용할 때 에디터가 유형 불일치를 알려줄 수도 있습니다. 이들의 실제 유형은 사용자가 전달한 구체적인 유형입니다.

Sizeof

go
func Sizeof(x ArbitraryType) uintptr

변수 x 의 크기를 바이트 단위로 반환하며, 참조된 콘텐츠의 크기는 포함하지 않습니다. 예를 들어:

go
func main() {
  var ints byte = 1
  fmt.Println(unsafe.Sizeof(ints))

  var floats float32 = 1.0
  fmt.Println(unsafe.Sizeof(floats))

  var complexs complex128 = 1 + 2i
  fmt.Println(unsafe.Sizeof(complexs))

  var slice []int = make([]int, 100)
  fmt.Println(unsafe.Sizeof(slice))

  var mp map[string]int = make(map[string]int, 0)
  fmt.Println(unsafe.Sizeof(mp))

  type person struct {
    name string
    age  int
  }
  fmt.Println(unsafe.Sizeof(person{}))

  type man struct {
    name string
  }
  fmt.Println(unsafe.Sizeof(man{}))
}
1
4
16
24
8
24
16

Offsetof

go
func Offsetof(x ArbitraryType) uintptr

이 함수는 구조체 내 필드 오프셋을 나타내는 데 사용되므로, x 는 반드시 구조체 필드여야 합니다. 즉, 반환 값은 구조체 주소 시작 위치에서 필드 주소 시작 위치까지의 바이트 수입니다. 예를 들어

go
func main() {
   type person struct {
      name string
      age  int
   }
   p := person{
      name: "aa",
      age:  11,
   }
   fmt.Println(unsafe.Sizeof(p))
   fmt.Println(unsafe.Offsetof(p.name))
   fmt.Println(unsafe.Sizeof(p.name))
   fmt.Println(unsafe.Offsetof(p.age))
   fmt.Println(unsafe.Sizeof(p.age))
}
24
0
16
16
8

Alignof

메모리 정렬이 무엇인지 잘 모르는 경우 다음 문서를 참조하세요: Go 언어 메모리 정렬 상세 설명

go
func Alignof(x ArbitraryType) uintptr

정렬 크기는 일반적으로 바이트 단위의 컴퓨터 워드 길이와 Sizeof 의 최솟값입니다. 예를 들어 amd64 머신에서 워드 길이는 64 비트이며, 이는 8 바이트입니다. 예를 들어:

go
func main() {
  type person struct {
    name string
    age  int32
  }
  p := person{
    name: "aa",
    age:  11,
  }
  fmt.Println(unsafe.Alignof(p), unsafe.Sizeof(p))
  fmt.Println(unsafe.Alignof(p.name), unsafe.Sizeof(p.name))
  fmt.Println(unsafe.Alignof(p.age), unsafe.Sizeof(p.age))
}
go
8 24
8 16
4 4

Pointer

go
type Pointer *ArbitraryType

Pointer 는 임의의 유형을 가리킬 수 있는 "포인터"이며, 그 유형은 *ArbitraryType 입니다. 이 유형은 uintptr 와 함께 사용되어야만 unsafe 패키지의 진정한 위력을 발휘할 수 있습니다. 공식 문서 설명에 따르면, unsafe.Pointer 유형은 네 가지 특수 연산을 수행할 수 있습니다.

  • 모든 유형의 포인터를 unsafe.Pointer 로 변환할 수 있습니다.
  • unsafe.Pointer 를 모든 유형의 포인터로 변환할 수 있습니다.
  • uintptrunsafe.Pointer 로 변환할 수 있습니다.
  • unsafe.Pointeruintptr 로 변환할 수 있습니다.

이 네 가지 특수 연산은 전체 unsafe 패키지의 기반을 구성하며, 바로 이 네 가지 연산을 통해 타입 시스템을 무시하고 메모리를 직접 읽고 쓰는 코드를 작성할 수 있습니다. 사용 시 각별히 주의해야 합니다.

TIP

unsafe.Pointer 는 역참조할 수 없으며, 주소를 취할 수도 없습니다.

(1) *T1unsafe.Pointer 로 변환한 후 *T2 로 변환

*T1, *T2 유형이 있다고 가정합니다. T2T1 보다 크지 않고 두 유형의 메모리 레이아웃이 동일한 경우, T2 유형의 데이터를 T1 으로 변환할 수 있습니다. 예를 들어:

go
func main() {
   fmt.Println(Float64bits(12.3))
   fmt.Println(Float64frombits(Float64bits(12.3)))
}

func Float64bits(f float64) uint64 {
   return *(*uint64)(unsafe.Pointer(&f))
}

func Float64frombits(b uint64) float64 {
   return *(*float64)(unsafe.Pointer(&b))
}
4623113902481840538
12.3

이 두 함수는 실제로 math 패키지의 두 함수이며, 과정 중의 유형 변화는 다음과 같습니다.

float64 -> *float64 -> unsafe.Pointer ->  *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64

(2) unsafe.Pointeruintptr 로 변환

unsafe.Pointeruintptr 로 변환할 때, 전자가 가리키는 주소가 후자의 값이 됩니다. uintptr 는 주소를 저장하지만, 전자는 문법적으로 포인터이며 참조인 반면, 후자는 단순히 정수 값일 뿐입니다. 예를 들어

go
func main() {
   num := 1
   fmt.Println(unsafe.Pointer(&num))
   fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}
0xc00001c088
0xc00001c088

더 큰 차이는 가비지 컬렉션 처리에 있습니다. unsafe.Pointer 는 참조이므로 필요할 때 회수되지 않지만, 후자는 단순히 값이므로 이러한 특별한 대우를 받지 못합니다. 또 다른 주의할 점은 포인터가 가리키는 요소 주소가 이동할 때 GC 는 포인터가 참조하는 이전 주소를 업데이트하지만, uintptr 가 저장한 값은 업데이트하지 않습니다. 예를 들어 다음 코드는 문제가 발생할 수 있습니다.

go
func main() {
   num := 16
   address := uintptr(unsafe.Pointer(&num))
   np := (*int64)(unsafe.Pointer(address))
   fmt.Println(*np)
}

일부 상황에서 GC 가 변수를 이동하면 address 가 가리키는 주소는 더 이상 유효하지 않으며, 이때 해당 값을 사용하여 포인터를 생성하면 panic 이 발생합니다.

panic: runtime error: invalid memory address or nil pointer dereference

따라서 Pointeruintptr 로 변환한 값을 저장하는 것은 권장하지 않습니다.

(3) uintptrunsafe.Pointer 로 변환

다음 방식을 통해 uintptr 로 포인터를 얻을 수 있으며, 포인터가 유효한 경우 예제 2 의 무효 주소 상황이 발생하지 않습니다. Pointer 와 유형 포인터는 포인터 연산을 지원하지 않지만, uintptr 는 정수 값일 뿐이므로 수학 연산을 수행할 수 있습니다. uintptr 에 수학 연산을 수행한 후 Pointer 로 변환하면 포인터 연산을 완료할 수 있습니다.

go
p = unsafe.Pointer(uintptr(p) + offset)

이를 통해 단일 포인터만으로도 배열과 구조체와 같은 일부 유형의 내부 요소에 액세스할 수 있으며, 내부 요소가 외부에 노출되었는지 여부와 상관없이 가능합니다. 예를 들어

go
func main() {
  type person struct {
    name string
    age  int32
  }
  p := &person{"jack", 18}
  pp := unsafe.Pointer(p)
  fmt.Println(*(*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.name))))
  fmt.Println(*(*int32)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(p.age))))

  s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
  ps := unsafe.Pointer(&s[0])
  fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 8)))
  fmt.Println(*(*int)(unsafe.Pointer(uintptr(ps) + 16)))
}
jack
18
2

Add

go
func Add(ptr Pointer, len IntegerType) Pointer

Add 는 오프셋 len 으로 업데이트된 Pointer 를 반환하며, Pointer(uintptr(ptr) + uintptr(len)) 와 동일합니다.

go
Pointer(uintptr(ptr) + uintptr(len))

예를 들어:

go
func main() {
   s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
   ps := unsafe.Pointer(&s[0])
   fmt.Println(*(*int)(unsafe.Add(ps, 8)))
   fmt.Println(*(*int)(unsafe.Add(ps, 16)))
}
2
3

SliceData

go
func SliceData(slice []ArbitraryType) *ArbitraryType

이 함수는 슬라이스를 받아底层 배열의 시작 주소를 반환합니다. SliceData 를 사용하지 않는 경우, 첫 번째 요소의 포인터를 가져底层 배열의 주소를 가져올 수 있습니다. 다음과 같습니다.

go
func main() {
  nums := []int{1, 2, 3, 4}
  for p, i := unsafe.Pointer(&nums[0]), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(nums[0])), i+1 {
    num := *(*int)(p)
    fmt.Println(num)
  }
}

물론 reflect.SliceHeader 유형을 사용하여 가져올 수도 있지만, 1.20 버전 이후로는 폐기되었습니다. SliceData 는 이를 대체하기 위한 것입니다. SliceData 사용 예시는 다음과 같습니다.

go
func main() {
  nums := []int{1, 2, 3, 4}
  for p, i := unsafe.Pointer(unsafe.SliceData(nums)), 0; i < len(nums); p, i = unsafe.Add(p, unsafe.Sizeof(int(0))), i+1 {
    num := *(*int)(p)
    fmt.Println(num)
  }
}

Slice

go
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

Slice 함수는 포인터와 길이 오프셋을 받아 해당 메모리의 슬라이스 표현 형태를 반환합니다. 이 과정에서 메모리 복사가 발생하지 않으며, 슬라이스를 수정하면 해당 주소의 데이터에 직접 영향을 미치고 그 반대도 마찬가지입니다. 일반적으로 SliceData 와 함께 사용됩니다.

go
func main() {
  nums := []int{1, 2, 3, 4}
  numsRef1 := unsafe.Slice(unsafe.SliceData(nums), len(nums))
  numsRef1[0] = 2
  fmt.Println(nums)
}
[2 2 3 4]

numsRef1 슬라이스 데이터를 수정하면 nums 의 데이터도 변경됩니다.

StringData

go
func StringData(str string) *byte

SliceData 함수와 동일하지만, 문자열을 바이트 슬라이스로 변환하는 요구사항이 빈번하므로 별도로 분리되었습니다. 사용 예시는 다음과 같습니다.

go
func main() {
  str := "hello,world!"
  for ptr, i := unsafe.Pointer(unsafe.StringData(str)), 0; i < len(str); ptr, i = unsafe.Add(ptr, unsafe.Sizeof(byte(0))), i+1 {
    char := *(*byte)(ptr)
    fmt.Println(string(char))
  }
}

문자열 리터럴은 프로세스의 읽기 전용 세그먼트에 저장되므로, 여기서 문자열底层 데이터를 수정하려고 하면 프로그램이 fatal 오류로 중단됩니다. 하지만 힙이나 스택에 저장된 문자열 변수의 경우, 런타임에底层 데이터를 수정하는 것이 완전히 가능합니다.

String

go
func String(ptr *byte, len IntegerType) string

Slice 함수와 동일하며, 바이트 유형 포인터와 길이 오프셋을 받아 문자열 표현 형태를 반환합니다. 이 과정에서 메모리 복사가 발생하지 않습니다. 다음은 바이트 슬라이스를 문자열로 변환하는 예시입니다.

go
func main() {
  bytes := []byte("hello world")
  str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
  fmt.Println(str)
}

StringDataString 은 문자열과 바이트 슬라이스 변환 과정에서 메모리 복사를 포함하지 않아 성능이 직접 유형 변환보다 좋지만, 읽기 전용 상황에서만 적합합니다. 데이터를 수정하려는 경우 이 함수를 사용하지 않는 것이 좋습니다.

Golang by www.golangdev.cn edit