并发
并发最难的部分就是要确保其他并发运行的进程、线程或goroutine不会意外修改用户的数据。
-
goroutine
goroutine是可以与其他goroutine并发执行的函数。
复制代码
-
通道
通道是go内置的数据结构,可以让用户在不同的goroutine传递具有类型的的消息。通道用于在几个运行的goroutine之间发送数据。
复制代码
Go类型系统
Go开发者使用组合设计模式,只需要简单的将一个类型嵌入到另一个类型就复用所有功能。
-
类型简单
用户定义的类型通常包含一组带类型的字段,用于存储数据。Go开发者构建更小的类型例如Customer和Admin,然后把这些小类型组合成更大的类型。
复制代码
-
Go接口对一组行为建模
接口用于描述类型的行为。如果一个类型实现了一个接口,意味着这个类型可以执行一组特定行为。
复制代码
-
引用类型
通道(channel)、映射(map)和切片(slice)都是引用类型,不过通道本身实现的是一组带类型的值,这组值用于在goroutine之前传递数据。通道内置同步机制,从而保证通信安全。
复制代码
命令
数组
数组是切片和映射的基础数据结构,Go中数组是一个长度固定的数据类型,用于存储一段具有相同数据类型的元素连续块。在Go语言中,声明变量总会使用对应类型的零值来对变量进行初始化,数组初始化时,数组内每个元素都初始化对应类型的零值。
-
声明
array := [n]int {x0-x(n-1)}
使用...替代数组长度:array := [...]{0,1,2,3}
声明数组指定元素的值:array := [...]{0:1,2:3}
复制代码
-
赋值
同样类型的数组可以赋值给另外一个数组。
复制代码
-
函数间传递数组,使用指针在函数间传递大数组
函数之间传递变量时,总是以值得方式传递的。如果传递的变量是一个数组,不管数组多长,都会完整复制,传递给函数。
复制代码
切片
切片是一种数据结构,切片的底层内存也是在连续块中分配,所以切片能够获得索引、迭代以及为垃圾回收优化的好处。
-
实现
切片是一个很小的对象,包含了3个字段的数据结构,这些字段包含需要操作底层数组的元数据。这三个字段分别是指向底层数组的指针、切片访问的元素的个数(长度)和切片允许增长的元素个数(容量)。
复制代码
-
创建和初始化
// 长度和容量都是5
使用长度声明一个字符串切片: slice := make([]string, 5)
// 长度是3,容量是5
使用长度和容量声明整型切片: slice := make([]int, 3, 5)
// 长度个容量一致
通过切片字面量来声明切片:slice := []string{...}
// 创建空整型切片,初始化第100个元素
使用索引来声明切片:slice :=[]int {99:100}
复制代码
-
nil和空切片
// 创建nil整型切片,指针nil,长度0,容量0
var slice []int
// 利用初始化,通过声明一个切片可以创建一个空切片
// 指针,长度0,容量0
slice := make([]int, 0)
slice := []int{}
不管是nil切片还是空切片对其调用append,len,cap的效果都是一样的
复制代码
-
计算长度和容量
slice := []int {1,2,3,4,5}
newSlice := slice[1:3]
对于底层数组容量是k的切片slice[i:j]来说
长度:j-i
容量:k-i
// 两个切片共享一个底层数组,如果一个切片修改了改底层数组的共享部分,另外一个切片也能感知。
复制代码
-
切片增长
append会智能处理底层数组容量增长,在切片容量小于1000个元素时,总是会成倍的增加容量,一旦元素超过1000个,容量的增长因子会设为1.25。
复制代码
-
使用三个索引创建切片
slice := source[2:3:4] // i:j:k
复制代码
-
迭代切片
// range 创建每个元素的副本,而不是直接返回对该元素的引用
// 迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以value的地址总是相同的
for index, value := range slice {
}
复制代码
映射
映射是一种数据结构,用于存储一系列无序的键值对。映射能够基于键快速检索数据。键就像索引一样,指向与该键关联的值。
-
实现
映射是一个集合,可以使用类似处理数组和切片的方式迭代映射中的元素,但是映射是无序的。无序的原因是因为映射的实现使用了散列表。
复制代码
-
创建和初始化
映射的键可以是任何值,这个值可以是内置类型,也可以是结构类型,只要这个值可以用==运算符做比较。切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能做为映射的键。
dict := make(map[string]int)
dict := map[string]string{"key1":"value1"}
复制代码
-
使用映射
m := map[string]string{}
// 通过声明映射创建一个nil映射
var color = map[string]string
//nil映射不能用于存储键值对
//函数中传递映射并不会制造出该映射的一个副本
复制代码
类型
// var,通常使用var来创建一个变量并初始化为其零值
// := 短变量声明操作符,短变量声明操作符一次操作完成两件事情,声明一个变量,并初始化
// 数值类型零值=0
// 字符串的零值=""
// bool型的零值=false
复制代码
-
自定义类型
// 使用type 声明一个结构体
type user struct {
name string
}
// var,通常使用var来创建一个变量并初始化为其零值
var u user
// := 短变量声明操作符
u := user{}
// 基于一个已有类型,将其作为新类型的类型说明
// 标准库time中描述时间间隔类型
type Duration int64
// ok
dur := Duration(int64(1))
// err
i := int64(Duration(1))
复制代码
-
方法
定义:如果有一个函数有接收者,这个函数就被称为方法
接收者:值接收者和指针接收者
如果使用值接收者声明方法,调用时会使用这个值得一个副本来调用方法
指针接收者使用实际值来调用方法
// 使用值接收者实现的方法
func (u user) notify() {
// todo
}
// 使用指针接收者实现的方法
func (u *user) changeName() {
}
复制代码
-
引用类型
Go 语言里的引用类型有:切片、映射、通道、接口和函数类型。
从技术细节上讲,字符串也是一种引用类型。
引用类型创建变量时,创建的变量被称作标头(header)值。
复制代码
-
类型本质
结构类型可以用来描述一组数据值
type Time struct {
sec int64
nsec int32
loc *Location
}
// 本质原始类型,返回了方法内的Time值副本
func (t time) Add(d Duration) Time {
...
return t
}
// 非原始类型
type File struct {
*file
}
type file struct {
fd int
name string
dirinfo *dirInfo
nepipe int32
}
// 如果一个创建用的工厂函数返回了一个指针,就表示这个返回的值的本质是非原始的
func Open(name string) (file *File, err error) {
return OpenFile(name, O_RDONLY, 0)
}
// 使用值接收者还是指针接收者,不应该由方法是否修改了接收到的值来决定
// 这个决策应该基于该类型的本质
// 例外:需要让类型值复合某个接口的时候,类型的本质是非原始本质的,也可以使用值接收者声明方法
func (f *File) Chdir() error {
...
return f
}
复制代码
-
接口
多态是指代码可以根据类型的具体实现采取不同的行为的能力。任何用户定义的类型都可以实现任何接口,所以对这个接口值方法的调用自然就是一种多态。
接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。如果用户定义的类型实现了某个接类型声明的一组方法,那么这个用户定义的类型的值就可以赋值给换个接口类型的值。
用户定义的类型通常称为:实体类型,因为如果离开内部存储的用户定义的类型的值实现,接口值并没有具体的行为。
标准包经典示例:
// io.Reader
// io.Writer
var b bytes.Buffer
b.Write([]byte("hello"))
fmt.Fprintf(&b, "World")
io.Copy(os.Stdout, &b)
复制代码
方法集
type notifier interface {
notify()
}
type user struct {
name string
email string
}
// 使用指针接收者
func (u *user)notify() {
// send code...
}
func sendNotification(n notifier) {
n.notify()
}
func main() {
u := user{}
// ok
sendNotification(&u)
// error:notify method has pointer receiver
sendNotification(u)
}
// 规范的方法集描述
// T 类型的值方法包含值接收者声明的方法
// *T 指向T类型的指针的方法集包含值接收者胜的方法也包含指针接收者声明的方法
// 因为不是总能获取一个值得地址,所以值得方法集只包括了使用值接收者实现的方法。
Values Methods Receviers
-------------------------------------
T (t T)
*T (t T) and (t *T)
接收者 参数
Methods Receviers Values
-------------------------------------
(t T) T and *T
(t *T) *T
复制代码
多态
type notifier interface {
notify()
}
type user struct {
name string
email string
}
func (u *user)notify() {
// send code by user...
}
type admin struct {
name string
email string
}
func (a *admin)notify() {
//send code by admin...
}
func sendNotification(n notifier) {
n.notify()
}
func main() {
u := user{}
sendNotification(&u)
a := admin{}
sendNotification(&a)
}
复制代码
-
嵌入类型
type user struct {
name string
}
type admin struct {
user
level int
}
func (u *user) age() {
return 18
}
// call
a := admin{user:user{}, level:1}
// 可以直接访问内部类型的方法
a.user.age()
// 内部类型方法也被提升到外部类型
a.age()
// up
type notifier interface {
notify()
}
func (u *user) notify() {
fmt.Println("send msg")
}
func sendNotification(n notifier) {
n.notify()
}
// call
sendNotification(&a)
复制代码
-
公开或未公开的标识符
// alertConuter 是一个未公开的类型
tyep alertConuter int
//一个标识符以小写字母开头时,这个标识符就是未公开的,即外表不可见
func New(value int) alertCounter {
return alertCounter(value)
}
// code 1:
package entities
type User struct {
Name string
email string
}
package main
func main() {
// ok
u := entities.User {
Name : "bill",
}
// 字段 email 未知
u := entities.User {
Name : "bill",
emial: "bill@bill.com",
}
}
code 2:
package entities
type user struct {
Name string
Email string
}
type Admin struct {
user
Age int
}
package main
func main() {
// ok
a := entities.Admin {
age : 10,
}
// 设置未公开的内部类的公开字段的值
a.Name = "bill"
a.Email = "bill@bill.com"
}
复制代码
并发
Go语言里的并发指的是能让某个函数独立于其他函数运行的能力。Go语言的并发同步模型来自一个叫通信顺序进程(CSP)的范型。CSP是一种消息传递模型,通过在goroutine之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在goroutine之间同步和传递数据得关键数据类型叫做通道(channel)。
-
并发与并行
当运行一个程序的时候,操作系统会为这个应用程序启动一个进程。这个进程包含了应用程序在运行中需要用到和维护的各种资源的容器。主要分为:内存、句柄、线程
一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程只是包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序本身的空间,所以当主线程终止时,应用程序也会终止。
操作系统会在物理处理器上调度线程来运行,而Go语言的运行时会在逻辑处理器上调度goroutine来运行。每个逻辑处理器都分别绑定到单个操作系统线程。Go1.5以上,Go语言运行时默认会为每个可用的物理处理器分配一个逻辑处理器,这些逻辑处理器会应用执行所有被创建的goroutine。
调度器对可以创建的逻辑处理器的数量没有限制,但语言运行时默认限制每个程序最多创建10000个线程。可以通过runtime/debug -> SetMaxThreads 来修改。
并发不是并行,并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情。
复制代码
-
goroutine
一个正在运行的goroutine在工作结束前,可以被停止并重新调度。
复制代码
-
竞争状态
如果两个或多个goroutine在没有相互同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就出相互竞争的状态,这种情况叫做竞争状态。
// 用竞争检测器标志来编译程序
go build -race
// 当前的goroutine从线程退出,并放回到队列
runtime.Gosched()
Go语言提供了传统的同步机制,就是共享资源加锁。atomic和sync包里的函数提供了很好的解决方案。
复制代码
-
锁
原子函数
原子函数能够以很底层的加锁机制来同步访问整型变量和指针。
复制代码
互斥锁
另一种同步访问共享资源的方式是使用互斥锁(mutex)。互斥锁这个名字来自互斥(mutual exclusion)的概念。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码
复制代码
-
通道
原子函数和互斥锁都能工作,但是依靠它们都不会让编写并发程序变得更简单,更不容易出错。通过通道发送和接收需要共享的的资源,在goroutine之间做同步。
当一个资源需要在goroutine之间共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据得机制。
// 无缓冲通道
unbuffered := make(chan int)
// 有缓冲通道
buffered := make(chan string, 10)
// 向通道发送值
buffered <- "val"
// 从通道接收一个值
value := <- buffered
// 关闭通道
close(buffered)
复制代码
无缓冲通道
无缓冲通道是指在接收前没有能力保存任何值得通道。这种类型的通道要去发送goroutine以及接收的goruntine都要准备好,才能完成发送和接收操作。如果两个goroutine没有准备好,通道会导致先执行发送或者接收操作的goroutine阻塞等待。无缓冲通道保证进行发送和接收的goroutine会在同一时间进行数据交换。
复制代码
有缓冲通道
有缓冲通道是一种在被接收前能存储一个活多个值的通道。只有在通道中没有要接收值时,接收才会阻塞,只有在通道没有可以用缓冲区容纳被发送值时,发送动作才会阻塞。当通道关闭后,goroutine依旧可以从通道接收数据,但是不能再向通道发送数据。
复制代码
并发模式
-
runner
// runner 包管理处理任务的运行和生命周期
package runner
import (
"errors"
"os"
"os/signal"
"time"
)
type Runner struct {
// 发送的信号
interrupt chan os.Signal
// 报告处理任务已经完成
complete chan error
// timeout 宝盖处理任务已经超时
timeout <- chan time.Time
// 持有一个组以索引顺序依次执行的函数
tasks []func(int)
}
// 会在任务执行超时时返回
var ErrTimeout = errors.New("received timeout")
// 会在接收到操作系统的时间返回
var ErrInterrupt = errors.New("received interrupt")
// 返回一个新的准备使用Runner
func New(d time.Duration) *Runner {
return &Runner{
interrupt : make(chan os.Signal, 1),
complete: make(chan error),
timeout: time.After(d),
}
}
// 将任务加到Runner上,接收一个int类型的ID作为参数
func (r *Runner) Add(tasks ...func(int)) {
r.tasks = append(r.tasks, tasks...)
}
// Start 执行所有任务,并监视通道事件
func (r *Runner) Start() error {
signal.Notify(r.interrupt, os.Interrupt)
go func() {
r.complete <- r.run()
}
select {
case err := <- r.complete:
return err
case <- r.timeout:
return ErrTimeout
}
}
func (r *Runner) run() error {
for id, task := range r.tasks {
if r.gotInterrupt() {
return ErrInterrupt
}
task(id)
}
return nil
}
// 一般来说select语句在没有任何要接收的数据时会阻塞,不过有了default分支就不会阻塞
func (r *Runner) gotInterrupt() bool {
select {
case <-r.interrupt:
signal.Stop(r.interrupt)
return true
default:
return false
}
}
复制代码
package main
import(
...
)
const timeout = 3 * time.Second
func main() {
r := runner.New(timeout)
r. Add(creteTask(), createTask())
if err := r.Start(); err != nil {
switch err {
case ruuner.ErrTimeout:
// 超时
os.Exit(1)
case runner.ErrInterrupt:
// 系统终止
os.Exit(2)
}
}
}
// 完成
}
func createTask() func(int) {
return func(id int) {
// processor
}
}
复制代码
-
pool
pool用于展示如何使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间及独立使用的资源。
package pool
import (
"errors"
"log"
"io"
"sync"
)
type Pool struct {
m sync.Mutex
resources chan io.Closer
factory func() (io.Closer, error)
closed bool
}
var ErrPoolClosed = errors.New("Pool has been closed.")
func New(fn func()(io.Closer, error), size uint) (*Pool, error) {
if size <= 0 {
return nil, errors.New("Size value too small.")
}
return &Pool{
factory: fn,
resources: make(chan io.Closer, size)
}, nil
}
}
func (p *Pool) Acquire() (io.Closer, error) {
select {
// 检查是否有空闲的资源
case r, ok := <- p.resources:
if !ok {
return nil, ErrPoolClosed
}
return r, nil
// 没有空闲的资源可用,创建一个新资源
default:
return p.factory()
}
}
func (p *Pool) Release(r io.Closer) {
// 保证本操作和close操作安全
p.m.Lock()
defer p.m.Unlock()
if p.closed {
r.Close()
return
}
select {
// 试图将这个资源放入队列
case p.resource <- r:
// 放入
default:
// 已经满队列了
// 关闭资源
r.Close()
}
}
// Close 会让资源池停止工作,并关闭所有现有资源
func (p *Pool) Close() {
p.m.Lock()
defer p.m.Unlock()
if p.closed {
return
}
p.closed = true
// 在情况通道里的资源之前,将通道关闭
close(p.resources)
for r := range p.resources {
r.Close()
}
}
复制代码
package main
import (
...
)
const (
maxGoroutines = 25
pooledResources = 2
)
type dbConnection struct {
ID int32
}
func (dbConn *dbConnection) Close() error {
// close
return nil
}
var idCounter int32
func createConnection() (io.Closer, error) {
id := atomic.AddInt32(&idCounter, 1)
// new connection id
return &dbConnection{id}, nil
}
func main() {
var wg sync.WaitGroup
wg.Add(maxGoroutines)
p, err := pool.New(createConnection, pooledResource)
if err := nil {
// print err
}
for query := 0; query < maxGoroutines; query ++ {
go func(q int){
performQueries(q, p)
}(query)
}
wg.Wait()
// close
p.Close()
}
func performQueries(query int, p *Pool) {
conn, err := p.Acquire()
if err != nil {
// print err
return
}
defer p.Release(conn)
// use conn
// finish
}
复制代码
-
work
work用于展示如何使用无缓冲的通道来创建一个goroutine池,这些goroutine执行并控制一组工作,让其并发执行。
pagekage work
import "sync"
// Worker 必须满足接口类型,才是使用工作池
type Worker interface {
Task()
}
// Pool 提示一个goroutine池,这个池可以完成任何已提交的Worker任务
type Pool struct {
work chan Worker
wg sync.WaitGroup
}
// New 创建一个新工作池
func New(maxGoroutines int) *Pool {
p := Pool {
work: make(chan Worker),
}
p.wg.Add(maxGoroutines)
for i := 0; i < maxGoroutines; i++ {
go func(){
for w:= range p.work {
w.Task()
}
p.wg.Done()
}()
}
return &p
}
// Run 提交工作到工作池
func (p *Pool) Run(w Worker) {
p.work <- w
}
func (p *Pool)Shutdown() {
close(p.work)
p.wg.Wait()
}
复制代码
package main
import (
...
)
var names = []string {
"steve"
"bob"
"mary"
"therese"
"jason"
}
// namePrinter 使用特定的方式打印名字
type namePrinter struct {
name string
}
// 实现Worker接口
func (m *namePrinter) Task() {
// doing
}
func main() {
p := work.New(2)
var wg sync.WaitGroup
wg.Add(100 * len(names))
for i := 0; i< 100; i++ {
for _, name := range names {
np := namePrinter { name }
go func() {
// 将任务提交执行,当Run返回时,我们就知道任务已经处理完成
p.Run(&np)
wg.Donw()
}()
}
}
wg.Wait()
// 让工作池停止工作,等待现有的工作完成
p.Shutdown()
}
复制代码
标准库
-
log
stdin 标准输入
stdout 标准输出
stderr 标准错误
在Linux下,当一个用户进程被创建的时候,系统会自动为该进程创建三个数据流。一个程序要运行,需要有输入、输出,如果错误,还有表现自身错误。这就要从某个地方读入数据、将数据输出到某个地方,这就构成了数据流。对于这三个数据流来说,默认是表现在用户终端上的。标准输入、输出和错误可以重新定位到文件中。
复制代码
pagekage main
impoert (
"io"
"io/ioutil"
"log"
"os"
)
var (
Trace *log.Logger
Info *log.Logger
Warning *log.Logger
Error *log.Logger
)
func init() {
Trace = log.New(...)
Info = log.New(...)
Warning = log.New(...)
Error = log.New(...)
}
func main() {
Trace.Println(...)
Info.Println(...)
Warning.Println(...)
Error.Println(...)
}
复制代码
测试
-
单元测试
单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在给定场景下,有没有按照期望工作。Go语言中有几种方法写单元测试:基础测试、表组测试以及可以使用一些方法来模仿(mock)测试代码所需要的外部资源。
func TestXXX(t *testing.T)
// -v 表示提供冗余输出
go test -v
复制代码
-
基准测试
基准测试是一种测试代码性能的方法。
func BenchmarkXXX(t *testing.B)
go test -v -run="none" -bench="BenchmarkXXX" -benchtime="5s" -benchmem
复制代码