Golang unsafe 轉換二進制資料

unsafe 包簡介

Go是一個記憶體安全的程式語言,c語言讓我們可以直接操控記憶體,好處是減去了中間處理記憶體的成本從而加速了程式,而Go也提供了一個unsafe包讓我們能直接操作記憶體。

unsafe包僅有三個函式及一個型別,函式分別為Sizeof(接收任何型態的變數,回傳該變數使用了多少bytes)、Offsetof(接收一個struct的一個欄位,回傳從struct開頭到那個欄位的bytes)、Alignof(接收一個欄位或變數,回傳該類型需要的bytes對齊數);一個型別是Pointer,注意unsafe.Pointer 與uintptr不是同一種型別,但可以互相轉換。

既然包的名稱都叫unsafe了,通常情況下我們不會在程式中使用它,unsafe包的使用時機通常是為了與其他系統的互通性,例如和操作系統(OS)的緊密配合、或是提高性能,下述的用unsafe轉換二進制資料的例子即可以提高程式的性能。但是它是不安全的,可能因程式的bug產生奇怪的錯誤,同時也不保證與未來版本的兼容性。

二進制的轉換程式

這是一個處理網路傳來的封包的程式,依據某一個protocal(定義如下面的DataPackage),我們先來直接看程式,下面再說明:

(網路讀取的資料例如: [ 7 182 93 89 212 111 110 93 21 0 0 0 0 0 1 0] 共 16 bytes

type DataPackage struct {
  Value uint32 //4 bytes
  Title [10]byte //10 bytes
  Enabled bool //1 byte
}

//使用golang其他安全的包來處理
func DataConvert(b [16]byte) DataPackage {
  result := DataPackage{}
  result.Value = binary.BigEndian.Uint32(b[:4}) //將大端序的bytes資料存成系統中定義的uint32資料
  copy(result.Title[:],b[4:14])
  result.Enabled = b[14] != 0   //從byte轉成 true or false
  return result
}
      
//使用unsafe 包來處理
func DataConvertUnsafe(b [16]byte) DataPackage {
  result :=*(*DataPackage)(unsafe.Pointer(&b))
  if isLittleEndian {
    result.Value = bits.ReverseBytes32(result.Value)
  }
  return result
}

//--------------------------------------------------------------                                           
//以下用來判斷電腦是大端序還是小端序編碼                                           
var isLittleEndian bool
                         
func init(){
  var x uint16 = 0xFF00
  xbyte := *(*[2]byte)(unsafe.Pointer(&x))
  isLittleEndian = (xbyte[0] == 0x00)
}

程式說明

  1. 其實主要需要說明的部分在於這一句 : *(*DataPackage)(unsafe.Pointer(&b));我們要從後面說明起:
    • &b: 將b這個變數(為一個16個byte的array)的指標(uintptr型態) 列出來
    • unsafe.Pointer(unitptr)會將unitptr指標轉換成 unsafe.Pointer指標這種型態,讓我們可以對記憶體進行直接操作
    • (*DataPackage)unsafe.Pointer則是將一個Pointer指標直接轉成 DataPackage這種型態的指標(利用* 來轉成指標)
    • 最後再用* 將轉換成的指標轉回原本的資料內容,也就是DataPackage type的資料
  2. 相較於不使用unsafe包的程式,需要每個資料的存到result中,使用unsafe包的程式利用記憶體轉換直接完成將資料存到result中的目的。速度在某些機器上可快兩倍 (不同機器需要實測)。
  3. 網路上傳輸的資料通常都是大端序的,而電腦上則會有小端序或大端序的不同,以一個值為 0xFF00的數字x,在大端序中記憶體會存為[FF 00],小端序則存為[00 FF],因此我們利用*(*[2]byte)(unsafe.Pointer(&x))將x強制轉成一個2 bytes的array, 觀察其[0] 的數字是否為00來判斷系統是否為小端序。
  4. 註: init()函數為Package被引入時會被自動執行的函數

雖然上述例子有帶來更快的程式的好處,但考量到程式的容易閱讀性及可能的不安全性,除非真的很有效能的考量,否則還真的不建議使用unsafe呢。