用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语言还是很容易上手的。
这张图如果看不清的话,我们将其拆成两张图,再注掉分支流程那部分的局部图:
分支流程部分的放大图: