1 简介
go or golang, 出自google的开源编程语言
主要目标是“兼具 Python 等动态语言的开发速度和 C/C++等编译型语言的性能与安全性”
Go 语言是 Google 公司开发的一种静态型、编译型并自带垃圾回收和并发的编程语言。
不但能访问底层操作系统,还提供了强大的网络编程和并发编程支持。Go 语言的用途众多,可以进行网络编程、系统编程、并发编程、分布式编程
编译型语言
go自带编译器,并发编译,最小编译单元为函数
go为并发而生
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go 语言运行时会参与调度 goroutine,并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用CPU性能。
多个 goroutine 中,Go 语言使用通道(channel)进行通信,程序可以将需要并发的环节设计为生产者模式和消费者的模式,将数据放入通道。通道的另外一端的代码将这些数据进行并发计算并返回结果
2 go标准库
Go语言标准库包名 功 能
bufio 带缓冲的 I/O 操作
bytes 实现字节操作
container 封装堆、列表和环形列表等容器
crypto 加密算法
database 数据库驱动和接口
debug 各种调试文件格式访问及调试功能
encoding 常见算法如 JSON、XML、Base64 等
flag 命令行解析
fmt 格式化操作
go Go 语言的词法、语法树、类型等。可通过这个包进行代码信息提取和修改
html HTML 转义及模板系统
image 常见图形格式的访问及生成
io 实现 I/O 原始访问接口及访问封装
math 数学库
net 网络库,支持 Socket、HTTP、邮件、RPC、SMTP 等
os 操作系统平台不依赖平台操作封装
path 兼容各操作系统的路径操作实用函数
plugin Go 1.7 加入的插件系统。支持将代码编译为插件,按需加载
reflect 语言反射支持。可以动态获得代码中的类型信息,获取和修改变量的值
regexp 正则表达式封装
runtime 运行时接口
sort 排序接口
strings 字符串转换、解析及实用函数
time 时间接口
text 文本模板及 Token 词法器
工程结构
GOPATH 及 go build/go install
3 变量声明及初始化
var 变量名 变量类型 = 表达式
在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。
等号右边的部分在编译原理里被称做右值(rvalue)
短变量声明在开发中比较常见
var hp int = 100 //标准声明
//等价于
var hp = 100 //右值推导
//等价于
hp := 100 //精简右值推导
下划线表示匿名变量 _
匿名变量不占用命名空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用
func GetData() (int, int) {
return 100, 200
}
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b) //100 200
4 整型
自动匹配平台的int uint (8, 16, 32, 64)
编译器尽量将内存对齐以获得最好的性能
浮点型 float32, float64 IEEE 754标准
5 编码
utif8
rune unicode
ascii
6 指针
& 取地址
* 取值
7 生命周期
栈stack LIFO
堆 heap
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片
Go 语言将这个过程整合到编译器中,命名为“变量逃逸分析”。这个技术由编译器分析代码的特征和代码生命期,决定应该如何堆还是栈进行内存分配
8 常量
const pi
空 nil
9 容器 container
数组,切片,映射map(多键索引),sync.Map,列表list
两种重要数据结构hash,平衡树,链表,
10 流程控制
if else if else
for for range
for key, value := range m {
fmt.Println(key, value)
}
11 通道 Channel
线程间通信,生产者-消费者模式
c := make(chan int)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}
使用goto,集中处理异常
err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()
12 函数
Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便。
Go 语言的函数属于“一等公民”(first-class),也就是说:
函数本身可以作为值进行传递。
支持匿名函数和闭包(closure)。
函数可以满足接口。
包(package)是 Go 源码的一种组织方式,一个包可以认为是一个文件夹
声明
func 函数名(参数列表)(返回参数列表){
函数体
}
闭包
package main
import (
"fmt"
)
func main() {
f := func(b int) int {
return -b;
}
a := f(5)
fmt.Println(a);
}
defer 延迟执行
使用的数据结构是栈,那么最后压入的函数,最先执行
defer 在声明时不会立即执行,而是在函数 return 后,再按照 FILO (先进后出)的原则依次执行每一个 defer.
defer 还有一个重要的特性,就是即便函数抛出了异常,也会被执行的
应用场景: 简化资源回收
package main
import (
"fmt"
)
func main() {
defer fmt.Println("1");
defer fmt.Println("2");
defer fmt.Println("3");
}
运行时错误
error 是 Go 系统声明的接口类型
type error interface {
Error() string
}
所有符合 Error()string 格式的方法,都能实现错误接口
宕机
Go 语言程序在宕机时,会将堆栈和 goroutine 信息输出到控制台,所以宕机也可以方便地知晓发生错误的位置。如果在编译时加入的调试信息甚至连崩溃现场的变量值、运行状态都可以获取
panic(“宕机”)
宕机恢复
无论是代码运行错误由 Runtime 层抛出的 panic 崩溃,还是主动触发的 panic 崩溃,都可以配合 defer 和 recover 实现错误捕捉和恢复,让代码在发生崩溃后允许继续运行。
// 发生宕机时,获取panic传递的上下文并打印
err := recover()
panic 和 defer 的组合有如下特性:
有 panic 没 recover,程序宕机。
有 panic 也有 recover 捕获,程序不会宕机。执行完对应的 defer 后,从宕机点退出当前函数后继续执行
13 结构体struct
复合类型
Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。
实例化(类型,new,地址),访问成员用.
package main
import (
"fmt"
)
type point struct {
x int
y int
}
func main() {
var p point;
p.x = 1;
p.y = 2;
fmt.Printf("x: %d\n", p.x);
fmt.Printf("y: %d\n", p.y);
var p1 = new(point);
p1.x = 3;
p1.y = 4;
fmt.Printf("x1: %d\n", p1.x);
fmt.Printf("y1: %d\n", p1.y);
var p2 = &point{};
p2.x = 5;
p2.y = 6;
fmt.Printf("x2: %d\n", p1.x);
fmt.Printf("y2: %d\n", p1.y);
}
14 方法与接收器
Go 语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收器(Receiver)。
如果将特定类型理解为结构体或“类”时,接收器的概念就类似于其他语言中的 this 或者 self。
在 Go 语言中,接收器的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。
接收器——方法作用的目标
接收器的格式如下:
func (接收器变量 接收器类型) 方法名(参数列表) (返回参数) {
函数体
}
理解指针类型的接收器
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self。
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的
理解非指针类型的接收器
当方法作用于非指针接收器时,Go 语言会在代码运行时将接收器的值复制一份。在非指针接收器的方法中可以获取接收器的成员值,但修改后无效
指针和非指针接收器的使用
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器。大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针
package main
import (
"fmt"
)
type Bag struct {
items []int
}
func (b *Bag) insert (id int) {
b.items = append(b.items, id)
}
func main() {
var bag = new(Bag);
bag.insert(12);
for key,val := range(bag.items) {
fmt.Println(key);
fmt.Println(val);
}
}
15 http包
Go 语言提供的 http 包里也大量使用了类型方法。Go 语言使用 http 包进行 HTTP 的请求,使用 http 包的 NewRequest() 方法可以创建一个 HTTP 请求,填充请求中的 http 头(req.Header),再调用 http.Client 的 Do 包方法,将传入的 HTTP 请求发送出去。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"strings"
)
func main() {
client := &http.Client{}
// 创建一个http请求
req, err := http.NewRequest("POST", "http://www.baidu.com/", strings.NewReader())
// 发现错误就打印并退出
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
// 为标头添加信息
req.Header.Add("User-Agent", "myClient")
// 开始请求
resp, err := client.Do(req)
// 处理请求的错误
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
data, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(data))
defer resp.Body.Close()
}
16 方法和函数的统一调用
无论是普通函数还是结构体的方法,只要它们的签名一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法。
// 声明一个结构体
type class struct {
}
// 给结构体添加Do方法
func (c *class) Do(v int) {
fmt.Println("call method do:", v)
}
// 普通函数的Do
func funcDo(v int) {
fmt.Println("call function do:", v)
}
func main() {
// 声明一个函数回调
var delegate func(int)
// 创建结构体实例
c := new(class)
// 将回调设为c的Do方法
delegate = c.Do
// 调用
delegate(100) // 100
// 将回调设为普通函数
delegate = funcDo
// 调用
delegate(100) //100
}
17 事件系统
将事件派发者和处理者解耦
事件注册
事件系统需要为外部提供一个注册入口。这个注册入口传入注册的事件名称和对应事件名称的响应函数,事件注册的过程就是将事件名称和响应函数关联并保存起来
事件调用
事件调用方是事发现场,负责将事件和事件发生的参数通过事件系统派发出去,而不关心事件到底由谁处理
package main
import "fmt"
// 声明角色的结构体
type Actor struct {
}
// 角色添加一个事件处理函数
func (a *Actor) OnEvent(param interface{}) {
fmt.Println("actor event:", param)
}
func main() {
a := new(Actor)
// 注册名为OnSkill的回调
RegisterEvent("OnSkill", a.OnEvent)
CallEvent("OnSkill", 100)
}
18 go语言接口interface
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
}
实现接口的条件
1 类型,方法格式一致
2 接口中的方法都要被实现
类型与接口关系
n:n
19 语言包package
Go 语言的包与文件夹一一对应,所有与包相关的操作,必须依赖于工作目录(GOPATH)
GOPATH 是 Go 语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录
$ go env //输出go的环境变量
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\Administrator\AppData\Local\go-build
set GOEXE=.exe
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOOS=windows
set GOPATH=C:\Users\Administrator\go
set GOPROXY=
set GORACE=
set GOROOT=C:\Program Files\Go
set GOTMPDIR=
set GOTOOLDIR=C:\Program Files\Go\pkg\tool\windows_amd64
set GCCGO=gccgo
set CC=gcc
set CXX=g++
set CGO_ENABLED=1
set GOMOD=
set CGO_CFLAGS=-g -O2
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-g -O2
set CGO_FFLAGS=-g -O2
set CGO_LDFLAGS=-g -O2
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -mthreads -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=C:\Users\ADMINI~1\AppData\Local\Temp\go-build360080466=/tmp/go-build -gno-record-gcc-switches
包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go 语言默认为我们提供了很多包,如 fmt、os、io 包等,开发者可以根据自己的需要创建自己的包
import 导入包
import "fmt"
import (
"fmt"
"os"
...
)
Go 语言提供了一个非常方便的特性:init() 函数
init() 函数的特性如下:
每个源码可以使用 1 个 init() 函数。
init() 函数会在程序执行前(main() 函数执行前)被自动调用。
调用顺序为 main() 中引用的包,以深度优先顺序初始化。
20 go并发
go并发是指多线程执行
Go 语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go 语言的并发通过 goroutine 特性完成。goroutine 类似于线程,但是可以根据需要创建多个 goroutine 并发工作。goroutine 是由 Go 语言的运行时调度完成,而线程是由操作系统调度完成。
Go 语言还提供 channel 在多个 goroutine 间进行通信。goroutine 和 channel 是 Go 语言秉承CSP(Communicating Sequential Process)并发模式的重要实现基础
goroutine 的概念类似于线程,但 goroutine 由 Go 程序运行时的调度和管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
Go 程序中使用 go 关键字为一个函数创建一个 goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。
go 函数名( 参数列表 )
//或者匿名函数创建goroutine
go func( 参数列表 ){
函数体
}( 调用参数列表 )
并发与并行
21 Go语言通道(chan)——goroutine之间通信的管道
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
声明通道
var 通道变量 chan 通道类型
ch1 := make(chan int)
//发送数据
ch1 <- 123
ch1 <- 456
//接收数据
data := <-ch1 //阻塞式
data,ok := <-ch1 //非阻塞式
func main() {
ch := make(chan int)
go func() {
fmt.Println("start goroutine")
// 通过通道通知main的goroutine
ch <- 123
fmt.Println("exit goroutine")
}()
//接收数据
var data = <-ch
fmt.Println(data)
}
① 通道的收发操作在不同的两个 goroutine 间进行。由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个 goroutine 中进行。
② 接收将持续阻塞直到发送方发送数据。如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
③ 每次接收一个元素
通道类型 生产者-消费者模式
无缓冲通道
无缓冲通道保证收发过程同步
var done chan bool
func HelloWorld() {
fmt.Println("Hello world goroutine")
time.Sleep(1*time.Second)
done <- true
}
func main() {
done = make(chan bool) // 创建一个channel
go HelloWorld()
data := <-done
fmt.Println(data)
}
有缓存通道
在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。发生阻塞条件:
带缓冲通道被填满时,尝试再次发送数据时发生阻塞。
带缓冲通道为空时,尝试接收数据时发生阻塞。
通道实例 := make(chan 通道类型, 缓冲大小)
ch = make(chan int, 5)
单向通道
var 通道实例 chan<- 元素类型 // 只能发送通道,无意义
var 通道实例 <-chan 元素类型 // 只能接收通道
关闭通道
给被关闭通道发送数据将会触发panic
缓冲通道在关闭后依然可以访问内部的数据
在通道关闭后,即便通道没有数据,在获取时也不会发生阻塞,但此时取出数据会失败
func main() {
ch := make(chan int, 3)
ch <- 0
ch <- 1
close(ch)
for i := 0; i < cap(ch); i++ {
v,s := <-ch
fmt.Println(v, s)
}
}
//输出结果
0 true //取到0值
1 true //取到1值
0 false //未取到值 false
22 多路复用
Go 语言中提供了 select 关键字,可以同时响应多个通道的操作。select 的每个 case 都会对应一个通道的收发过程
23 rpc 远程过程调用
Remote Procedure Call
利用channel通道实现
24 竟态检测
当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。
// 序列号生成器
func GenID() int64 {
// 尝试原子的增加序列号
atomic.AddInt64(&seq, 1) // atomic.AddInt64()
return seq
}
func main() {
//生成10个并发序列号
for i := 0; i < 10; i++ {
go GenID()
}
fmt.Println(GenID())
}
25 互斥锁与读写互斥锁
sync.Mutex
sync.RWMutex
变量名+Guard
package main
import (
"fmt"
"sync"
)
var (
// 逻辑中使用的某个变量
count int
// 与变量对应的使用互斥锁,在读多写少的环境中,可以优先使用读写互斥锁(sync.RWMutex),它比互斥锁更加高效。sync 包中的 RWMutex 提供了读写互斥锁的封装
countGuard sync.Mutex //countGuard sync.RWMutex
)
func GetCount() int {
// 锁定
countGuard.Lock()
// 在函数退出时解除锁定
defer countGuard.Unlock()
return count
}
func SetCount(c int) {
countGuard.Lock()
count = c
countGuard.Unlock()
}
func main() {
// 可以进行并发安全的设置
SetCount(5)
// 可以进行并发安全的获取
fmt.Println(GetCount())
}
26 等待组
sync.WaitGroup
除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务
27 golang 反射 reflect
反射是指在程序运行期对程序本身进行访问和修改的能力
Go 程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树(AST)对源码进行扫描后获得这些信息
在 Go 程序中,使用 reflect.TypeOf() 函数可以获得任意值的类型对象(reflect.Type),程序通过类型对象可以访问任意值的类型信息
package main
import (
"fmt"
"reflect"
)
func main() {
var a int //var a string, var a *int var a struct
typeOfA := reflect.TypeOf(a)
fmt.Println(typeOfA.Name(), typeOfA.Kind())
}
// int int
1 reflect.Elem()——通过反射获取指针指向的元素类型
2 通过反射值对象(reflect.Type)的 NumField() 和 Field() 方法获得结构体成员的详细信息
3 使用 reflect.ValueOf() 函数获得值的反射值对象(reflect.Value)
4 IsNil()和IsValid()——判断反射值的空和有效性
5 通过反射调用函数
package main
import (
"fmt"
"reflect"
)
func main() {
var a *string
typeOfA := reflect.TypeOf(a)
fmt.Println(typeOfA.Name(), typeOfA.Kind())
var age int = 5
ageType := reflect.TypeOf(age)
fmt.Println(ageType)
ageVal := reflect.ValueOf(age)
val := ageVal.Int()
fmt.Println(val)
}
// 输出结果
ptr
int
5
28 go编译和工具链
go build/install/run/get/test/pprof
build 编译 文件/包 生成可执行文件
run 编译并执行,从main执行,不会留下可执行文件
get 获取远程代码,编译并安装 例如:go get+远程包
test 代码测试性能
单元测试源码文件可以由多个测试用例组成,每个测试用例函数需要以Test为前缀,命名文件时需要让文件必须以_test结尾,例如:
func TestXXX( t *testing.T )
测试用例文件不会参与正常源码编译,不会被包含到可执行文件中。
测试用例文件使用 go test 指令来执行,没有也不需要 main() 作为函数入口。所有源码内以Test开头的函数会自动被执行。
pprof 性能测试
参考文档
1 http://c.biancheng.net/golang/intro/
2 https://studygolang.com/pkgdoc
3 https://studygolang.com/
4 https://www.godoc.org/