unsafe
Địa chỉ tài liệu chính thức: unsafe package - unsafe - Go Packages
Thư viện chuẩn unsafe là một thư viện do chính thức cung cấp có thể thực hiện lập trình cấp thấp, các thao tác mà gói này cung cấp có thể bỏ qua hệ thống kiểu của Go để đọc ghi bộ nhớ. Gói này có thể không có tính di động, và chính thức tuyên bố gói này không được bảo vệ bởi quy tắc tương thích Go 1. Dù vậy, unsafe vẫn được sử dụng trong rất nhiều dự án, trong đó cũng bao gồm thư viện chuẩn do chính thức cung cấp.
TIP
Lý do không thể di động là vì kết quả của một số thao tác phụ thuộc vào triển khai của hệ điều hành, các hệ thống khác nhau có thể có kết quả khác nhau.
ArbitraryType
type ArbitraryType intArbitrary có thể dịch là tùy ý, ở đây đại diện cho bất kỳ kiểu nào, và không tương đương với any, thực tế kiểu này không thuộc gói unsafe, xuất hiện ở đây chỉ vì mục đích tài liệu.
IntegerType
type IntegerType intIntegerType đại diện cho bất kỳ kiểu số nguyên nào, thực tế kiểu này không thuộc gói unsafe, xuất hiện ở đây chỉ vì mục đích tài liệu.
Hai kiểu trên không cần quá để ý, chúng chỉ là đại diện mà thôi, khi sử dụng hàm của gói unsafe trình biên tập thậm chí sẽ nhắc bạn kiểu không khớp, kiểu thực tế của chúng chính là kiểu cụ thể bạn truyền vào.
Sizeof
func Sizeof(x ArbitraryType) uintptrTrả về kích thước của biến x theo đơn vị byte, không bao gồm kích thước của nội dung được tham chiếu, ví dụ:
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
16Offsetof
func Offsetof(x ArbitraryType) uintptrHàm này dùng để biểu thị độ lệch trường trong struct, vì vậy x phải là một trường struct, hoặc nói cách khác giá trị trả về chính là số byte giữa địa chỉ bắt đầu của struct và địa chỉ bắt đầu của trường, ví dụ
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
8Alignof
Nếu không hiểu căn chỉnh bộ nhớ là gì, có thể đến xem: Giải thích chi tiết về căn chỉnh bộ nhớ trong Go
func Alignof(x ArbitraryType) uintptrKích thước căn chỉnh thông thường là giá trị nhỏ nhất giữa độ dài từ máy tính theo đơn vị byte và Sizeof, ví dụ trên máy amd64, độ dài từ là 64 bit, tức là 8 byte, ví dụ:
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))
}8 24
8 16
4 4Pointer
type Pointer *ArbitraryTypePointer là một loại "con trỏ" có thể trỏ đến bất kỳ kiểu nào, kiểu của nó là *ArbitraryType, kiểu này kết hợp với uintptr mới có thể phát huy sức mạnh thực sự của gói unsafe. Trong mô tả của tài liệu chính thức, kiểu unsafe.Pointer có thể thực hiện bốn thao tác đặc biệt, lần lượt là:
- Bất kỳ con trỏ kiểu nào cũng có thể chuyển đổi thành
unsafe.Pointer unsafe.Pointercó thể chuyển đổi thành bất kỳ con trỏ kiểu nàouintptrcó thể chuyển đổi thànhunsafe.Pointerunsafe.Pointercó thể chuyển đổi thànhuintptr
Bốn thao tác đặc biệt này cấu thành nền tảng của toàn bộ gói unsafe, cũng chính là bốn thao tác này mới có thể viết được code bỏ qua hệ thống kiểu để đọc ghi bộ nhớ trực tiếp, khuyên khi sử dụng nên đặc biệt chú ý.
TIP
unsafe.Pointer không thể giải tham chiếu, tương tự cũng không thể lấy địa chỉ.
(1) Chuyển đổi *T1 thành unsafe.Pointer rồi chuyển đổi thành *T2
Hiện có kiểu *T1, *T2, giả sử T2 không lớn hơn T1 và bố cục bộ nhớ của cả hai tương đương, thì cho phép chuyển đổi dữ liệu kiểu T2 này thành T1. Ví dụ:
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.3Hai hàm này thực tế là hai hàm trong gói math, biến đổi kiểu trong quá trình như sau
float64 -> *float64 -> unsafe.Pointer -> *uint64 -> uint64 -> *uint64 -> unsafe.Pointer -> *float64 -> float64(2) Chuyển đổi unsafe.Pointer thành uintptr
Khi chuyển đổi unsafe.Pointer thành uintptr, sẽ lấy địa chỉ mà cái trước trỏ đến làm giá trị của cái sau, uintptr lưu trữ là địa chỉ, điểm khác biệt là, cái trước về mặt cú pháp là một con trỏ, là một tham chiếu, cái sau chỉ là một giá trị số nguyên. Ví dụ
func main() {
num := 1
fmt.Println(unsafe.Pointer(&num))
fmt.Printf("0x%x", uintptr(unsafe.Pointer(&num)))
}0xc00001c088
0xc00001c088Điểm khác biệt lớn hơn là xử lý thu gom rác, vì unsafe.Pointer là một tham chiếu, khi cần sẽ không bị thu hồi, còn cái sau chỉ là một giá trị, tự nhiên sẽ không có đãi ngộ đặc biệt này, điểm cần chú ý khác là khi địa chỉ phần tử mà con trỏ trỏ đến di chuyển, GC sẽ cập nhật địa chỉ cũ mà con trỏ tham chiếu, nhưng sẽ không cập nhật giá trị mà uintptr lưu trữ. Ví dụ code dưới đây có thể gặp vấn đề:
func main() {
num := 16
address := uintptr(unsafe.Pointer(&num))
np := (*int64)(unsafe.Pointer(address))
fmt.Println(*np)
}Khi một số trường hợp, GC di chuyển biến sau, địa chỉ mà address trỏ đến đã không còn hiệu lực, lúc này lại sử dụng giá trị đó để tạo con trỏ sẽ gây ra panic
panic: runtime error: invalid memory address or nil pointer dereferenceVì vậy không khuyên lưu trữ giá trị sau khi chuyển đổi Pointer thành uintptr.
(3) Chuyển đổi thành unsafe.Pointer thông qua uintptr
Cách dưới đây có thể nhận được một con trỏ thông qua uintptr, chỉ cần con trỏ có hiệu lực thì sẽ không xuất hiện tình trạng địa chỉ không hợp lệ như ví dụ hai. Pointer và con trỏ kiểu bản thân không hỗ trợ toán tử con trỏ, nhưng uintptr chỉ là một giá trị số nguyên, có thể thực hiện toán toán học, sau khi thực hiện toán toán học với uintptr rồi chuyển đổi thành Pointer là có thể hoàn thành toán tử con trỏ.
p = unsafe.Pointer(uintptr(p) + offset)Như vậy, chỉ thông qua một con trỏ là có thể truy cập được một số phần tử nội bộ của kiểu, ví dụ mảng và struct, bất kể phần tử nội bộ của nó có được expose ra ngoài hay không, ví dụ
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
2Add
func Add(ptr Pointer, len IntegerType) PointerAdd sẽ trả về Pointer sau khi cập nhật sử dụng độ lệch len, tương đương với Pointer(uintptr(ptr) + uintptr(len))
Pointer(uintptr(ptr) + uintptr(len))Ví dụ:
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
3SliceData
func SliceData(slice []ArbitraryType) *ArbitraryTypeHàm này nhận một slice, trả về địa chỉ bắt đầu của mảng cơ sở của nó. Nếu không sử dụng SliceData, thì chỉ có thể lấy địa chỉ của mảng cơ sở thông qua con trỏ của phần tử đầu tiên của nó, như sau
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)
}
}Tất nhiên cũng có thể lấy thông qua kiểu reflect.SliceHeader, nhưng sau phiên bản 1.20 nó đã bị loại bỏ, SliceData chính là để thay thế nó, ví dụ sử dụng SliceData như sau
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
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryTypeHàm Slice nhận một con trỏ, và độ lệch độ dài, nó sẽ trả về hình thức biểu diễn slice của đoạn bộ nhớ đó, quá trình không liên quan đến sao chép bộ nhớ, việc sửa đổi slice sẽ ảnh hưởng trực tiếp đến dữ liệu trên địa chỉ đó, ngược lại cũng vậy, nó thường được sử dụng phối hợp với SliceData.
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]Việc sửa đổi dữ liệu của slice numsRef1 sẽ khiến dữ liệu của nums cũng thay đổi
StringData
func StringData(str string) *byteGiống hàm SliceData, chỉ vì nhu cầu chuyển đổi chuỗi sang slice byte khá thường xuyên, nên tách riêng ra, ví dụ sử dụng như sau
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))
}
}Vì literal chuỗi được lưu trữ trong đoạn chỉ đọc của tiến trình, nên nếu bạn ở đây cố gắng sửa đổi dữ liệu cơ sở của chuỗi, chương trình sẽ trực tiếp crash báo fatal. Nhưng đối với biến chuỗi được lưu trữ trên heap stack, việc sửa đổi dữ liệu cơ sở của nó trong thời gian chạy là hoàn toàn khả thi.
String
func String(ptr *byte, len IntegerType) stringGiống hàm Slice, nhận một con trỏ kiểu byte, và độ lệch độ dài của nó, trả về hình thức biểu diễn chuỗi của nó, quá trình không liên quan đến sao chép bộ nhớ. Dưới đây là một ví dụ chuyển đổi slice byte sang chuỗi
func main() {
bytes := []byte("hello world")
str := unsafe.String(unsafe.SliceData(bytes), len(bytes))
fmt.Println(str)
}StringData và String không liên quan đến sao chép bộ nhớ trong quá trình chuyển đổi giữa chuỗi và slice byte, hiệu suất tốt hơn so với chuyển đổi kiểu trực tiếp, nhưng chỉ áp dụng trong trường hợp chỉ đọc, nếu bạn định sửa đổi dữ liệu, thì tốt nhất không nên dùng cái này.
