在本地启动文档
godoc -http :8000
CLI
go build -v # 编译代码
go clean # 清除编译文件
go fmt # 格式化代码
go get # 动态获取远程代码包
go install # 安装某个包
go test # 读取 *_test.go ,生成并运行测试用的可执行文件
关键字速览
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
内置基础类型
booltruefalsefalse
intuintruneint8int16int32int64byteuint8uint16uint32uint64runeint32byteuint8
float32float64floatfloat64
complex128complex64RE + IMiREIMi
stringUTF-8""string
s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)
errorpackageerrors
变量声明
const constantName = value // 定义常量
const Pi float32 = 3.1415926 // //如果需要,也可以明确指定常量的类型:
常量可以指定相当多的小数位数, 若指定给float32自动缩短为32bit,指定给float64自动缩短为64bit。
var variableName type // 定义一个名称为“variableName”,类型为"type"的变量
var vname1, vname2, vname3 type // 定义三个类型都是“type”的变量
var variableName type = value // 初始化“variableName”的变量为“value”值,类型是“type”
var vname1, vname2, vname3 type= v1, v2, v3 // 定义三个类型都是"type"的变量,并分别初始化
varName := value
int0
若变量在声明时没有赋初值,它的初值将为 零值(zero-value),比如:
var a int
fmt.Println(a) // 0
_
已声明但未使用的变量会在编译阶段报错。
类型转换
T(v)vT
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
i := 42
f := float64(i)
u := uint(f)
类型推导
var:=
当右值定义了类型时,新变量的类型与其相同:
var i int
j := i // j 也是一个 int
intfloat64complex128
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
iota枚举
iotaenum
package main
import (
"fmt"
)
const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)
const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0
const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)
const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)
func main() {
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}
array
写在前面:Go 语言中数组、字符串和切片三者是密切相关的数据结构。这三种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。
因为数组的长度是数组类型的一个部分,不同长度或不同类型的数据组成的数组都是不同的类型,因此在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值)
array
var arr [n]type
var a [3]int // 定义长度为3的int型数组, 元素全部为0
var b = [...]int{1, 2, 3} // 定义长度为3的int型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2} // 定义长度为3的int型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为6的int型数组, 元素为 1, 2, 0, 0, 5, 6
fmt.Printf%T%#v
fmt.Printf("b: %T\n", b) // b: [3]int
fmt.Printf("b: %#v\n", b) // b: [3]int{1, 2, 3}
:=
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} // 自动根据元素个数来计算长度
[3]int[4]int
当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。
遍历数组¶
for i := range a {
fmt.Printf("a[%d]: %d\n", i, a[i])
}
for i, v := range b {
fmt.Printf("b[%d]: %d\n", i, v)
}
for i := 0; i < len(c); i++ {
fmt.Printf("c[%d]: %d\n", i, c[i])
}
数组指针¶
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
fmt.Println(a[0], a[1]) // 打印数组的前2个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似
多维数组¶
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}
字符串
一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。
每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。
for range
reflect.StringHeader
type StringHeader struct {
Data uintptr // 指向底层字节数组
Len int // 字符串的字节的长度
}
reflect.StringHeader
字符串虽然不是切片,但是支持切片操作,不同位置的切片底层也访问同一块内存数据(因为字符串是只读的,相同的字符串面值常量通常是对应同一个字符串常量)
printfmt.Printfor range
下面的“Hello, 世界”字符串中包含了中文字符,可以通过打印转型为字节类型来查看字符底层对应的数据:
fmt.Printf("%#v\n", []byte("Hello, 世界"))
输出的结果是:
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, \
0x95, 0x8c}
0xe4, 0xb8, 0x960xe7, 0x95, 0x8c
fmt.Println("\xe4\xb8\x96") // 打印: 世
fmt.Println("\xe7\x95\x8c") // 打印: 界
[]byte
for i, c := range []byte("世界abc") {
fmt.Println(i, c)
}
或者是采用传统的下标方式遍历字符串的字节数组:
const s = "\xe4\x00\x00\xe7\x95\x8cabc"
for i := 0; i < len(s); i++ {
fmt.Printf("%d %x\n", i, s[i])
}
for range[]rune
fmt.Printf("%#v\n", []rune("世界")) // []int32{19990, 30028}
fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
slice
slice
slicereflect.SliceHeader
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
slicearrayslicearray
slice...slice
var fslice []int // 和声明array一样,只是少了长度
slice := []byte {'a', 'b', 'c', 'd'} // 声明一个slice,并初始化数据
slicesliceslicearray[i:j]ijarray[j]j-i
slice有一些简便的操作:
slicear[:n]ar[0:n]slicear[n:]ar[n:len(ar)]slicear[:]ar[0:len(ar)]
slice
slice
slicesliceslice
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]
上面代码的真正存储结构如下图所示
slice
lenslicecapsliceappendslicesliceslicecopycopyslicesrcdst
appendslicesliceslice(cap-len) == 0sliceslice
从 Go1.2 开始 slice 支持第三个参数用以指定其容量。
之前我们一直采用这种方式在 slice 或者 array 基础上来获取一个 slice:
var array [10]int
slice := array[2:4]
这个例子里面 slice 的容量是 8,新版本里面可以指定这个容量:
slice = array[2:4:7]
7-2
array[:i:j]
添加切片元素¶
appendN
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
appendappend
除了在切片的尾部追加,我们还可以在切片的开头添加元素:
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制1次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。
appendappend
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
appenda[i:]a[:i]
copyappend
a = append(a, 0) // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x // 设置新添加的元素
appendcopy
copyappend
a = append(a, x...) // 为x切片扩展足够的空间
copy(a[i+len(x):], a[i:]) // a[i:]向后移动len(x)个位置
copy(a[i:], x) // 复制新添加的切片
appendappend
删除切片元素¶
根据要删除元素的位置有三种情况:从开头位置删除,从中间位置删除,从尾部删除。其中删除切片尾部的元素最快:
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
append
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
copy
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
appendcopy
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况。
map
mapmap
mapmap[keyType]valueType
mapslicekeysliceindexintmap
// 声明一个字典,其 key 是 string 类型,值是 int 类型,这种方式的声明需要在使用之前使用make初始化
var numbers map[string]int
// 另一种map的声明方式
numbers = make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3
fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据
// 打印出来如:第三个数字是: 3
使用map过程中需要注意的几点:
mapmapindexkeymapslicelenmapmapkeymapnumbers["one"]=11one11map
mapkeydeletemap
// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C") // 删除key为C的元素
make、new操作
makemapslicechannelnew
newnew(T)T*TT
new
make(T, args)new(T)slicemapchannelT*Tslicearrayslicenilslicemapchannelmake
make
零值
关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”:
int 0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool false
string ""
if
if
if
goto
goto
for
for expression1; expression2; expression3 {
//...
}
expression1expression3expression2expression1expression3
,i, j = i+1, j-1
expression1expression3while
breakcontinue
forrangeslicemap
for k,v:=range map {
fmt.Println("map's key:",k)
fmt.Println("map's val:",v)
}
_
for _, v := range map{
fmt.Println("map's val:", v)
}
switch
switch sExpr {
case expr1:
some instructions
case expr2:
some other instructions
case expr3:
some other instructions
default:
other code
}
sExprexpr1expr2expr3
switchtrue
switchcasebreakswitchfallthrough
func
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
最好命名返回值,因为不命名返回值,虽代码更简洁,但是生成的文档可读性差。
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
变参¶
函数可以有不定数量的参数。为了做到这点,首先需要定义函数使其接受变参:
func myfunc(arg ...int) {}
arg ...intintargintslice
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。
传值与传指针¶
当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。
channelslicemapslice
函数作为值、类型¶
type
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
可以把这个类型的函数当做值来传递。
defer
当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。
- 延迟调用的参数会立刻生成
Panic和Recover
panicrecover
panic
panicFpanicFFpanicpanicgoroutinepanicpanic
recoverdeferrecoverrecoverdefer
panicgoroutinerecoverrecovernilgoroutinepanicrecoverpanic
函数和函数
initpackagemainpackage main
这两个函数在定义时不能有任何的参数和返回值。
packageinitpackageinit
init()main()
packageinitpackage mainmain
mainmain
initmainmaininitmain
main.maininitmain.main
import
我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下:
import(
"fmt"
)
GOROOT
Go的import还支持如下两种方式来加载自己写的模块:
import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import
import “shorturl/model” //加载gopath/src/shorturl/model模块
这个包导入之后,调用这个包的函数时,可以省略前缀的包名
import(
. "fmt"
)
- 别名操作
把包命名成另一个名字
f.Println("hello world")
import(
f "fmt"
)
- _ 操作
_ 操作引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
Struct
type person struct {
name string
age int
}
匿名字段(嵌入字段)¶
Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。
- 匿名字段能够实现字段的继承。
- 最外层的优先访问。
- 自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作(如append)。
- 所有继承来的方法的接收者参数依然是那个匿名成员本身,而不是当前的变量。
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}
func main() {
// 我们初始化一个学生
mark := Student{Human{"Mark", 25, 120}, "Computer Science"}
// 我们访问相应的字段
fmt.Println("His name is ", mark.name)
fmt.Println("His age is ", mark.age)
fmt.Println("His weight is ", mark.weight)
fmt.Println("His speciality is ", mark.speciality)
// 修改对应的备注信息
mark.speciality = "AI"
fmt.Println("Mark changed his speciality")
fmt.Println("His speciality is ", mark.speciality)
// 修改他的年龄信息
fmt.Println("Mark become old")
mark.age = 46
fmt.Println("His age is", mark.age)
// 修改他的体重信息
fmt.Println("Mark is not an athlet anymore")
mark.weight += 60
fmt.Println("His weight is", mark.weight)
}
Student访问属性age和name的时候,就像访问自己所有用的字段一样。
student还能访问Human这个字段作为字段名:
mark.Human = Human{"Marcus", 55, 220}
mark.Human.age -= 1
所有的内置类型和自定义类型都是可以作为匿名字段,而不仅仅是struct字段
package main
import "fmt"
type Skills []string
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,struct
Skills // 匿名字段,自定义的类型string slice
int // 内置类型作为匿名字段
speciality string
}
func main() {
// 初始化学生Jane
jane := Student{Human:Human{"Jane", 35, 100}, speciality:"Biology"}
// 现在我们来访问相应的字段
fmt.Println("Her name is ", jane.name)
fmt.Println("Her age is ", jane.age)
fmt.Println("Her weight is ", jane.weight)
fmt.Println("Her speciality is ", jane.speciality)
// 我们来修改他的skill技能字段
jane.Skills = []string{"anatomy"}
fmt.Println("Her skills are ", jane.Skills)
fmt.Println("She acquired two new ones ")
jane.Skills = append(jane.Skills, "physics", "golang")
fmt.Println("Her skills now are ", jane.Skills)
// 修改匿名内置类型字段
jane.int = 3
fmt.Println("Her preferred number is", jane.int)
}
method
method的语法如下:
func (r ReceiverType) funcName(parameters) (results)
Receiver以值传递不会改变原对象,以指针传递会改变原对象。
.
method 可以定义在任何内置类型、struct等各种类型上面。
method 也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。
method 可以重写,类似于匿名字段。
方法表达式¶
方法表达式的特性可以将方法还原为普通类型的函数:
// 不依赖具体的文件对象
// func CloseFile(f *File) error
var CloseFile = (*File).Close
// 不依赖具体的文件对象
// func ReadFile(f *File, offset int64, data []byte) int
var ReadFile = (*File).Read
// 文件处理
f, _ := OpenFile("foo.dat")
ReadFile(f, 0, data)
CloseFile(f)
Interface
interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。
interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现。
type Men interface {
SayHi()
Sing(lyrics string)
Guzzle(beerStein string)
}
如果我们定义一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。因为m能够持有这三种类型的对象。
空 interface¶
任意的类型都实现了空interface(即 interface{}),也就是包含0个method的interface。
空interface对于描述起不到任何的作用(因为它不包含任何的method),但是空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。
一个函数把interface{}作为参数,那么它可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。
interface 函数参数¶
interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考:可以通过定义interface参数,让函数接受各种类型的参数。
举个🌰:fmt.Println是我们常用的一个函数,但是你是否注意到它可以接受任意类型的数据。打开fmt的源码文件,可以看到这样一个定义:
type Stringer interface {
String() string
}
// Stringer 接口只有一个 String 方法,因此只要实现 String 方法即可实现 Stringer 接口。
也就是说,任何实现了String方法的类型都能作为参数被fmt.Println调用。
注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法了。
interface变量存储的类型¶
我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:
- Comma-ok断言,可以直接判断是否是该类型的变量。
value, ok = element.(T)
这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
- switch测试
element.(type)comma-ok
嵌入interface¶
功能类似于 Struct 的匿名字段:如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。
举个🌰
// 在源码包 container/heap 的一个定义
type Interface interface {
sort.Interface // 嵌入字段sort.Interface
Push(x interface{}) // a Push method to push elements into the heap
Pop() interface{} // a Pop elements that pops elements from the heap
}
sort.Interface其实就是嵌入字段,把sort.Interface的所有method给隐式的包含进来了。也就是下面三个方法:
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less returns whether the element with index i should sort
// before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
再举一个🌰
io包下面的 io.ReadWriter ,它包含了io包下面的Reader和Writer两个interface:
// io.ReadWriter
type ReadWriter interface {
Reader
Writer
}
反射
反射机制不仅包括要能在运行时对程序自身信息进行检测,还要求程序能进一步根据这些信息改变程序状态或结构。我们一般用到的包是reflect包。参考 laws of reflection
使用reflect一般分成三步:
要去反射某个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如:
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
最后,反射的字段必须是可修改的。反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相应的值,必须这样写
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
goroutine
go hello(a, b, c)
多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。
runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。
默认情况下,在Go 1.5将标识并发系统线程个数的runtime.GOMAXPROCS的初始值由1改为了运行环境的CPU核数。
但在Go 1.5以前调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。
在Go语言中,同一个Goroutine线程内部,顺序一致性内存模型是得到保证的。但是不同的Goroutine之间,并不满足顺序一致性内存模型,需要通过明确定义的同步事件来作为同步的参考。如果两个事件不可排序,那么就说这两个事件是并发的。为了最大化并行,Go语言的编译器和处理器在不影响上述规定的前提下可能会对执行语句重新排序(CPU也会对一些指令进行乱序执行)。
channels
channel 默认是无缓冲的。
channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。
无缓冲的 channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。
无缓冲的 channel 是同步的;两端中任意一端的 channel 将等到另一端准备好为止。
定义一个channel时,也需要定义发送到channel的值的类型。必须使用make 创建channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
<-
ch <- v // 发送v到channel ch.
v := <-ch // 从ch中接收数据,并赋值给v
在无缓存的Channel上的每一次发送操作都有与其对应的接收操作相配对,发送和接收操作通常发生在不同的Goroutine上(在同一个Goroutine上执行2个操作很容易导致死锁)。无缓存的Channel上的发送操作总在对应的接收操作完成前发生.
对于从无缓冲Channel进行的接收,发生在对该Channel进行的发送完成之前。
Buffered Channels¶
可以指定 channel 的缓冲大小
ch := make(chan type, value)
当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。
缓冲 channel 是异步的,除非 channel 已满,否则发送或接收消息将不会等待。
KK+CC
Range和Close
close
v, ok := <-ch
for i := range c
可以通过range,像操作slice或者map一样操作缓存类型的channel:
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
for i := range cclosev, ok := <-ch
记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic
channel不像文件之类的,不需要经常去关闭,只有当确实没有任何发送数据了,或者想显式的结束range循环之类的
select
select
select
select
select
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
}
time.After
default
selectdefault
select {
case msg1 := <- c1:
fmt.Println("Message 1", msg1)
case msg2 := <- c2:
fmt.Println("Message 2", msg2)
case <- time.After(time.Second):
fmt.Println("timeout")
default:
fmt.Println("nothing ready")
}
runtime goroutine
runtime包中有几个处理goroutine的函数:
- Goexit
退出当前执行的goroutine,但是defer函数还会继续调用
- Gosched
让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
- NumCPU
返回 CPU 核数量
- NumGoroutine
返回正在执行和排队的任务总数
- GOMAXPROCS
用来设置可以并行计算的CPU核数的最大值,并返回之前的值。
runtime.GOMAXPROCS
指针随时可能会变
不要假设变量在内存中的位置是固定不变的,指针随时可能会变。
不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址,因为:
Go语言支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。每个goroutine刚启动时只会分配很小的栈(4或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以达到GB级(依赖具体实现,在目前的实现中,32位体系结构为250MB,64位体系结构为1GB)。在Go1.4以前,Go的动态栈采用的是分段式的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。但是链表实现的动态栈对某些导致跨越链表不同节点的热点调用的性能影响较大,因为相邻的链表节点它们在内存位置一般不是相邻的,这会增加CPU高速缓存命中失败的几率。为了解决热点调用的CPU缓存命中率问题,Go1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。不过连续动态栈也带来了新的问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。**虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但指针不再是固定不变的了。
原子操作
所谓的原子操作就是并发编程中“最小的且不可并行化”的操作。
通常,如果多个并发体对同一个共享资源进行的操作是原子的话,那么同一时刻最多只能有一个并发体对该资源进行操作。
一般情况下,原子操作都是通过“互斥”访问来保证的,通常由特殊的CPU指令提供保护。
写在前面:互斥锁的代价比普通整数的原子读写高很多,在性能敏感的地方可以增加一个数字型的标志位,通过原子检测标志位状态降低互斥锁的使用次数来提高性能。
sync.Mutex
import (
"sync"
)
// 定义结构体并且立即用于变量初始化
var total struct {
sync.Mutex
value int
}
func worker(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i <= 100; i++ {
total.Lock() // 进入临界区前加锁
total.value += i // 临界区,修改共享变量 total.value
total.Unlock() // 退出临界区后解锁
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
fmt.Println(total.value)
}
sync/atomic
atomic.ValueLoadStoreinterface{}
atomic.AddUint64total
import (
"sync"
"sync/atomic"
)
var total uint64
func worker(wg *sync.WaitGroup) {
defer wg.Done()
var i uint64
for i = 0; i <= 100; i++ {
atomic.AddUint64(&total, i) // 官方支持的原子操作
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go worker(&wg)
go worker(&wg)
wg.Wait()
}
sync.Once
type Once struct {
m Mutex
done uint32
}
func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 1 {
return
}
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
sync.Once
var (
instance *singleton
once sync.Once
)
func Instance() *singleton {
once.Do(func() {
instance = &singleton{}
})
return instance
}
控制并发数
vfsvfsgatefsgatefs
gatefs
import (
"golang.org/x/tools/godoc/vfs"
"golang.org/x/tools/godoc/vfs/gatefs"
)
func main() {
fs := gatefs.New(vfs.OS("/path"), make(chan bool, 8))
// ...
}
vfs.OS("/path")gatefs.New
gatefsgateenterleaveenter
type gate chan bool
func (g gate) enter() { g <- true }
func (g gate) leave() { <-g }
gatefsenterleave
type gatefs struct {
fs vfs.FileSystem
gate
}
func (fs gatefs) Lstat(p string) (os.FileInfo, error) {
fs.enter()
defer fs.leave()
return fs.fs.Lstat(p)
}
我们不仅可以控制最大的并发数目,而且可以通过带缓存Channel的使用量和最大容量比例来判断程序运行的并发率。当管道为空的时候可以认为是空闲状态,当管道满了时任务是繁忙状态。
通知 goroutine 结束
关闭管道以广播退出指令¶
closecancel
mainsync.WaitGroup
func worker(wg *sync.WaitGroup, cannel chan bool) {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-cannel:
return
}
}
}
func main() {
cancel := make(chan bool)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(&wg, cancel)
}
time.Sleep(time.Second)
close(cancel)
wg.Wait()
}
Context 包¶
context
context
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
default:
fmt.Println("hello")
case <-ctx.Done():
return ctx.Err()
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
time.Sleep(time.Second)
cancel()
wg.Wait()
}
main
错误处理
map
if v, ok := m["key"]; ok {
return v
}
errnosyscall.Errnoerrnosyscallsyscall.Errno
syscallerrsyscall.Errno
err := syscall.Chmod(":invalid path:", 0666)
if err != nil {
log.Fatal(err.(syscall.Errno))
}
recoverpanic
错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG或发生了其它不可控的问题。
recover
以JSON解析器为例,说明recover的使用场景。考虑到JSON解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。
func ParseJSON(input string) (s *Syntax, err error) {
defer func() {
if p := recover(); p != nil {
err = fmt.Errorf("JSON: internal error: %v", p)
}
}()
// ...parser...
}
jsonrecoverpanic
panic
Go访问C内存创建大于2GB的内存
因为Go语言实现的限制,我们无法在Go语言中创建大于2GB内存的切片(具体请参考makeslice实现代码)。不过借助cgo技术,我们可以在C语言环境创建大于2GB的内存,然后转为Go语言的切片使用:
package main
/*
#include <stdlib.h>
void* makeslice(size_t memsize) {
return malloc(memsize);
}
*/
import "C"
import "unsafe"
func makeByteSlize(n int) []byte {
p := C.makeslice(C.size_t(n))
return ((*[1 << 31]byte)(p))[0:n:n]
}
func freeByteSlice(p []byte) {
C.free(unsafe.Pointer(&p[0]))
}
func main() {
s := makeByteSlize(1<<32+1)
s[len(s)-1] = 255
print(s[len(s)-1])
freeByteSlice(s)
}
makeByteSlizefreeByteSlice
因为C语言内存空间是稳定的,基于C语言内存构造的切片也是绝对稳定的,不会因为Go语言栈的变化而被移动。
图片
参考 文档
Image
package image
type Image interface {
ColorModel() color.Model
Bounds() Rectangle // 此处的 Rectangle 为 image.Rectangle
At(x, y int) color.Color
}
BoundsRectangleimage.Rectangleimage
color.Colorcolor.Modelimage.RGBAimage.RGBAModel