Go语言常被拿来和C与Python进行比较,Go语言在语法上确实和C与Python有很多的相似性,不过这两种语言我都不太擅长,C语言仅局限在大学的课本上,Python倒是之前用来写过一些安装脚本。总之,我是不够资格去评论Go与这两种语言的差别了,在学习的过程中,倒是会与Java做些比较,其实多少也有些相似性。
package
package <pkgName> 定义当前文件属于哪个包。
包名如果是main,则表明它是一个可独立运行的包,它在编译后会产生可执行文件。除了main包之外,其它的包最后都会生成*.a文件(也就是包文件),并放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac为例就是$GOPATH/pkg/darwin_amd64)。
每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,main函数既没有参数,也没有返回值。
包名和包所在的文件夹名可以是不同的,<pkgName> 即为通过 package <pkgName> 声明的包名,而非文件夹名。
import
import用来导入包文件。除了标准库之外,import还支持如下两种方式来加载自己写的模块:
1、相对路径
import “./model” //与当前文件同一目录的model目录,但是不建议使用
2、绝对路径
import “shorturl/model” //加载gopath/src/shorturl/model模块
import的几种特殊使用方式:
1、点操作
import(
. "fmt"
)
点操作的含义就是这个包导入之后,在调用这个包的函数时,可以省略前缀的包名,也就是将 fmt.Println(“hello world”) 简写成 Println(“hello world”)。
2、别名操作
import(
f "fmt"
)
调用包函数时前缀变成了别名,即 f.Println(“hello world”)。
3、_操作
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作就是引入该包,而不直接使用包里面的函数,只是调用了该包里面的init函数。
变量
定义变量的标准方式:
var varName type
定义多个变量:
var vname1, vname2, vname3 type
定义变量并初始化值:
var varName type = value
定义并初始化多个变量:
var vname1, vname2, vname3 type= v1, v2, v3
可以忽略类型声明,Go会根据其值的类型来初始化。
简短声明:
vname1, vname2, vname3 := v1, v2, v3
:= 取代了var和type,但只能用在函数内部,在函数外部使用会无法编译通过,所以一般用var方式来定义全局变量。
_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。
另外需要注意的是,Go对于已声明但未使用的变量会在编译阶段报错。
常量
定义常量的标准方式:
const constantName type = value
类型声明可以忽略。
定义一组常量就可以作为enum:
const(
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
)
这里的iota,在声明enum的时候采用,它默认开始值是0,每调用一次加1,每遇到一个const关键字,iota就会重置。
若一行定义多个常量的值为iota,则这些常量的值相同。
const(
x, y, z = iota, iota, iota // x=0, y=0, z=0
)
数据类型
Go语言中约定变量名或函数名的定义方式决定可访问性:
大写字母开头的变量是公用变量,其它包可以读取;小写字母开头是私有变量。
大写字母开头的函数也是一样,相当于Java中的public方法;小写字母开头的就是private方法。
Boolean
布尔值的类型为bool,值是true或false,默认为false。
数值类型
整数类型有无符号int和带符号uint两种。这两种类型的长度相同,但具体长度取决于不同编译器的实现,通常为32位。
Go里面也有直接定义好位数的类型:rune, int8, int16, int32, int64和byte, uint8, uint16, uint32, uint64。其中rune是int32的别名,byte是uint8的别名。这些类型的变量之间不允许互相赋值或操作,否则会在编译时报错。
浮点数类型有float32和float64两种(没有float类型),默认是float64。
复数类型有complex128(64位实数+64位虚数)和complex64(32位实数+32位虚数)两种。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。
字符串
Go中的字符串都是采用UTF-8字符集编码。字符串是用一对双引号 ” ” 或反引号 ` ` 括起来定义,它的类型是string。
Go的字符串是不可变的,可以使用 + 操作符来连接两个字符串。
多行的字符串可以通过 ` 来声明,` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,没有字符转义。
array
array数组的定义方式:
var arr [n]type
n表示数组的长度,type表示存储元素的类型。对数组的操作都是通过 [] 来进行读取或赋值。
由于长度也是数组类型的一部分,因此[3]int与[4]int是不同的类型,数组不能改变其长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。
数组可以使用 := 来声明:
a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
c := [...]int{4, 5, 6} // 可以省略长度而采用 "..." 的方式,Go会自动根据元素个数来计算长度
Go支持嵌套数组,即多维数组。
slice
slice动态数组并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层的array,slice的声明也可以像array一样,只是不需要指定长度。
slice和array在声明时的区别:声明数组时,方括号内写明了数组的长度或使用…自动计算长度,而声明slice时,方括号内没有任何字符。
slice可以通过 array[i:j] 来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。
slice的默认开始位置是0,ar[:n] 等价于 ar[0:n]。
slice的第二个序列默认是数组的长度,ar[n:] 等价于 ar[n:len(ar)]。
如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于 ar[0:len(ar)]。
slice是引用类型,当引用改变其中元素的值时,其它的所有引用都会改变该值。
slice像一个结构体(struct),这个结构体包含了三个元素:
指针 - 指向数组中slice指定的开始位置。
长度 - slice的长度。
最大长度 - slice开始位置到数组的最后位置的长度。
slice的常用内置函数:
len - 获取slice的长度。
cap - 获取slice的最大容量。
append - 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice。
copy - 从源slice中复制元素到目标dest,并且返回复制的元素的个数。
map
map也就是Python中的字典,它的格式为:
map[keyType]valueType
map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。
map的长度是不固定的,和slice一样,也是一种引用类型。
内置的len函数同样适用于map,返回map拥有的key的数量。
map的值可以很方便的修改,通过 numbers[“one”]=11 可以很容易的把key为one的字典值改为11。
map和其他基本类型不同,它不是thread safe的,在多个goroutine存取时,必须使用mutex lock进行加锁。
map可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式。可以通过delete删除map的元素。
流程控制
if
if条件判断语句中没有圆括号,但需要有花括号。
if语句里面允许声明变量,这个变量的作用域是该条件逻辑块内。
goto
用goto跳转到在当前函数内定义的标签。标签名是大小写敏感的。
for
for条件语句不需要括号。
若需要进行多个赋值操作,由于Go里面没有,操作符,则需同时赋值多个变量,如:*i, j = i+1, j-1
若条件语句中只存在一个条件,则 ; 也可以省略,相当于实现了while的功能,如:
sum := 1
for sum < 1000 {
sum += sum
}
break用于跳出当前循环,continue用于跳过本次循环。
break和continue还可以跟着标号,用来跳到多重循环中的外层循环。
for配合range可以用于读取slice和map的数据,如:
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
由于Go支持 “多值返回”, 而对于 声明而未被调用 的变量, 编译器会报错, 在这种情况下, 可以使用 _ 来丢弃不需要的返回值,如:
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
switch表达式没有类型要求。case语句中可使用多个值。
默认每个case执行完成都会break,匹配成功后不会自动向下执行其他case,但是可以使用fallthrough强制执行后面的case代码。
default用于无匹配条件的case。
函数
函数通过关键字func来声明,格式如下:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
// statement
return value1, value2
}
关键字func用来声明一个函数funcName;函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔;函数可以返回多个值。
返回值可以不声明变量只定义类型;如果只有一个返回值且不声明返回值变量,那么可以省略包括返回值的括号;如果没有返回值,那么就直接省略最后的返回信息;如果有返回值,那么必须在函数的外层添加return语句;如果定义了返回值变量,那么return语句可以不用带上变量名。
在Go中函数也是一种变量,可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型:
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
变参
Go函数支持变参,在函数体中,变参是一个slice。如:
func myfunc(arg ...int) {}
传值与传指针
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
传递引用是通过传指针来实现的。传指针使得多个函数能操作同一个对象。
传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。
Go语言中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。若函数需改变slice的长度,则仍需要取地址传递指针。
结构体struct
struct的声明方式:
type structName struct {}
struct的使用方式:
1、按照顺序提供初始化值。例如:
P := person{"Tom", 25}
2、通过field:value的方式初始化,这样可以任意顺序。例如:
P := person{age:24, name:"Tom"}
3、通过new函数分配一个指针。例如此处P的类型为*person:
P := new(person)
定义struct的时候,若字段只提供类型,而不写字段名,它就是匿名字段,也称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
包含匿名字段的struct,若定义了与匿名字段类型中同名的字段,则可实现覆盖。
method
定义method的格式:
func (r ReceiverType) funcName(parameters) (results)
receiver可以为任何类型。若receiver为指针类型,Go会自动将传入的实例变量转为指针,反之亦然。
若匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method,通过这种方式实现method继承。
包含匿名字段的struct,若定义与匿名字段同名的method,则可实现method重写。
interface
interface是一组method的组合。
interface的声明方式:
type interfaceName interface { }
空interface(interface{})不包含任何的method,可以用于存储任意类型的数值。一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。
在interface中定义interface类型的字段,即可继承该interface中的所有method。
类型判断
comma-ok判断变量的类型:
value, ok = element.(T)
value是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
switch测试变量的类型:
switch value := element.(type) {
case int:
fmt.Printf("element is an int and its value is %d\n", value)
case string:
fmt.Printf(“element is a string and its value is %s\n", value)
default:
fmt.Println("element is of a different type", index)
}
element.(type)语法不能在switch外的任何逻辑里面使用,如果要在switch外面判断一个类型就要使用comma-ok。
内存分配
make用于内建类型(map、slice 和channel)的内存分配。
new用于各种类型的内存分配。new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。
make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。
错误处理
error
Go内置有一个error类型,专门用来处理错误信息。
自定义error类型可以通过实现error接口的Error方法。
defer
defer延迟语句,当函数执行到最后时,defer语句会按照逆序执行,最后该函数返回。
多个defer是采用后进先出模式。
Panic
panic 是一个内建函数,类似于异常处理,可以中断原有的控制流程。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。panic可以由直接调用panic产生,也可以由运行时错误产生。
Recover
recover 是一个内建的函数,可以让出现panic的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine出现panic,调用recover可以捕获到panic的输入值,并且恢复正常的执行。
panic类似java中的throw exception,recover类似catch exception。
main() && init()
init函数能够应用于所有的package,main函数只能应用于package main。这两个函数在定义时不能有任何的参数和返回值。Go程序会自动调用init()和main(),所以你不需要主动调用这两个函数。每个package中的init函数是可选的,但package main就必须包含一个main函数。
虽然一个package里面可以写任意多个init函数,但从可读性和可维护性来说,建议在一个package中只有一个init函数。
程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话)。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。
反射
反射需要先将值通过reflect.TypeOf或者reflect.ValueOf转化成reflect对象。
t := reflect.TypeOf(i) //得到类型的元数据
v := reflect.ValueOf(i) //得到实际的值
如果需要修改反射的字段,转换成reflect对象的时候需要传入指针。
并发处理
goroutine
goroutine本质就是线程,是通过Go的runtime管理的一个线程管理器。通过关键字go就可以启动一个goroutine。
默认情况下,调度器仅使用单线程,如果一个goroutine没有被阻塞,那么其它的goroutine就不会被执行。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 来告诉调度器同时使用多个线程。
lock
对于多个线程的并发处理,需要加锁来保证操作的准确性。在包sync中提供了Mutex用于对代码块进行加锁和解锁。
atomic
为保证变量的原子操作,在包sync/atomic中提供了很多原子操作的方法用于确保线程安全。
channels
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine之间使用channel进行数据的通信,可以通过它发送或者接收值。这些值只能是channel类型。定义一个channel时,也需要定义发送到channel的值的类型。
必须使用make 创建channel:
channelName := make(chan type)
channel通过操作符 <- 来接收和发送数据,例如:
ch <- v // 发送v到channel ch.
v := <-ch // 从ch中接收数据,并赋值给v
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。
Buffered Channels
Go允许指定channel的缓冲大小,就是channel可以存储多少元素。
例如:ch:= make(chan bool, 4)
创建了可以存储4个元素的bool型channel。在这个channel中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel中读取一些元素,腾出空间。
Buffered Channels的定义方式:
channelName := make(chan type, value)
Range && Close
可以通过range,像操作slice一样操作缓存类型的channel。
for i := range c
能够不断的读取channel里面的数据,直到该channel被显式的关闭。生产者通过close函数关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法 v, ok := <-ch
测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。
一定要在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic。
channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的。
Select
如果存在多个channel的时候,通过select可以监听channel上的数据流动。
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
可以在select中设置超时,避免整个程序进入阻塞的情况。
例如:
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
select的语法类似于switch,default就是当监听的channel都没有准备好的时候执行,这样就不会阻塞等待channel。
goroutine的相关函数
runtime包中有几个处理goroutine的常用函数:
Goexit
退出当前执行的goroutine,但是defer函数还会继续调用
Gosched
让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU
返回 CPU 核数量
NumGoroutine
返回正在执行和排队的任务总数
GOMAXPROCS
用来设置可以并行计算的CPU核数的最大值,并返回之前的值。