Go v1.10

提纲

本系列课程面向 golang 的初学者,分为四个部分:

  • [2 hr] Golang 语法接触
  • [1 hr] Web 编程简介
  • [1 hr] 测试与性能分析
  • [1 hr] 深入理解 Golang

AboutMe

CaiZhonghua(Laisky) 云平台部架构师

Email:

课件放在:

go-lesson-slides-1

https://s3.laisky.com/public/slides

我不是 golang 的专家,只是作为一个平时也喜欢用 golang 的开发者,来抛砖引玉的分享一下我对 golang 的理解。

也欢迎各位在后续的使用中,分享自己的心得。

提纲

本系列课程面向 golang 的初学者,分为四个部分:

  • [2 hr] Golang 语法接触
  • [1 hr] Web 编程简介
  • [1 hr] 测试与性能分析
  • [1 hr] Dive into Golang

概述

http://blog.csdn.net/baobeijuzi/article/details/43418993

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语言迎来了第一个引人注目的里程碑。

!

主要的优点

  • 静态类型
  • 快速编译
  • 高效 GC
  • 部署简易
  • goroutine
  • 实力雄厚的爹

Go 的哲学是“少即是多”,用像 Python 一样的语法写出 c++ 的性能。

比起其他很多静态语言花哨的特性,Go 最大的特点就是高度工程化,一切的设计都是以现实工程为导向的,尽可能的解决实际的工程问题。并且 Go 高度重视性能,追求高并发低延迟,是一款非常强力的语言。

这也是为什么 Go 在知乎 PL 大佬眼中一文不值,但却在工业界迅速蔓延的原因。

Golang 从 2013 年至今的趋势

再加上 Java

安装配置 go

具体的安装很简单,到官网下载相应的安装包即可,不再累述。

安装前需要配置一下环境变量

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

Golang 语法简介

下面就来过一遍 go 的语法,各个编程语言间大同小异,这里简单过一遍,有一个大致的印象,建议各位回去后自己写几段小代码,熟悉熟悉 go 的用法,我们下周开始会讲一些框架、调试相关的,如果有一点实际经验的话,效果会更好。

分为以下几个步骤来介绍:

  • 变量
  • 数据结构
  • 流程
  • interface

变量

变量类型有:

  • 整数(包含有符号和无符号类型)
    • int
    • int8 / byte(alias)
    • int16
    • int32 / rune(alias)
    • int64
    • uint / uint8 / uint6 / uint32 / uint64
  • 浮点型
    • float32
    • float64
  • complex
    • complex64
    • complex128
  • 字符串,都采用 UTF-8 编码,用 "" 或 `` 表示
    • string
  • bool
    • true
    • false
  • pointer
  • error

变量的声明与使用

// 定义变量,类型放在最后
// 变量声明时会被默认赋予零值
// 分别为: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
)

指针

使用 & 取地址,* 取值:

var (
    a int = 2
    p = &a  // 取地址
)

*p = 3  // 取值并赋值,a 的值被改掉

这里只简单的介绍下指针的用法,稍候会更详细的说一说

作用域

go 的作用域从小到大来排序有:

  • block
  • 文件
  • builtin

block

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,包括类型、取值和内建函数,可以在任意地方使用。

Builtins 内建函数

  • 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:虚部。

数据结构

包含:

  • struct
  • map
  • array:长度不可改变
  • slice:长度可变

struct

// 声明结构体
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
}

Array

数组的长度不可改变,在声明时就要确定数组成员的类型和数组长度:

// 声明数组
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}}  // 简写

Slice

这是一种可变长的队列

// 创建切片
// 切片长度不确定,可以像数组一样赋值
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

// 声明 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 函数
  • method 方法
  • for 循环
  • switch 分支选择

func 函数

定义函数:

// 需要定义参数的类型和返回的类型
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 时,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

Method 方法

方法,指的是绑定在任意 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 可以分为两种:

  • pointer methods
  • value methods

区别就在于,绑定的时候传入的是 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 的原因。

for 循环

go 只有 for 循环

// 定义 初始值,目标和步长的完整 for 循环
for i := 0; i < 10; i++ {
    sum += i
}

// 也可以简写
// 默认初始值为 0,步长为 1
for ; sum < 1000; {}

// 其实连分号都可以省略,用 while 一样的语法
for sum < 1000 {}

// 死循环
for {}

for 中还可以使用 continuebreak

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
// 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")
}

interface

接口类型用于接收“任意类型”,可以给借口类型声明方法,则该接口只接收有指定方法的类型:

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 输出。

不过尤其是要小心不要滥用空接口,不要因为空指针方便就在所有的地方滥用空接口。

语法深入

下面介绍稍微复杂一些的语法,可以提供一些更高级的功能

comma-ok 断言

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.Typereflect.Value。对应两个方法:reflect.TypeOfreflect.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 的哲学中,将错误从概念上区分为两类:

  • error:代码逻辑可处理的异常,通过日志等方式处理掉;
  • panic:未曾预期的异常,会导致程序惊恐(panic),一般还会导致程序退出。

error

这也导致了 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 抛给上一层调用者,最终 panic 会上浮到 main 函数,并最终导致程序退出。

你也可以使用 recover 来捕获 panic(不要轻易使用 recover!)。而且我们前面提到过 defer 的调用是在函数返回之前,所以我们可以结合使用这两者来在函数内捕获 panic

func demo() {
    defer func() {
        if err:=recover(); err!=nil {
            // deal with panic
        }
    }()

    // 正常的代码
    // ...

    // 抛出一个 fatal
    panic("some message")

}

项目结构

包括:

  • 安装 golang
  • 包结构
  • 依赖管理

包结构

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

这个很好理解,因为代码都是放在 $GOPATH/src 的,所以只要每一个项目都用一个不同的 GOPATH,就能实现资源隔离了。

一般可以用 autoenv 来实现自动的切换环境变量

全局 GOPATH

所有的项目还是放在全局的 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

启动一个 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 的实现细节,在后续的分享中会有详细介绍。

这里只介绍用法,用起来就是这么的简单!

channel

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 还可以是单向的(只读或只写)

// 声明只读的 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
  • RWMutex
  • Once
  • WaitGroup
  • Cond
  • Map
  • Pool

因为是面向初学者,我就简单介绍其中的一部分工具了

Mutex

互斥锁 sync.Mutex

既然是并发,就绕不开资源竞争(race condition)和敏感区(critical section),所以需要使用锁来进行访问控制。

// 互斥锁的基本操作
l := sync.Mutex{}

l.Lock()
l.UnLock()

比较简单,就不多说了,可以嵌套在其他类型里做访问控制

type struct Demo {
    sync.Mutex
    Field string
}

Cond 条件锁

一种基于等待和通知的同步机制,常用的方法有:

  • .Wait():等待唤醒
  • .Signal():唤醒一个 Wait
  • .Broadcase():唤醒所有 Wait
  • .L:获取到内部的互斥锁
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()

WaitGroup

专门用于同步多个 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 的计数清零
}

sync.Map

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)

回头再看一下这个列表,简单一句话过一遍,就不细讲了:

  • Mutex:互斥锁
  • RWMutex:读写锁,允许多读
  • Once:单次运行
  • WaitGroup:等待一组 goroutine
  • Cond:Condition 锁
  • Map:goutine 安全的 map
  • Pool:内存池,用来缓存类型,高频创建类型时提高性能

runtime

v1.5 以前,goroutine 默认只会使用一个 CPU,使用多核的话,需要调整 runtime 的参数。

runtime 的几个常用方法:

全局设定,一般在 main 函数内调用:

  • runtime.NumCPU() // 返回当前系统的 CPU 数
  • runtime.GOMAXPROCS(n) // 设置可用的 CPU 数(v1.5 后不用再设置了)

goroutine 内的操作:

  • runtime.Gosched() // 让出 CPU
  • runtime.Goexit() // 退出当前 goroutine

GC

GC 是 go 早期版本(1.6 以前)饱受诟病的地方,1.2 版本中,stop-the-world pause 经常高达数秒。

从 1.5 开始采用 concurrent gc,并在后续的几个版本里着重优化 gc,在 1.9 及以后的版本中,中断时间基本可以维持在 1 ms 以下(和堆中的数据数量相关)。

我们可以直观的感受下 go 的开发团队对 gc 的优化是多么的富有成效

1.5

1.6

1.8

简介

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 { ...

生成文档

go 可以非常简单的生成网页版的文档,仅仅需要执行

godoc <package_name> --http=:12800


# 或者直接进入项目根目录
godoc --http=:12800

document

命名

  • 全局变量:驼峰式,可导出的使用大写字母开头
  • 参数传递:驼峰式,小写字母开头
  • 局部变量:下划线风格命名

import

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 中文资料,建议各位都回去搜一搜,找到本自己喜欢的材料,认真阅读。(不用太挑教材,入门阶段都大同小异)