編譯dll文件(源代碼c++):
g++ -shared main.cpp -o test.dll
set GOARCH=386
第一個DLL函數,第一個參數,要求傳入一個指針,直接指向[]byte類型,注意,是直接指向;第2個參數為[]byte長度;第三個參數為一個直接指向string類型指針;返回一個整數,標識調用成功或失敗。
最折磨我的就是直接指向某種類型的指針傳遞問題,查了N多資料,都是類似下面這樣:
p:= unsafe.Pointer(&dat)
g:=dll32.NewProc("XXX") r, _, _ :=g.Call(uintptr(p),uintptr(cdat),uintptr(pk))
我開始也這樣用,怎么弄都不對,然后我用OD載入調試,發現傳進去的東西根本不是DLL想要的。
這樣傳進去的數據會被2層指針指向,ptrA->ptrB->[]byte,傳進去的是ptrA,所以導致無法正常調用。那么問題來了,怎么才能傳進去的指針直接指向數據,達到類似ptrA->[]byte這樣的效果呢?
問題的重點就在這里,研究了好幾天uintptr發現不是它的問題,問題出在
unsafe.Pointer
它上面,它會在指針外面再包一層指針,怎么解決呢?我只能考慮先把指針轉成整數再傳進去,結果
p:= *((*int32)(unsafe.Pointer(&dat)))
r, _, _ :=g.Call(uintptr(p),uintptr(cdat),uintptr(pk))
這樣成功了。下面傳遞整數指針就簡單多了
cdat:=len(dat)
這樣即可,再后面傳遞字符串指針,指針獲取方式和byte一樣即可。但是問題又來了,執行不成功,繼續OD,發現有問題,問題在於GO語言字符串后面在內存中沒有結尾標志。那GO自己怎么判斷字符串結尾呢?我想應該是每個字符串GO都同時記錄了長度吧,不過不確定,有明白的大神請告知,這個問題我就只能這樣,先把字符串轉換成byte,然后在byte最后加0,類似這樣
keystr:=[]byte{49,50,51,0} pk:= *((*int32)(unsafe.Pointer(&keystr)))
這個問題就解決了,這個字符串就變成windows識別的了。返回值整數,直接就能用,這點我很奇怪,不知道為什么,比如這里,可以直接
r, _, _ =g.Call(uintptr(p),uintptr(cdat),uintptr(pk))
if r!=1 {
按理說,返回的是個指向整數的指針,應該*r才對,不懂,大神告知。
然后現在所有傳遞參數的問題解決了,后面問題又來了,第2個函數,調用后返回值是指向字符串的指針,這個指針指向的內容字符串當然是0結尾的windows格式了,GO依然無法正確讀取。怎么辦呢,只能自己寫了個函數處理這個問題
//根據DLL返回的指針,逐個取出字節,到0字節時判斷為字符串結尾,返回字節數組轉成的字符串 func prttostr(vcode uintptr) string { var vbyte []byte for i:=0;i<10;i++{ sbyte:=*((*byte)(unsafe.Pointer(vcode))) if sbyte==0{ break } vbyte=append(vbyte,sbyte) vcode += 1 } return string(vbyte) }
原理就是操作指針一直向后移動,發現0了就停止,這樣問題解決了,雖然所有的資料和大神都告訴我,不要輕易操作指針,但是這種情況下,不這么弄,怎么弄呢?誰能告訴我。
無論如何問題終於解決了,也許解決的不夠完美,我想應該有更簡單的辦法,但是我不知道。
完整代碼:
package main import ( "syscall" "C" "unsafe" "fmt" ) func main(){ h, err := syscall.LoadLibrary("test.dll") if err != nil { abort("LoadLibrary", err) } defer syscall.FreeLibrary(h) proc, err := syscall.GetProcAddress(h, "SomeFunction") if err != nil { abort("GetProcAddress", err) } var cmsg *C.char = C.CString("EB90EB90EB90EA2101001203090A0431FFFFFFFFFFFFFFFF090201701215000000583E0F000609380300146E0000002D785000003BA41000005AC40E00005EE001000061910300002700000000000A023695121500000054041600060AF2020014A00000002DCC4200003B901000005A5C0D00005E3103000061850300002700000000000B02057012150000001C901C00066F25050014B40000002D887700003BA41000005A920E00005E1102000061910300002700000000000C025273121500000024AE1400069EE9030014960000002D505F00003B9A1000005A740E00005E29020000618E03000027000000000068") var cmsgres *C.char r, _, _ := syscall.Syscall(uintptr(proc),1, uintptr(unsafe.Pointer(cmsg)), uintptr(unsafe.Pointer(&cmsgres)), 0) // var str *string = ((*string)(unsafe.Pointer(r))) // fmt.Println("結果", r, str) // fmt.Println(C.GoString(cmsgres)) fmt.Println(prttostr(r)) } func abort(funcname string, err error) { panic(funcname + " failed: " + err.Error()) } func prttostr(vcode uintptr) string { var vbyte []byte for { sbyte := *((*byte)(unsafe.Pointer(vcode))) if sbyte == 0 { break } vbyte = append(vbyte, sbyte) vcode += 1 } return string(vbyte) }
拓展:
3.Go語言數據類型
本篇接着 Go語言學習筆記4 講Go語言數據類型,主要如下:
3.7 結構體
結構體類型既可以包含若干個命名元素(又稱字段),又可以與若干個方法相關聯。
1.類型表示法
結構體類型的聲明可以包含若干個字段的聲明。字段聲明左邊的標識符表示了該字段的名稱,右邊的標識符代表了該字段的類型,這兩個標識符之間用空格分隔。
結構體類型聲明中的每個字段聲明都獨占一行。同一個結構體類型聲明中的字段不能出現重名的情況。
結構體類型也分為命名結構體類型和匿名結構體類型。
命名結構體類型
命名結構體類型以關鍵字type開始,依次包含結構體類型的名稱、關鍵字struct和由花括號括起來的字段聲明列表。如下:
type Sequence struct { len int cap int Sortable sortableArray sort.Interface }
結構體類型的字段的類型可以是任何數據類型。當字段名稱的首字母是大寫字母時,我們就可以在任何位置(包括其他代碼包)上通過其所屬的結構體類型的值(以下簡稱結構體值)和選擇表達式訪問到它們。否則當字段名稱的首字母是小寫,這些字段就是包級私有的(只有在該結構體聲明所屬的代碼包中才能對它們進行訪問或者給它們賦值)。
如果一個字段聲明中只有類型而沒有指定名稱,這個字段就叫做匿名字段。如上結構體 Sequence中的 Sortable 就是一個匿名字段。匿名字段有時也被稱為嵌入式的字段或結構體類型的嵌入類型。
匿名字段的類型必須由一個數據類型的名稱或者一個與非接口類型對應的指針類型的名稱代表。代表匿名字段類型的非限定名稱將被隱含地作為該字段的名稱。如果匿名字段是一個指針類型的話,那么這個指針類型所指的數據類型的非限定名稱(由非限定標識符代表的名稱)就會被作為該字段的名稱。非限定標識符就是不包含代碼包名稱和點的標識符。
匿名類型的隱含名稱的實例,如下:
type Anonymities struct { T1 *T2 P.T3 *P.T4 }
這個名為 Anonymities 的結構體類型包含了4個匿名字段。其中,T1 和 P.T3 為非指針的數據類型,它們隱含的名稱分別為 T1 和 T3;*T2 和 *P.T4 為指針類型,它們隱含的名稱分別為 T2 和 T4。
注意:匿名字段的隱含名稱也不能與它所屬的結構體類型中的其他字段名稱重復。
結構體類型中的嵌入字段的類型所附帶的方法都會成為該結構體類型的方法,結構體類型自動實現了它包含的所有嵌入類型所實現的接口類型。但是嵌入類型的方法的接收者類型仍然是該嵌入類型,而不是被嵌入的結構體類型。當在結構體類型中調用實際上屬於嵌入類型的方法的時候,這一調用會被自動轉發到這個嵌入類型的值上。
現在對 Sequence 的聲明進行改動,如下:
type Sequence struct { Sortable sorted bool }
上面的 Sequence 中的匿名字段 Sortable 用來存儲和操作可排序序列,布爾類型的字段 sorted 用來表示類型值是否已經被排序。
假設有一個 Sequence 類型的值 seq,調用 Sortable 接口類型中的方法 Sort,如下:
seq.Sort()
如果 Sequence 類型中也包含了一個與 Sortable 接口類型中的方法 Sort 的名稱和簽名相同的方法,那么上面的調用一定是對 Sequence 類型值自身附帶的 Sort 方法的調用,而嵌入類型 Sortable 的方法 Sort 被隱藏了。
如果需要在原有的排序操作上添加一些額外功能,可以這樣聲明一個同名的方法:
func (self *Sequence) Sort() { self.Sortable.Sort() self.sorted = true }
這樣聲明的方法實現了對於匿名字段 Sortable 的 Sort 方法的功能進行無縫擴展的目的。
如果兩個 Sort 方法的名稱相同但簽名不同,那么嵌入類型 Sortable 的方法 Sort 也同樣會被隱藏。這時,在 Sequence 的類型值上調用 Sort 方法的時候,必須依據該 Sequence 結構體類型的 Sort方法的簽名來編寫調用表達式。如下聲明 Sequence 類型附帶的名為 Sort 的方法:
func (self *Sequence) Sort(quicksort bool) { //省略若干語句 }
但是調用表達式 seq.Sort() 就會造成一個編譯錯誤,因為 Sortable 的無參數的 Sort 方法已經被隱藏了,只能通過 seq.Sort(true) 或 seq.Sort(false) 來對 Sequence 的 Sort 方法進行調用。
注意:無論被嵌入類型是否包含了同名的方法,調用表達式 seq.Sortable.Sort() 總是可以來調用嵌入類 Sortable 的 Sort 方法。
現在,區別一下嵌入類型是一個非指針的數據類型還是一個指針類型,假設有結構體類型 S 和非指針類型的數據類型 T,那么 *S 表示指向 S 的指針類型,*T 表示指向 T 的指針類型,則:
-
如果在 S 中包含了一個嵌入類型 T,那么 S 和 *S 的方法集合中都會包含接收者類型為 T 的方法。除此之外,*S 的方法集合中還會包含接收者類型為 *T 的方法。
-
如果在 S 中包含了一個嵌入類型 *T,那么 S 和 *S 的方法集合中都會包含接收者類型為 T 和 *T 的所有方法。
現在再討論另一個問題。假設,我們有一個名為 List 的結構體類型,並且在它的聲明中嵌入了類型 Sequence,如下:
type List struct { Sequence }
假設有一個 List 類型的值 list,調用嵌入的 Sequence 類型值的字段 sorted,如下:
list.sorted
如果 List 類型也有一個名稱為 sorted 的字段的話,那么其中的 Sequence 類型值的字段 sorted就會被隱藏。
注意: 選擇表達式 list.sorted 只代表了對 List 類型的 sorted 字段的訪問,不論這兩個名稱為 sorted 的字段的類型是否相同。和上面的類似,這里選擇表達式 list.Sequence.sorted 總是可以訪問到嵌入類型 Sequence 的值的 sorted 字段。
對於結構體類型的多層嵌入的規則,有兩點需要說明:
-
可以在被嵌入的結構體類型的值上像調用它自己的字段或方法那樣調用任意深度的嵌入類型值的字段或方法。唯一的前提條件就是這些嵌入類型的字段或方法沒有被隱藏。如果它們被隱藏,也可以通過類似 list. Sequence.sorted 這樣的表達式進行訪問或調用它們。
-
被嵌入的結構體類型的字段或方法可以隱藏任意深度的嵌入類型的同名字段或方法。任何較淺層次的嵌入類型的字段或方法都會隱藏較深層次的嵌入類型包含的同名的字段或方法。注意,這種隱藏是可以交叉進行的,即字段可以隱藏方法,方法也可以隱藏字段,只要它們的名稱相同即可。
如果在同一嵌入層次中的兩個嵌入類型擁有同名的字段或方法,那么涉及它們的選擇表達式或調用表達式會因為編譯器不能確定被選擇或調用的目標而造成一個編譯錯誤。
匿名結構體類型
匿名結構體類型比命名結構體類型少了關鍵字type和類型名稱,聲明如下:
struct { Sortable sorted bool }
可以在數組類型、切片類型或字典類型的聲明中,將一個匿名的結構體類型作為他們的元素的類型。還可以將匿名結構體類型作為一個變量的類型,例如:
var anonym struct { a int b string }
不過對於上面,更常用的做法就是在聲明以匿名結構體類型為類型的變量的同時對其初始化,例如:
anonym := struct { a int b string }{0, "string"}
與命名結構體類型相比,匿名結構體類型更像是“一次性”的類型,它不具有通用性,常常被用在臨時數據存儲和傳遞的場景中。
在Go語言中,可以在結構體類型聲明中的字段聲明的后面添加一個字符串字面量標簽,以作為對應字段的附加屬性。例如:
type Person struct { Name string `json:"name"` Age uint8 `json:"age"` Address string `json:"addr"` }
如上的字段的字符串字面量標簽一般有兩個反引號包裹的任意字符串組成。並且,它應該被添加但在與其對應的字段的同一行的最右側。
這種標簽對於使用該結構體類型及其值的代碼來說是不可見的。但是,可以用標准庫代碼包 reflect中提供的函數查看到結構體類型中字段的標簽。這種標簽常常會在一些特殊應用場景下使用,比如,標准庫代碼包 encoding/json 中的函數會根據這種標簽的內容確定與該結構體類型中的字段對應的 JSON 節點的名稱。
2.值表示法
結構體值一般由復合字面量(類型字面量和花括號構成)來表達。在Go語言中,常常將用於表示結構體值的復合字面量簡稱為結構體字面量。在同一個結構體字面量中,一個字段名稱只能出現一次。例如:
Sequence{Sortable: SortableStrings{"3", "2", "1"}, sorted: false}
類型 SortableStrings 實現了接口類型 Sortable,這個可以在Go語言學習筆記4中了解到。這里就可以把一個 SortableStrings 類型的值賦給 Sortable 字段。
編寫結構體字面量,還可以忽略字段的名稱,但有如下的兩個限制:
-
如果想要省略其中某個或某些鍵值對的鍵,那么其他的鍵值對的鍵也必須省略。
Sequence{ SortableStrings{"3", "2", "1"}, sorted: false}//這是不合法的
-
多個字段值之間的順序應該與結構體類型聲明中的字段聲明的順序一致,並且不能夠省略掉任何一字段的賦值。但是不省略字段名稱的字面量卻沒有此限制。例如:
Sequence{ sorted: false , Sortable: SortableStrings{"3", "2", "1"}}//合法 Sequence{SortableStrings{"3", "2", "1"}, false}//合法 Sequence{ Sortable: SortableStrings{"3", "2", "1"}}//合法,未被明確賦值的字段的值會被其類型的零值填充。 Sequence{ false , SortableStrings{"3", "2", "1"}}//不合法,順序不一致,會編譯錯誤 Sequence{ SortableStrings{"3", "2", "1"}}//不合法,順序不一致,會編譯錯誤
在Go語言中,可以在結構體字面量中不指定任何字段的值。例如:
Sequence{}//這種情況下,兩個字段都被賦予它們所屬類型的零值。
與數組類型相同,結構體類型屬於值類型。結構體類型的零值就是如上的不為任何字段賦值的結構體字面量。
3.屬性和基本操作
一個結構體類型的屬性就是它所包含的字段和與它關聯的方法。在訪問權限允許的情況下,我們可以使用選擇表達式訪問結構體值中的字段,也可以使用調用表達式調用結構體值關聯的方法。
在Go語言中,只存在嵌入而不存在繼承的概念。不能把前面聲明的 List 類型的值賦給一個 Sequence 類型的變量,這樣的賦值語句會造成一個編譯錯誤。在一個結構體類型的別名類型的值上,既不能調用那個結構體類型的方法,也不能調用與那個結構體類型對應的指針類型的方法。別名類型不是它源類型的子類型,但別名類型內部的結構會與它的源類型一致。
對於一個結構體類型的別名類型來說,它擁有源類型的全部字段,但這個別名類型並沒有繼承與它的源類型關聯的任何方法。
如果只是將 List 類型作為 Sequence 類型的一個別名類型,那么聲明如下:
type List Sequence
此時,List 類型的值的表示方法與 Sequence 類型的值的表示方法一樣,如下:
List{ SortableStrings{"4", "5", "6"}, false}
如果有一個 List 類型的值 List,那么選擇表達式 list.sorted 訪問的就是這個 List 類型的值的 sorted 字段,同樣,我們也可以通過選擇表達式 list.Sortable 訪問這個值的嵌入字段 Sortable。但是這個 List 類型目前卻不包含與它的源類型 Sequence 關聯的方法。
在Go語言中,雖然很多預定義類型都屬於泛型類型(比如數組類型、切片類型、字典類型和通道類型),但卻不支持自定義的泛型類型。為了使 Sequence 類型能夠部分模擬泛型類型的行為特征,只是向它嵌入 Sortable 接口類型是不夠的,需要對 Sortable 接口類型進行拓展。如下:
type GenericSeq interface { Sortable Append(e interface{}) bool Set(index int, e interface{}) bool Delete(index int) (interface{}, bool) ElemValue(index int) interface{} ElemType() reflect.Type value() interface{} }
如上的接口類型 GenericSeq 中聲明了用於添加、修改、刪除、查詢元素,以及獲取元素類型的方法。一個數據類型要實現 GenericSeq 接口類型,也必須實現 Sortable 接口類型。
現在,將嵌入到 Sequence 類型的 Sortable 接口類型改為 GenericSeq 接口類型,聲明如下:
type Sequence struct { GenericSeq sorted bool elemType reflect.Type }
在如上的類型聲明中,添加了一個 reflect.Type 類型(即標准庫代碼包 reflect 中的 Type 類型)的字段 elemType,目的用它來緩存 GenericSeq 字段中存儲的值的元素類型。
為了能夠在改變 GenericSeq 字段存儲的值的過程中及時對字段 sorted 和 elemType 的值進行修改,如下還創建了幾個與 Sequence 類型關聯的方法。聲明如下:
func (self *Sequence) Sort() { self.GenericSeq.Sort() self.sorted = true } func (self *Sequence) Append(e interface{}) bool{ result := self. GenericSeq.Append(e) //省略部分代碼 self.sorted = true //省略部分代碼 return result } func (self *Sequence) Set(index int, e interface{}) bool { result := self. GenericSeq.Set(index, e) //省略部分代碼 self.sorted = true //省略部分代碼 return result } func (self *Sequence) ElemType() reflect.Type { //省略部分代碼 self.elemType = self.GenericSeq.ElemType() //省略部分代碼 return self.elemType }
如上的這些方法分別與接口類型 GenericSeq 或 Sortable 中聲明的某個方法有着相同的方法名稱和方法簽名。通過這種方式隱藏了 GenericSeq 字段中存儲的值的這些同名方法,並對它們進行了無縫擴展。
GenericSeq 接口類型的實現類型以及 Sequence 類型的完整實現代碼如下:
http://download.csdn.net/download/u012855229/9566546
3.8指針
指針是一個代表着某個內存地址的值。這個內存地址往往是在內存中存儲的另一個變量的值的起始位置。Go語言既沒有像Java語言那樣取消了代碼對指針的直接操作的能力,也避免了C/C++語言中由於對指針的濫用而造成的安全和可靠性問題。
Go語言的指針類型指代了指向一個給定類型的變量的指針。它常常被稱為指針的基本類型。指針類型是Go語言的復合類型之一。
1.類型表示法
可以通過在任何一個有效的數據類型的左邊加入 * 來得到與之對應的指針類型。例如,一個元素類型為 int 的切片類型所對應的指針類型是 *[]int ,前面的結構體類型 Sequence 所對應的指針類型是 *Sequence。
注意:如果代表類型的是一個限定標識符(如 sort.StringSlice),那么表示與其對應的指針類型的字面量應該是 *sort.StringSlice ,而不是 sort.*StringSlice。
在Go語言中,還有一個專門用於存儲內存地址的類型 uintptr。而 uintptr 類型與 int 類型和 uint 類型一樣,也屬於數組類型。它的值是一個能夠保存一個指針類型值(簡稱指針值)的位模式形式。
2.值表示法
如果一個變量 v 的值是可尋址的,表達式 &v 就代表了指向變量 v 的值的指針值。
知識點: 如果某個值確實被存儲在了計算機中,並且有一個內存地址可以代表這個值在內存中存儲的起始位置,那么就可以說這個值以及代表它的變量是可尋址的。
3.屬性和基本操作
指針類型屬於引用類型,它的零值是 nil。
對指針的操作,從標准代碼包 unsafe 講起,如下為省略文檔的 unsafe 包下面的 unsafe.go 的源碼(親們可以自己到Go安裝包 src 目錄查看詳細內容):
package unsafe type ArbitraryType int type Pointer *ArbitraryType func Sizeof(x ArbitraryType) uintptr func Offsetof(x ArbitraryType) uintptr func Alignof(x ArbitraryType) uintptr
在代碼包 unsafe 中,有一個名為 ArbitraryType 的類型。從類型聲明上看,它是 int 類型的一個別名類型。但是,它實際上可以代表任意的Go語言表達式的結果類型。事實上,它也並不算是 unsafe 包的一部分,在這里聲明它僅處於代碼文檔化的目的。另外 unsafe 還聲明了一個名為 Pointer 的類型,它代表了ArbitraryType 類型的指針類型。
如下有4個與 unsafe.Pointer 類型相關的特殊轉換操作:
-
一個指向其他類型的指針值都可以被轉換為一個unsafe.Pointer類型值。例如,如果有一個float32類型的變量f32,那么可以將與它的對應的指針值轉換為一個unsafe.Pointer類型的值:
pointer := unsafe.Pointer(&f32)
-
其中,在特殊標記 := 右邊就是用於進行轉換操作的調用表達式。取值表達式 &f32 的求值結果是一個 *float32 類型的值。
-
一個 unsafe.Pointer 類型值可以被轉換為一個與任何類型對應的指針類型的值。例如:
vptr := (*int)(pointer)
-
上面的代碼用於將 pointer 的值轉換為與指向int類型值的指針值,並賦值給變量 vptr。int 類型和 *float32 類型在內存中的布局是不同的,如果我們在它們之上直接進行類型轉換(對應表達式* (int)(&f32))* 是不行,這會產生一個編譯錯誤。有了上面的 unsafe.Pointer 作為中轉類型的時候,看起來操作沒有問題,但在使用取值表達式 *vptr 的時候會出現問題,int 類型的值和 float32 類型的值解析得到的結果是完全不同的,這樣會產生一個不正確的結果。比如,如果這里對變量 vptr 的賦值語句改為:
vptr := (*string)(pointer)
-
取值表達式 *vptr的求值就會引發一個運行時恐慌。
-
一個 uintptr 類型的值也可以被轉換為一個 unsafe.Pointer 類型的值。例如:
pointer2 := unsafe.Pointer(uptr)
-
一個 unsafe.Pointer 類型值可以被轉換為一個 uintptr 類型的值。例如:
uptr := uintptr(pointer)
注意:正是因為這些特殊的轉換操作,unsafe.Pointer 類型可以使程序繞過Go語言的類型系統並在任意的內存地址上進行讀寫操作成為可能。但這些操作非常危險,小心使用。
現在用之前的結構體類型 Person 舉例,如下:
type Person struct { Name string `json:"name"` Age uint8 `json:"age"` Address string `json:"addr"` }
初始化 Person 的值,並把它的指針值賦給變量 p :
p := &Person(“Huazie”, 23, “Nanjing”)
下面利用上述特殊轉換操作中的第一條和第三條獲取這個結構體值在內存中的存儲地址:
var puptr = uintptr(unsafe.Pointer(p))
變量 puptr 的值就是存儲上面那個 Person 類型值的內存地址。由於類型 uintptr 的值實際上是一個無符號整數,所以我們可以在該類型的值上進行任何算術運算。例如:
var np uintptr = puptr + unsafe.Offsetof(p.Name) //變量np表示結構體中的Name字段值的內存地址。
如上 unsafe.Offsetof 函數會返回作為參數的某字段(由相應的選擇表達式表示)在其所屬的結構體類型之中的存儲偏移量。也就是,在內存中從存儲這個結構體值的起始位置到存儲其中某字段的值的起始位置之間的距離。這個存儲偏移量(或者說距離)的單位是字節,它的值的類型是 uintptr。對於同一個結構體類型和它的同一個字段來說,這個存儲偏移量總是相同的。
在獲得存儲 Name 字段值的內存地址之后,將它還原成指向這個 Name 字段值的指針類型值,如下:
var name *string = (*string)(unsafe.Pointer(np))
獲取這個 Name 字段的值:
*name
只要獲得了存儲某個值的內存地址,就可以通過一定的算術運算得到存儲在其他內存地址上的值甚至程序。如下一個恆等式顯示上面的一些操作:
uintptr(unsafe.Pointer(&s)) + unsafe.Offsetof(s.f) == uintptr(unsafe.Pointer(&s.f))
3.9數據初始化
這里的數據的初始化是指對某個數據類型的值或變量的初始化。在Go語言中,幾乎所有的數據類型的值都可以使用字面量來進行表示和初始化。在大多數情況下,使用字面量就可以滿足初始化值或變量的要求。
Go語言還提供了兩個專門用於數據初始化的內建函數 new 和 make。
1.new
內建函數 new 用於為值分配內存。它並不會去初始化分配到的內存,而只會清零它。例如:
new(T)
如上調用該表達式求值時,所做的是為 T 類型的新值分配並清零一塊內存空間,然后將這塊內存空間的地址作為結果返回。而這個結果就是指向這個新的 T 類型值的指針值。它的類型為 *T。new 函數返回的 *T 類型值總是指向一個 T 類型的零值。例如:
new(string)//求值結果指向的是一個string類型的零值"" new([3]int)//求值結果指向的是一個[3]int類型的零值[3]int{0,0,0}
以標准庫代碼包 bytes 中的結構體類型 Buffer 為例, bytes.Buffer 是一個尺寸可變的字節緩沖區。它的零值就是一個立即可用的空緩沖區。例如:
new(bytes.Buffer)//求值結果就是一個指向一個空緩沖區的指針值,可以立即在這個緩沖區上進行讀寫操作
標准庫代碼包 sync 中的結構體類型 Mutex 也是一個可以 new 后即用的數據類型。它的零值就是一個處於未鎖定狀態的互斥量。
2.make
內建函數 make 只能被用於創建切片函數、字典類型和通道類型的值,並返回一個已被初始化的(即非零值的)的對應類型的值。以上3個復合類型的特殊結構都是引用類型,在它們的每一個值的內部都會保持着一個對某個底層數據結構值的引用。如果不對它們的值進行初始化,那么其中的這種引用關系是不會建立起來的,同時相關的內部值也會不正確。因此在創建這3個引用類型的值的時候,必須將內存空間分配和數據初始化這兩個步驟綁定在一起。
內建函數 make 除了會接受一個表示目標類型的類型字面量之外,還會接受一個或兩個額外的參數。
對於切片類型來說,可以把新值的長度和容量也傳遞給 make 函數,例如:
make([]int, 10, 100)
如上創建了一個新的 []int 類型的值,這個值的長度為 10,容量為 100。如果省略最后一個參數,即不指定新值的容量。這樣的話,該值的容量會與其長度一致。例如:
s := make{[]int, 10}
變量 s 的類型是 []int 的,而長度和容量都是 10。
在使用 make 函數初始化一個切片值的過程中,該值會引用一個長度與其容量相同且元素類型與其元素類型一致的數組值。這個數組值就是該切片值的底層數組。該數組值中每個元素都是當前元素類型的零值。但是,切片值只會展現出數量與其長度相同的元素。因此 make([]int, 10, 100) 所創建並初始化的值就是 []int{0,0,0,0,0,0,0,0}。
在使用 make 函數創建字典類型的值的時候,也可以指定其底層數據結果的長度。但是,該字典值只會展示出我們明確“放入”的鍵值對。例如:
make(map[string]int, 100)
它所創建和初始化的值會使 map[string]int{}。雖然可以忽略那個用於表示底層數據結構長度的參數,但是這邊還是建議:應該在性能敏感的應用場景下,根據這個字典值可能包含的鍵值對的數量以及“放入”它們的時間,仔細地設置該長度參數。
對於通道類型的值的數據初始化,這里可以使用 make 函數創建一個通道類型的值:
make(chan int, 10)
其中的第一個參數表示的是通道的類型,而第二個參數則表示該通道的長度。與字典類型相同,第二個參數也可以被忽略掉。對於忽略它的含義,在之后的博文中詳細講解通道類型的時候大家可以了解。
內建函數 make 只能被應用在引用類型的值的創建上。並且,它的結果是第一個參數所代表的類型的值,而不是指向這個值的指針值。如果想要獲取該指針值的話,只需要如下:
m := make(map[string]int , 100) mp := &m
對於數據初始化需要考慮的一些規則:
-
字面量可以被用於初始化幾乎所有的Go語言數據類型的值,除了接口類型和通道類型。接口類型沒有值,而通道類型的值只能使用 make 函數來創建。如果需要指向值的指針值,可以在表示該值的字面量之上進行取址操作。
-
內建函數 new 主要用於創建值類型的值。調用 new 函數的額表達式的結果值將會是指向被創建值的指針值,並且被創建值會是其所屬數據類型的零值。因此,new 函數不適合被用來創建引用類型的值。其直接的原因是引用類型的值的零值都是 nil,是不可用的。
-
內建函數 make 僅被用於某些引用類型(切片類型、字典類型和通道類型)的指的創建。它在創建值之后還會對其進行必要的初始化。與 new 函數不同,調用 make 函數的表達式的結果值將會是被創建的值本身,而不是指向它的指針值。