我不是 golang 的专家,只是作为一个平时也喜欢用 golang 的开发者,来抛砖引玉的分享一下我对 golang 的理解。
也欢迎各位在后续的使用中,分享自己的心得。
本系列课程面向 golang 的初学者,分为四个部分:
Go语言的前身是Limbo,Limbo是用于开发运行在小型计算机上的分布式应用的编程语言,他支持模块化编程,编译期和运行时的强类型检查,进程内基于具有类型的通信通,原子性垃圾收集和简单的抽象数据类型。他被设计为:即便是在没有硬件内存保护的小型设备上,也能安全运行。
贝尔实验室从20世纪80年代开始了一个名为Plan 9的操作系统研究项目,目的是为了解决Unix中的一些问题,发展出一个Unix的后续替代系统。在之后的十几年中,该研究项目又演变出了另外一个叫Inferno的项目分支,以及一个名为Limbo的编程语言。
贝尔实验室后来经历了多次动荡,Plan 9项目原班人马加入了Google,在Google,他们创造了Go语言。
早在2007年9月,Go语言还是这帮大牛20%的自由时间的实验项目。2008年5月,Google发现了Go语言的巨大潜力,开始全力支持这个项目,这批人全身心投入Go语言的设计和开发工作中。2009年11月,Go语言的第一个版本正式对外发布。2012年3月28日,Go语言第一个正式版本正式发布。Go语言迎来了第一个引人注目的里程碑。
!
Go 的哲学是“少即是多”,用像 Python 一样的语法写出 c++ 的性能。
比起其他很多静态语言花哨的特性,Go 最大的特点就是高度工程化,一切的设计都是以现实工程为导向的,尽可能的解决实际的工程问题。并且 Go 高度重视性能,追求高并发低延迟,是一款非常强力的语言。
这也是为什么 Go 在知乎 PL 大佬眼中一文不值,但却在工业界迅速蔓延的原因。
再加上 Java
具体的安装很简单,到官网下载相应的安装包即可,不再累述。
安装前需要配置一下环境变量
export GOROOT=/usr/local/opt/go/libexec/
export GOPATH=/Users/laisky/Projects/go
export PATH=$PATH:$GOPATH/bin:$GOROOT/bin
GOROOT:go 安装的位置(v 1.10 后该变量有默认值了,可以不用设置)
GOPATH:go 项目放置的地方,同时记得把 $GOPATH/bin
添加进 PATH
具体的包结构在稍后的会更详细的介绍。
目前在学习语法时,可以简单的在 $GOPATH/src
内建立任意一个 xxx.go
文件,在其中写上
package main
func main() {
// xxx
}
就可以测试运行代码了,运行时只需要执行
go run xxx.go
下面就来过一遍 go 的语法,各个编程语言间大同小异,这里简单过一遍,有一个大致的印象,建议各位回去后自己写几段小代码,熟悉熟悉 go 的用法,我们下周开始会讲一些框架、调试相关的,如果有一点实际经验的话,效果会更好。
分为以下几个步骤来介绍:
变量类型有:
变量的声明与使用
// 定义变量,类型放在最后
// 变量声明时会被默认赋予零值
// 分别为:false, 0, ""
var c, python, java bool
// 也可以包含初始化的值
var a, b int = 3, 4
// 可以使用简洁赋值 :=
// 可以不用写 var,自动进行类型推导
k := 3
i := k
f := float64(i)
u := uint(f)
// 可以放在一起写
var (
ToBe bool = false // 布尔数
MaxInt uint64 = 1<<64 - 1 // 整型
z complex128 = cmplx.Sqrt(-5 + 12i) // 复数
)
常量:
// 使用 const 定义常量
// 常量必需指明类型,不能使用 :=
const a int = 2
const (
x int = 2
y = x
)
block 可以简单的理解为所有被大括号包裹起来的区域:{...}
。
v := "outer"
fmt.Println(v) // outer
{
// 注意这里是声明(declaration)而不是赋值(assignment)
v := "inner"
fmt.Println(v) // inner
{
fmt.Println(v) // inner
}
}
{
fmt.Println(v) // outer
}
fmt.Println(v) // outer
文件作用域仅仅对 import
语句有效,也就是说 import 的包仅在当前文件内可见。
package scope 就是我们最经常遇到的 scope,也就是说,同一个 package 内的所有变量都存在于同一个 scope 内,同一 package 内的不同文件可以直接互相使用各自的变量。
builtin 就是各种内建的 keyword,包括类型、取值和内建函数,可以在任意地方使用。
close
:关闭 channel;delete
:删除 map 中的项;len
:返回长度;cap
:获取 slice 的最大容量,对于 array 和 字符串等效于 len;new
:分配内存并初始化类型,返回一个 pointer;make
:分配内存并初始化一个 slice、map、channel,可以设置长度;copy
:复制 slice;append
:给 slice 增加元素;panic
:抛出 panic;recover
:捕获 panic;print
:输出字符串;println
:输出行;complex
:复数;real
:实部;imag
:虚部。// 声明结构体
type Vertex struct {
X int
Y int
}
// 创建结构体
var (
v1 = Vertex{1, 2}
v2 = Vertex{} // X, Y = 0
v3 = Vertex{X: 1} // 显式赋值
p = &Vertex{1, 2} // 取指针
)
// struct 还可以隐式声明
// 有一点点像 OOP 的 mixin
type Human struct {
Age int
sex string
}
type Student struct {
Human // 默认会包含 Human 的全部字段
Class string
}
数组的长度不可改变,在声明时就要确定数组成员的类型和数组长度:
// 声明数组
var arr [n]type
// demo
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
// 创建数组的时候可以省略个数,自动根据后面的元素来计数
c := [...]int{1, 2, 3}
// 数组可以嵌套
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}} // 简写
这是一种可变长的队列
// 创建切片
// 切片长度不确定,可以像数组一样赋值
p := []int{2, 3, 5, 7, 11, 13}
// 构建切片
// 其中 len 和 cap 可以省略,默认为 0
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
// 取单一值
fmt.Println(p[1])
// 取切片
// 和 py 一样是前闭后开
fmt.Println("p[1:4] ==", p[1:4]) // 取 1,2,3
// p[1:4] == [3 5 7]
fmt.Println("p[:3] ==", p[:3]) // 取 0,1,2
// p[:3] == [2 3 5]
fmt.Println("p[4:] ==", p[4:]) // 取 4,...
// p[4:] == [11 13]
// 声明 map
// string -> Vertex 的映射
// 使用 make 创建 map 对象
// 等效于 map[string]string{}
var m map[string]string = make(map[string]string)
// 赋值
m["a"] = "2"
// 批量赋值
var m = map[string]string{
"a": "b",
"c": "d",
}
// 在 map m 中插入或修改一个元素
m[key] = elem
// 获得元素
elem = m[key]
// 删除元素
delete(m, key)
// 通过双赋值检测某个键存在
// ok = false | true
elem, ok = m[key]
定义函数:
// 需要定义参数的类型和返回的类型
func add(x int, y int) int {
return x + y
}
// 参数的类型可以简写
func add(x, y int) int {
return x + y
}
// 可以返回任意多个值
func swap(x, y int) (int, int) {
return y, x
}
// 返回值命名
// 命名的返回值可以直接在函数里像变量一样的使用
func sum(x, y int) (total int) {
total = x + y
// 直接返回(不推荐)
// 最好显示的写出返回的变量
return
}
Go 内的函数不支持默认参数,开发组认为你应该严格的区别不同功能的函数,而不是把它们揉在一起。
不过很多时候默认参数还是很有用的,你可以考虑用 struct 来作为参数传递。使用 slice 来实现可变长参数
控制程序调用顺序,可以令语句在父函数返回后再执行。 当函数内有多个 defer 时,defer 会维护一个栈,当父函数返回后,按照 LIFO 原则出栈调用。
func main() {
// defer 会在 main 函数执行完成后再执行
defer fmt.Println("world")
fmt.Println("hello")
}
输出: hello world
再看一个例子
func deferDemo() {
defer println()
for i := 0; i < 5; i++ {
defer func() {
print(i, " ")
}()
}
}
这个函数将会输出:5 5 5 5 5
方法,指的是绑定在任意 type 上的函数,是 go 提供的一种面向对象的方法
// 0. 首先声明一个类型
// 0. 然后给这个类型定义方法
type Demo struct {
X, Y int
}
// 给指定的 type 绑定方法
func (d *Demo) Sum() int {
return d.X + d.Y
}
func main() {
d := Demo{4, 4}
fmt.Println(d.Sum()) // 8
}
method 可以分为两种:
区别就在于,绑定的时候传入的是 value 还是 pointer:
type MyStruct struct {
X int
}
// 绑定一个 pointer method
func (s *MyStruct) Abs() float64 {}
// 绑定一个 value method
func (s MyStruct) Abs() float64 {}
需要注意的是:变量的指针可以调用 pointer methods 和 value methods,而变量的值只能调用 value methods。
这是因为 pointer methods 可以改变指针所指向的变量,使用值来调用 pointer methods 会非常危险。所以 go 干脆禁止了这种行为。
这也是对 slice 执行 append 时,需要返回一个新 slice 的原因。
go 只有 for 循环
// 定义 初始值,目标和步长的完整 for 循环
for i := 0; i < 10; i++ {
sum += i
}
// 也可以简写
// 默认初始值为 0,步长为 1
for ; sum < 1000; {}
// 其实连分号都可以省略,用 while 一样的语法
for sum < 1000 {}
// 死循环
for {}
for 中还可以使用 continue
和 break
go 中还提供了 range 关键词可以用于遍历列表和映射
a := []string{"a", "b", "c", "d"}
for idx, val := range a { // idx 是索引,val 是值
fmt.Println(idx, val)
}
d := map[string]int{}
for key, val := range d {
// ...
}
// switch
// switch 默认不会穿透,除非显式指定 fallthrough
// 不过 fallthrough 只会穿透一次,连续穿透需要定义多个 fallthrough
switch a {
case 1:
// ...
fallthrough // 会穿透,执行下面的匹配
case 2:
// ...
default:
// ...
}
// 不设 switch 条件时相当于 switch true
// 也就是会判断 case 是否为 true
// 相当于 if - else - if 的另一种写法
switch {
case x == 2:
fmt.Println("1")
fallthrough
case x < 3:
fmt.Println("2")
fallthrough
default:
fmt.Println("default")
}
switch 还可以用来判断类型:
// 判断类型
var ia interface{} // 只能判断 interface 的类型
ia = a
switch ia.(type) {
case int:
fmt.Println("int")
case float32:
fmt.Println("float")
case string:
fmt.Println("string")
case bool:
fmt.Println("bool")
default:
fmt.Println("default")
}
接口类型用于接收“任意类型”,可以给借口类型声明方法,则该接口只接收有指定方法的类型:
type Polygon struct {
height int
width int
}
// 为结构 Polygon 定义具体的方法
func (this *Polygon) GetPerimeter() int {
return (this.height + this.width) * 2
}
// 定义接口
// 这个接口上有一个名为 GetPerimeter 的抽象方法,返回一个 int
// 实现了 GetPerimeter 方法的对象,就可以被赋值给这个接口
type graph interface {
GetPerimeter() int
}
func main() {
var g graph
pa := Polygon{height 10, width 10}
g = &pa // 可以将 Polygon 类型赋值给 graph interface,接口都是指针
}
不声明任何方法的接口称为空接口,可以接收任何类型的变量:
func demo(val interface{}) {
// 这个 val 参数可以接受任意类型
}
可以对 interface 进行类型转换,转换为具体的类型:
if val, ok := i.(int); ok {
// success converted interface to int
}
interface 是 go 中非常强大的一个特性,妥善的用好 interface 可以实现 OOP 中的 duck typing。
比如任何实现了 Write
方法的类型,都可以被 Fprintf
输出。
不过尤其是要小心不要滥用空接口,不要因为空指针方便就在所有的地方滥用空接口。
下面介绍稍微复杂一些的语法,可以提供一些更高级的功能
Go 提供了一种用来判断变量类型的语法:
// 判断 element 是一个 interface
// T 是断言的类型
// ok 会返回一个 bool,判断 element 是否是 T 类型
// value 会返回 element 的值
value, ok = element.(T)
一个简单的例子:
// 判断类型
var va interface{} // 定义 va 为 interface
va = &pa
if t, ok := va.(*PolygonA); ok {
fmt.Printf("The type is: %T\n", t)
} else if t, ok := va.(*PolygonB); ok {
fmt.Printf("The type is: %T\n", t)
}
这个语法常被用于将 interface 转换为具体的数据类型
这种返回 ok 的语法非常普遍,判断字典中是否存在某个 key 也是这么写的
if val, ok := m["key"]; ok {
// do sth with val
}
go 提供了两个反射类型:reflect.Type
和 reflect.Value
。对应两个方法:reflect.TypeOf
和 reflect.ValueOf
,可以获取到任意变量(interface{})的类型和取值。
所以 reflect 主要用于和 interface{} 相配合,可以实现相当动态的特性。
var (
x float64 = 3.4
vx interface{}
)
// 给 vx 赋值,vx 是一个接口,并不知道原始数据的类型
vx = x
// 利用反射获取原始数据的类型和值
fmt.Println("type:", reflect.TypeOf(vx)) // type: float64
fmt.Println("value:", reflect.ValueOf(vx)) // value: 3.4
v := reflect.ValueOf(vx) // 获得 reflect.Value 类型
fmt.Println("value:", v)
fmt.Println("type:", v.Type()) // float64
fmt.Println("kind:", v.Kind()) // float64
fmt.Println("float value:", v.Float()) // 3.4
fmt.Println(v.Interface())
fmt.Printf("value is %5.2e\n", v.Interface())
y := v.Interface().(float64)
fmt.Println(y)
这个章节题目其实并不准确的,因为 Go 中所有的赋值都是值传递(浅拷贝)。
不过我们经常会看到一句话:
slice, map, channel, func, interface 是引用类型,其他都是值类型。
这句话严格地说也是错的,因为 go 里只有 value,顶多区分为 pointer value 和 nonpointer value,并不存在什么值类型和引用类型。前面说了,所有的赋值都是拷贝,不过有时候拷贝的是指针,所以看上去就像是引用类型了。
上述提到的几个类型,只是因为它们的 struct 里存放了指向数据的指针,而不是直接在栈里存放数据,所以我们在传参的时候,可以直接的传递:
func changeSlice(sli []int) {
sli[1] = 2
}
func main() {
sli := []int{1, 1, 1}
changeSlice(sli)
fmt.Println(sli) // {1, 2, 1}
}
但是需要注意的是,像 slice 这样的类型,会随着数据量变化而动态重新分配内存的,所以如果只是修改元素的值的话,确实可以 change-inplace,但如果改变了 slice 的长度,可能会导致重新生成一个新的 slice 长度:
func changeSlice(sli []int) {
sli[1] = 2
// type Slice struct {
// Array
// Capacity uint64
// }
// append 很可能会导致 slice 重新分配内存
// 所以 append 只能返回一个新的 head
sli = append(sli, 3) // 这句话不一定会对原始 slice 生效
}
func main() {
sli := []int{1, 1, 1}
changeSlice(sli) // 不会被改变
fmt.Println(sli) // {1, 1, 1}
}
这时候就需要传递 slice 的指针了
go 中的异常处理和 Java 等语言有较大的差异,需要特别的留意。
其异常处理最大的特点,大概就是:没有 try-catch
在 go 的哲学中,将错误从概念上区分为两类:
这也导致了 go 中广受争议的一种写法,好不好我就不评价了。
go 建议函数返回值的最后一个都是 error
func demo() (err error) {
return_val, err := some_func()
if err != nil {
// deal with err
// ...
// 向上一级返回 err
return errors.Wrap(err, "some message")
}
// 然后才是正常的逻辑代码
// ...
}
当程序遇到一个 panic 异常,会中止当前程序,并且在程序返回时将 panic 抛给上一层调用者,最终 panic 会上浮到 main 函数,并最终导致程序退出。
你也可以使用 recover 来捕获 panic(不要轻易使用 recover!)。而且我们前面提到过 defer 的调用是在函数返回之前,所以我们可以结合使用这两者来在函数内捕获 panic
func demo() {
defer func() {
if err:=recover(); err!=nil {
// deal with panic
}
}()
// 正常的代码
// ...
// 抛出一个 fatal
panic("some message")
}
go 的包结构是被业界广为争论的一点,因为 google 内部使用单一 repo,所以 golang 也在设计上认为所有的项目都会放在一起,并且全部保持在主干。
$GOPATH/src
内应该包含所有的源代码,其结构是:$GOPATH/src/<repositray>/<user>/<project_name>
,比如,拿我举个例子,我在使用 github,我的名字是 Laisky,我打算开一个名为 demo 的项目练练手,那么我应该在这么做:
mkdir -p $GOPATH/src/github.com/Laisky/demo
cd $GOPATH/src/github.com/Laisky/demo
vi main.go // 开始编码
main.go 形如:
package main // 包名
// 必须有一个 main 函数
func main() {
}
任何人想要使用这个包,只需要 import 即可,import 有几种不同的写法:
// 导入 package,可以使用 package name
// demo.xxx
"github.com/Laisky/demo"
// 重命名 package name
// demo2.xxx
demo2 "github.com/Laisky/demo"
// 初始化,但是不导入
_ "github.com/Laisky/demo"
// 将 package 内的变量导入当前文件
// xxx
. "github.com/Laisky/demo"
项目内也可以创建自文件夹作为子包,比如
├── tasks
│ ├── default.go
│ ├── elasticsearch.go
│ └── heartbeat.go
├── main.go
我有一个 tasks 自文件夹,里面放了三个文件,那么这三个文件的内的 package 声明都应该是:
package tasks
同一个 package 内共享同一个 scope,所以 package 内的公共变量不能重名,同 package 的所有公共变量都可以直接使用
go 中对公共私有的区分相当简单,首字母大写的都是公共。
这一规定适用于 package 内的变量名以及 struct 中的 field 名。
var exampleVar int // 私有变量
var ExampleVar int // 公共变量
因为在谷歌内部,所有代码都在一个 repo,并且只有 master branch,所以 go 语言在面世之初根本没有考虑版本管理和依赖管理的问题。
比如全局唯一 GOPATH,因为依赖都是放在 $GOPATH/src
下,所以导致所有项目共享依赖。依赖直接就是代码包的 master,所以几乎完全没有版本管理。
不过业界还是需要版本管理的,所以现在也有了一系列的最佳实践。
主要的分歧就是:GOPATH
是否唯一?
这个很好理解,因为代码都是放在 $GOPATH/src
的,所以只要每一个项目都用一个不同的 GOPATH,就能实现资源隔离了。
一般可以用 autoenv 来实现自动的切换环境变量
所有的项目还是放在全局的 GOPATH 下,不过每一个项目的依赖都放在项目路径内的 vendor 文件夹内,如 $GOPATH/src/github.com/Laisky/demo/vendor
,在 build 的时候,go 会优先的在 vendor 里寻找依赖。
不过这样做的话,就不能用 go get
安装依赖了,而是要借助第三方包,比如 glide
。
一些常用命令
cd $GOPATH/src/github.com/Laisky/demo
# 初始化项目
glide init
# 安装依赖
glide install xxx
# 运行
go run main.go
## 编译
go build . # 编译为二进制文件后,放进 $GOPATH/bin
go build main.go # 编译成二进制文件 main
goroutine 是 golang 中最为出彩的特性,这里做一个简单的介绍。
过去在做并行编程的时候,我们需要考虑采用线程、进程或是协程。
在编程的时候还要考虑复杂的回调,小心翼翼的避免阻塞,具有相当的难度。而 go 将这一切调度都封装进了 runtime scheduler,对外提供统一简单的 goroutine,让你可以用几乎完全同步的代码,来编写并发程序。
而且每一个 goroutine 仅需要 2KB 的初始栈空间(JAVA 是 1 MB),在每台机器上启动数十万的 goroutine 也不算奇怪。
开发人员可以更加专注于逻辑的实现,而不是运行资源的调度。
启动一个 goroutine 非常简单,在调用任何函数的时候,使用 go
关键词即可:
// 一个普通的阻塞函数
func say(s string) {
time.Sleep(time.Second * 1)
fmt.Println(s)
}
// 最终会输出 "hello bob"
func main() {
go say("bob") // 启动 goroutine,此句会立刻返回
go say("bob")
go say("bob") // 并发😄
fmt.Printf("hello ")
time.Sleep(time.Second * 2) // 等待 goroutine 结束
}
所以你现在可以在把所有阻塞的操作全部扔到一起,然后在主逻辑简单轻松的 go 启动 goroutine,就可以立刻去着手处理其他的逻辑了,而完全不用再考虑阻塞问题。
就像用线程一样的简单,然后还比线程轻量。
不过 goroutine 的使用需要特别小心,不要滥用 goroutine 的并发,要时刻关注并发中的资源使用率,以及 goroutine 的回收情况。
因为 goroutine 实在是太容易被滥用,现在社区都开玩笑说下一版可以考虑加一个 goroutine GC 了……
关于 goroutine 的实现细节,在后续的分享中会有详细介绍。
这里只介绍用法,用起来就是这么的简单!
python、javascript、c# 等语言采用 Promise(async/await) 这种形式来做同步,而 go 采用了阻塞消息队列来实现同步。
channel 用来实现 goroutine 间的通信,是一个 FIFO 的队列,通过入队阻塞和出队等待,来实现 goroutine 间的同步。
// 通过 make 创建 channel
c := make(chan int)
// 下面这两行是伪代码
// 这两行并不能写在一起,而应该写在不同的 goroutine 中,否则这样会阻塞的
ch <- v // 发送 v 到channel ch.
v := <-ch // 从 ch 中接收数据,并赋值给 v
v, ok := <- ch // 如果 ok==false,说明 ch 已关闭
// 关闭 channel
ch.Close()
也可以通过给 channel 设置 buffer,来部分的解除入队操作的阻塞
// 创造一个带 buffer 的 channel
// value 是 buffer 大小
// 当 value == 0 时,相当于是一个阻塞的 channel
ch := make(chan type, value)
// push 时,若还有 buffer,则为非阻塞操作
// 若 buffer 已用完,则为阻塞操作
// pull 同理
ch <- 1
一些方便的用法:
可以使用 range 来遍历 channel
// 可以用 range 监听 channel
// 每当 channel 里被 push 了消息,就执行一次循环
// 直到 channel 关闭才会退出循环
for i := range c {
fmt.Println(i)
}
可以用 select 来监听多个 channel
// 哪个 channel 先返回就执行谁
// 也可以通过设置 default 来让 pull 操作变为非阻塞(相当于 q.get_nowait)
select {
case i := <-c:
// use i...
case <- time.After(5 * time.Second):
// 设置 timeout...
default:
// 当 c 阻塞的时候就会执行这里
// 设定 default 让 select 变成非阻塞操作
}
我们可以用 channel 来改写前面的函数,利用 channel 来做同步:
var ch = make(chan int)
func say(v string) {
time.Sleep(time.Second * 1)
fmt.Println(v)
ch <- 0
}
func main() {
go say("bob") // 启动 goroutine
fmt.Printf("hello ")
// 最终会输出 "hello bob"
// 等待 goroutine 执行完成
<-ch
}
顺带一提,channel 还可以是单向的(只读或只写)
// 声明只读的 channel
make(<-chan string)
// 声明只写的 channel
make(chan<- string)
单向 channel 一般用户函数参数,限定函数对 channel 的使用:
// 只允许函数内写入 channel
func ping(pings chan<- string, msg string) {
pings <- msg
}
// 只允许读取 channel
func pong(pings <-chan string, pongs chan<- string) {
msg := <-pings
pongs <- msg
}
因为是面向初学者,我就简单介绍其中的一部分工具了
互斥锁 sync.Mutex
既然是并发,就绕不开资源竞争(race condition)和敏感区(critical section),所以需要使用锁来进行访问控制。
// 互斥锁的基本操作
l := sync.Mutex{}
l.Lock()
l.UnLock()
比较简单,就不多说了,可以嵌套在其他类型里做访问控制
type struct Demo {
sync.Mutex
Field string
}
一种基于等待和通知的同步机制,常用的方法有:
func runWithCond(cond *sync.Cond, f func()) {
defer cond.Signal() // 唤醒一个 cond.Wait
f()
}
// 创建 condition 需要传入一个锁住的互斥锁
lock := &sync.Mutex{}
lock.Lock()
cond := sync.NewCond( lock) // 返回一个 *cond 指针
go runWithCond(cond, output1)
cond.Wait()
专门用于同步多个 goroutine 的工具
用于同步 goroutine 的工具
func goroutineDemo(wg *sync.WaitGroup) {
defer wg.Done() // Done 会使 wg 的计数减一
}
func main() {
wg := &sync.WaitGroup{}
wg.Add(2) // 给 wg 的计数增加 2
go goroutineDemo(wg)
go goroutineDemo(wg)
wg.Wait() // 等待 wg 的计数清零
}
go 中的 map 并不是 thread-safe 的,所以在多个 goroutine 进行并发写入的时候,go 会直接报错。
在 goroutine 中,应使用 sync.Map
var m = &sync.Map{}
// 存数据
m.Load(key)
// 取数据
m.Store(key, valg)
// 删数据
m.Delete(key)
// 遍历
func (m *Map) Range(f func(key, value interface{}) bool)
回头再看一下这个列表,简单一句话过一遍,就不细讲了:
v1.5 以前,goroutine 默认只会使用一个 CPU,使用多核的话,需要调整 runtime 的参数。
runtime 的几个常用方法:
全局设定,一般在 main 函数内调用:
runtime.NumCPU()
// 返回当前系统的 CPU 数runtime.GOMAXPROCS(n)
// 设置可用的 CPU 数(v1.5 后不用再设置了)goroutine 内的操作:
runtime.Gosched()
// 让出 CPUruntime.Goexit()
// 退出当前 goroutineGC 是 go 早期版本(1.6 以前)饱受诟病的地方,1.2 版本中,stop-the-world pause 经常高达数秒。
从 1.5 开始采用 concurrent gc,并在后续的几个版本里着重优化 gc,在 1.9 及以后的版本中,中断时间基本可以维持在 1 ms 以下(和堆中的数据数量相关)。
我们可以直观的感受下 go 的开发团队对 gc 的优化是多么的富有成效
Go 的 GC 是 concurrent GC,使用了三色标识法就行回收,所以只有在最终的回收阶段会出现 stop-the-world pause。
这种做法的优点是暂定时间短,可以实现相当低的延迟(low latency)。缺点是因为并发运行,会牺牲掉一些性能(throughput)。不过因为 goroutine 的效率相当高,所以整体性能也非常可观。
Go 的发布非常简单,只需要使用 $go build .
打包成二进制文件后,就可以直接发布到服务器上运行
可以使用 docker 的 multi stage build,先编译 go 二进制文件,然后再把二进制文件拷贝进一个干净的新容器发布,极大的节省空间:
FROM golang:1.9.4-alpine3.6 AS gobin
RUN mkdir -p /go/src/pateo.com/go-ramjet
RUN apk update && apk upgrade && \
apk add --no-cache bash git openssh
ADD . /go/src/pateo.com/go-ramjet
WORKDIR /go/src/pateo.com/go-ramjet
RUN go build main.go
FROM alpine:3.6
COPY --from=gobin /go/src/pateo.com/go-ramjet/main .
CMD ["./main"]
比如我目前在做一个程序,二进制文件 15M,打包成镜像后仅 20M,压缩后发布,仅需要传输 7MB 即可直接运行。
最后来简单的总结一下 golang 的编码规范
每个包都应该有一个包注释,包如果有多个go文件,就只需要在入口文件写包注释。 第一行以 Package 开头。
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is Governed by a BSD-style
// license that can be found in the LICENSE file.
// Package strings implements simple functions to manipulate strings.
package strings
可导出函数必须要有注释,第一行以函数名开头
可导出的函数以大写字母开头
// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {
// Request represents a request to run a command.
type Request struct { ...
import (
"encoding/json" //标准包
"strings"
"myproject/models" //内部包
"myproject/utils"
"github.com/go-sql-driver/mysql" //第三方包
)
不要使用相对路径
// 这是不好的导入
import "../net"
// 这是正确的做法
import "github.com/repo/proj/src/net"
今天的基础介绍就到这里。
谢谢各位的参与和倾听。
中国作为 Golang 社区最活跃的国家,网上有大量的 golang 中文资料,建议各位都回去搜一搜,找到本自己喜欢的材料,认真阅读。(不用太挑教材,入门阶段都大同小异)