用Go寫Android應用(3) - Go語言速成
Go快餐
下面我們將Go與C/C++/Java的一些比較不同的地方提煉一下,讓大家可以快速上手。然后在實踐中繼續學習。
Go是支持GC的
好的方面是,不用自己管理內存了。
不好的方面是,GC影響性能的話,要想辦法優化啊。
Go的變量定義類型在后面
例:
變量:
var i int = 10
常量
const ClassFile string = FilePath + "Test.class"
struct也是在后面
定義自定義類型的struct,也不像C語言一樣在前面,跟系統類型一樣,放到后面。前面有個type關鍵字。
例:
type ELFFile struct {
eiClass int
littleEndian bool
osABI string
}
類型推斷和函數返回多個值
在函數里面使用時,可以使用定義和賦值合一的辦法,就是使用:=運算符。這時候不需要指定類型,因為可以通過后面的語句來推斷出類型。
例:
buf, err := ioutil.ReadFile(elfFileName)
從上邊的例子我們還可以看到,Go語言支持函數返回多個參數。如果有的參數并不重要,可以使用特殊變量"_",不理它就是了。
未使用的變量和包將導致編譯不過
在Go中,如果引用了包不用,或者是定義了變量不使用,不是產生警告,而是直接導致編譯失敗!
大寫開頭是public,小寫開頭是private
Go語言沒有額外定義public和private限定符。如果一個變量或函數以大寫字母開頭,比如"Println",那么它就是public的,如果小寫開頭就是private的。
數組賦值會做拷貝
小心,將一個數組的值賦給另一個數組,會引發對數組的復制喲。
流程控制中可以不用小括號
if語句,for循環等控制語句中的小括號是可以省略不寫的。
例:if后面的判斷不用小括號
if err != nil {
fmt.Println("Error reading ELF:", err)
}
switch默認帶break
Go語言的switch不需要寫break,break是默認的行為。相反,如果不需要break,需要加一個fallthrough語句取消掉默認的break.
Go語言有指針
默認是傳值復制,如果需要傳引用的,請用指針吧。
Go語言有goto
保持一個方向,盡量避免跳來跳去吧。
同時,Go語言也是支持break和continue的,而且二者都是可以帶標號跳轉的。goto可以留到最后再用。
Go語言只有for循環這一種
Go語言沒有提供while和do while循環,更沒有do until之類的。一切都是for循環。
死循環就是:
for ;; {
...
}
main函數和init函數
Go應用的入口點是main包的main函數。
每個package可以寫一個init函數,會被自動調用。
Go語言沒有this指針
需要明確指定對象,沒有隱藏的this指針潛規則可以用。
例,必須直接指定對象:
func (elfFile *ELFFile) ParseEIClass_v2(value byte) {
if value == 1 {
elfFile.eiClass = 32
fmt.Println("It is 32-bit")
} else if value == 2 {
elfFile.eiClass = 64
fmt.Println("It is 64-bit")
} else {
elfFile.eiClass = 0
fmt.Println("unknown format, neither 32-bit nor 64-bit")
}
}
Go的做法是把潛規則變成明文,在普通函數定義的前面,加上接收對象的聲明。
非侵入式的接口設計
鴨子原則,只要一個東西,走起來像鴨子,叫起來像鴨子,我們就可以認為它是一只鴨子。
Go語言的interface就是這么設計的。
我們來看一個例子,假設有三種虛擬機,都支持athrow方法:
package main
type Hotspot struct {
}
type Dalvik struct {
}
type AndroidRuntime struct {
}
func (vm Hotspot) athrow() {
}
func (dalvik Dalvik) athrow() {
dalvik.throw()
}
func (dalvik Dalvik) throw() {
}
func (art AndroidRuntime) athrow() {
art.pDeliverException()
}
func (art AndroidRuntime) pDeliverException() {
}
但是上面三種虛擬機的實現是不同的,Hotspot是直接支持這條指令,Dalvik是調用自己的throw指令,而ART是調用pDeliverException過程。
但不管怎樣,它們都聲稱支持athorw這個方法調用。
于是我們可以聲明一個interface叫SupportException,定義這一個方法。
從此以后,各種JVM的實現,都可以賦給一個SupportException類型的變量。
我們看使用的例子:
type SupportException interface {
athrow()
}
func throwException() {
hotspot := Hotspot{}
dalvik := Dalvik{}
art := AndroidRuntime{}
var jvm1 SupportException
var jvm2 SupportException
var jvm3 SupportException
jvm1 = hotspot
jvm1.athrow()
jvm2 = dalvik
jvm2.athrow()
jvm3 = art
jvm3.athrow()
}
也是就說,定義類的時候,根本不用管接口的定義,只要實現就好了。最后再從各個類的實現中總結出接口來就好。
defer延遲執行
defer提供了函數級延時執行的機制度。就相當于函數級的finally,一定會被執行到。
比如打開文件成功后,就可以先defer一個關閉文件或者channel的操作。
func dex2oat(ch chan bool, dexFile string) {
defer close(ch)
ch <- dex2oatImpl(dexFile)
fmt.Println("Dex2OAT finished!")
}
函數小的時候可能還得記得,大了之后defer的作用就顯現出來了,可以避免忘事兒。
引用類型
切片(slice)
Go語言的數組是只讀的。
數組可以用兩種方式來定義長度:
- 一是直接指定長度
- 二是讓Go來計算長度
直接給定長度,我們可以這么寫:
magic := [4]byte{0x7f, 'E', 'L', 'F'}
如果我們懶得算有幾個,或者太長不好算,就可以給3個點,請編譯器幫我們算:
magic := [...]byte{0x7f, 'E', 'L', 'F'}
為什么需要寫3個點,而不能直接給個空的方括號呢?
因為如果方括號為空,就不是數組了,而變成另外一種類型,叫做切片。
例:定義切片:
magic2 := buf[0 : 3]
buf是個數組,magic2是從第0個元素到第3個元素的切片。
如果是從頭開始,冒號前面可以省略,如果是切片到最后一個元素為止,剛冒號后面可以省略。如果做一個完整的切片,頭尾都可以省略。
下面的函數展示了如何從切片中消費一個字節,然后返回一個新的切片:
func ReadU1_v2(data []byte) (byte, []byte) {
if data == nil {
return 0, nil
} else if len(data) > 1 {
return data[0], data[1:]
} else {
return data[0], nil
}
}
切片的屬性
切片其實是一個有三個數據組成的數據結構:
- 指向數組的指針
- 切片的長度,如上例,可以通過len()函數獲取
- 切片的最大容量,可通過cap()函數來獲取
切片的函數
- append:向切片追加一個或多個元素。相當于實現了動態數組的功能。如果切片所指向的原數組的容量不足,超出了切片的cap,則會為其分配一個新的數組。原數組不變。
- copy,切片之間做數據復制。
map
Go語言內建對map的支持。
- map也是一種引用類型,如果兩個map指向同一底層數據結構,則一個改變,另一個也改變。
- map通過鍵-值對進行賦值
- 鍵可以是任意的實現了==與!=操作的類型
- map是無序的,不能遍歷
引用類型的內存分配
map,slice還有最后要講的channel可以通過make函數進行內存分配。
用戶自定義類型
前面講過了沒有this指針的事情,這里再總結一下。
定義用戶自定義類型
通過struct關鍵字來定義:
type ELFFile struct {
elfFileName string
eiClass int
littleEndian bool
osABI string
}
使用自定義類型
直接當成普通類型使用就好了。最簡單的方法就是直接用:=賦給一個變量使用,也省得指定類型了。
可以通過鍵:值的方式來賦初值。
例:
elfFile := ELFFile{elfFileName: OatFile}
為自定義類型定義方法
前面講過了,給普通函數前面加一個對象接收者的聲明就可以了。
例:
func (elfFile *ELFFile) ParseEiData_v2(value byte) {
switch value {
case 1:
elfFile.littleEndian = true
fmt.Println("It is Little Endian")
IsLittleEndian = true
case 2:
elfFile.littleEndian = false
fmt.Println("It is Big Endian")
IsLittleEndian = false
default:
fmt.Println("Unknow Endian, the value is:", value)
}
}
Go的多作務機制
Go語言從語言層面天生就支持并發。通過go語句,每個函數都可以運行在一個Goroutine中,類似于一個線程。
多個Goroutine之間通過Channel來發送消息來實現通信。
我們舉個簡單的例子,實現一個Future模式吧。讓三個dex2oat作務并發:
func dex2oat(ch chan bool, dexFile string) {
ch <- dex2oatImpl(dexFile)
fmt.Println("Dex2OAT finished!")
}
func dex2oatImpl(dexFile string) bool {
return true
}
上面的dex2oat函數傳入一個bool型的Channel,我們通過這個Channel向調用者返回結果。
調用者的代碼如下:
channels := make([]chan bool, 3)
for i := 0; i < len(channels); i++ {
channels[i] = make(chan bool)
}
go dex2oat(channels[0], "Test1.dex")
go dex2oat(channels[1], "Test2.dex")
go dex2oat(channels[2], "Test3.dex")
for _, ch := range channels {
value := <-ch
fmt.Println("The result is ", value)
}
首先是new一個Channel數組,然后make Channel對象。
接著通過go關鍵字去開三個goroutine去分別執行dex2oat。
于是主任務就阻塞等待3個子任務分別返回,最后相當于把結果join在一起,再繼續往下執行。
小結
總結一下,Go的核心內容就上面這么多。
當然,其中的細節我們都沒有展開。希望給大家留個印象就是Go語言還是很容易上手的。
這張圖如果看不清的話,我們將其拆成兩張圖,再注掉分支流程那部分的局部圖:
分支流程部分的放大圖: