【Go】学习笔记

数据类型分类

值类型:基本数据类型是Go语言实际的原子,复合数据类型是由不同的方式组合基本类型构造出来的数据类型,如:数组,slice,map,结构体

整型    int8,uint               # 基础类型之数字类型
浮点型  float32,float64        # 基础类型之数字类型
复数                            # 基础类型之数字类型
布尔型  bool                    # 基础类型,只能存true/false,占据1个字节,不能转换为整型,0和1也不能转换为布尔
字符串  string                  # 基础类型
数组                            # 复合类型 
结构体  struct                  # 复合类型

内置基础类型

bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // utf8的别称
rune // alias for int32,represents a Unicode code point
float32 float64
complex64 complex128

引用类型:即保存的是对程序中一个变量的或状态的间接引用,对其修改将影响所有该引用的拷贝

指针    *
切片    slice
字典    map
函数    func
管道    chan
接口    interface

引用类型只有在程序运行期间才能够确定,所以 Go 语言编译器是无法在编译期对它们进行检查的,

贴士:Go语言没有字符型,可以使用byte来保存单个字母

零值机制

Go变量初始化会自带默认值,不像其他语言为空,下面列出各种数据类型对应的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  ""

格式化输出

常用格式化输出:

%%  %字面量
%b  二进制整数值,基数为2,或者是一个科学记数法表示的指数为2的浮点数
%c  该值对应的unicode字符
%d  十进制数值,基数为10
%e  科学记数法e表示的浮点或者复数
%E  科学记数法E表示的浮点或者附属
%f  标准计数法表示的浮点或者附属
%o  8进制度
%p  十六进制表示的一个地址值
%s  输出字符串或字节数组
%T  输出值的类型,注意int32和int是两种不同的类型,编译器不会自动转换,需要类型转换。
%v  值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%t  布尔类型
%q  该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x  表示为十六进制,使用a-f
%X  表示为十六进制,使用A-F
%U  表示为Unicode格式:U+1234,等价于"U+%04X"  

make、new操作

make用于内建类型(map、slice和channel)的内存分配。new用于各种类型的内存分配

new返回指针:new(T) 分配了零值填充的T类型的内存空间,并返回其地址,即一个*T类型的值。用Go的术语来说,它返回了一个指针,指向新分配的类型T的零值。

make(T,args)与new(T)有着不同的功能,make只能创建slice,map和channel,并返回一个有初始值(非零)的T类型,而不是*T。

本质来讲,导致这三个类型有所不同的原因是,指向数据结构的引用在使用前必须被初始化。例如:一个slice,是一个包含 指向数据(内部array)的指针、长度和容量的三项描述符,在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充了适当的值。

make返回初始化后的(非零)值。

method

假设有这么一个场景,你定义了一个struct叫做长方形,你现在想要计算它的面积,那么按照我们常规的思路应该会用下面的方式来实现。

type Rectangle struct {
    width, height float64
}

func area(r Rectangle) float64 {
    return r.width * r.height
}

func main() {
    area1 := area(Rectangle{12, 2})
    fmt.Println(area1)
}

虽然实现了计算矩形的面积,但是area()不是作为Rectangle的方法实现的。从概念来说,面积也是矩形的一个属性。

基于上述原因,就有了method的概念,method附属在一个给定的类型上,它的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。

method area()是依赖于某个形状(比如说Rectangle)来发生作用的。Rectangle.area()的发出者是Rectangle,area()是属于Rectangle的方法,而非一个外围函数。

更具体地说,Rectangle存在字段length和width,同时存在方法area(),这些字段和方法都属于Rectangle。

用RobPike的话来说就是:“A method is a function with an implicitfir stargument,called a receiver。”

method的语法如下:

func (r ReceiverType) funcName(parameters) (results)

用method实现刚才的例子

type Square struct {
    side float64
}

func (s Square) area() float64 {
    return s.side * s.side
}

type Circle struct {
    radius float64
}

func (c Circle) area() float64 {
    return c.radius * c.radius * math.Pi
}

method还需注意以下几点:

  • 虽然method名字一样,但是如果接收者不一样,那么method就不一样。
  • method里面可以访问接收者的字段。
  • 调用method通过访问,就像struct里面访问字段一样。

注: 上面的method方法都是以值传递,而非引用传递。Receiver还可以是指针。两者的差别在于,指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

method不仅可以作用在struct,还可以定义在任何你自定义的类型,内置类型,struct等各种数据类型上面。
(struct只是自定义类型中比较特殊的一种),自定义类型可以通过下面格式申明。

type typeName typeLiteral

如下:

type age int

type money fload32

type months map[string]int

m := months {
    "January":31,
    "February":28,
    ...
    "December":31
}

任何类型都是可以定义,更复杂点的method例子:

const (
    WHITE = iota
    BLACK
    BLUE
    RED
    YELLOW
)

type Color byte

type Box struct {
    width, height, depth float64
    color                Color
}

type BoxList []Box //a slice of boxes

func (b Box) Volume() float64 {
    return b.width * b.height * b.depth
}

func (b *Box) SetColor(c Color) {
    // (*b).color = c
    //当你用指针去访问相应字段时(虽然指针没有任何的字段)Go语言知道你要通过指针去获取这个值
    b.color = c
}

func (b1 BoxList) BiggestsColor() Color {
    v := 0.00
    c := Color(WHITE)
    for _, b := range b1 {
        if b.Volume() > v {
            v = b.Volume()
            c = b.color
        }
    }
    return c
}

func (b1 BoxList) PaintColor(c Color) {
    for i := range b1 {
        // (&b1[i]).SetColor(c)
        // Go语言知道receiver是指针,它自动帮你转了
        b1[i].SetColor(c)
    }
}

func (c Color) String() string {
    strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
    return strings[c]
}

func TestBox(t *testing.T) {
    boxes := BoxList{
        Box{4, 4, 4, RED},
        Box{10, 4, 4, BLACK},
        Box{2, 1, 1, BLUE},
        Box{12, 21, 2, WHITE},
        Box{3, 4, 5, YELLOW},
    }

    fmt.Printf("We have %d boxes in our set\n", len(boxes))
    fmt.Println("The color of the last one is ", boxes[len(boxes)-1].color.String())
    fmt.Println("The biggest one is ", boxes.BiggestsColor().String())
    fmt.Println("Let`s paint them in a certain color")
    boxes.PaintColor(YELLOW)
    fmt.Println("The color of the second one is", boxes[1].color.String())
    fmt.Println("Obviously,now,th biggest one is", boxes.BiggestsColor().String())
}

指针作为receiver

如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method。类似的,如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要*P去调用这个method。所以,不用担心你调用的是不见指针的method,Go语言知道你要做的一切。

method继承

Go语言的method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human
    school string
}

type Employee struct {
    Human
    company string
}

func (h *Human) SayHi() {
    fmt.Printf("Hi,I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "029-222-YYYY"}, "MIT"}
    sam := Student{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

method重写

上面例子,如果Employee想实现自己的SayHi,和匿名字段冲突一样,可以在Employee上定义一个method,重写匿名字段的方法。

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human
    school string
}

type Employee struct {
    Human
    company string
}

func (h *Human) SayHi() {
    fmt.Printf("Hi,I am %s you can call me on %s\n", h.name, h.phone)
}

func (e *Employee) sayHi() {
    fmt.Printf("Hi,I am %s,I work at %s. Call me on %s\n", e.name, e.company, e.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "029-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

Go语言的面向对象非常简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。

interface

什么是interface

简单地说,interface是一组method的组合,我们通过interface来定义对象的以组行为。

interface类型

interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

示例:

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human
    school string
    loan   float32
}

type Employee struct {
    Human
    company string
    money   float32
}

func (h Human) SayHi() {
    fmt.Printf("Hi,I am %s you can call me on %s\n", h.name, h.phone)
}

func (h Human) Sing(lyrics string) {
    fmt.Println("La la la la ...", lyrics)
}

func (e Employee) SayHi() {
    fmt.Printf("Hi,I am %s,I work at %s. Call me on %s\n", e.name, e.company, e.phone)
}

// Interface Men 被 Human,student 和Employee 实现
// 因为这三个类型都实现了这两个方法
type Men interface {
    SayHi()
    Sing(lyrics string)
}

func main() {
    mark := Student{Human{"Mark", 25, "029-222-YYYY"}, "MIT", 0.00}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc", 10.00}
    tom := Employee{Human{"Tom", 29, "111-888-XYZX"}, "Things Ltd", 100}

    //定义Men类型的变量i
    var i Men
    i = mark
    fmt.Println("This is mark ,a stduent:")
    i.SayHi()
    i.Sing("November rain")

    i = sam

    fmt.Println("This is mark ,an employee:")
    i.SayHi()
    i.Sing("Born to be wild")

    //定义 slice Men
    x := make([]Men, 3)
    x[0], x[1], x[2] = mark, sam, tom
    for _, v := range x {
        v.SayHi()
    }
}

通过上面代码,可以发现interface就是一组方法的集合,必须由其他非interface类型实现,而不能自我实现,Go语言通过interface实现了duck-typing:,即“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

空接口

空interface(interface{})不包含任何的method,因此所有的类型都实现了空interface,即空接口帮我们存储任意类型的数值。

简单示例:

// 定义 a 为空接口
var a interface{}
var i int = 5
s := "hello world"
a = i
a = s

一个函数把interface{}作为参数,那么它可以接受任意类型的值作为参数,如果一个函数返回interface{},就可以返回任意类型的值。

interface函数参数

interface的变量可以持有任意实现该interface类型的对象。可以通过定义interface参数,让函数接收各种类型的参数。

举个例子:fmt.Println是我们常用的一个函数,它可以接受任意类型的数据。打开fmt的源码文件,可以看到如下定义:

type Stringer interface {
    String() string
}

也就是说,任何实现了String方法的类型都能作为参数被ftm.Println调用。

package main
import (
    "fmt"
    "strconv"
)

type Human struct {
    name  string
    age   int
    phone string
}

//通过这个方法 Human 实现了 fmt.Stringer
func (h Human) String() string {
    return h.name + " - " + strconv.Itoa(h.age) + "years - ?" + h.phone
}

func main() {
    Bob := Human{"Bob", 29, "000-777-XXXX"}
    fmt.Println("This human is :", Bob)
}

即如果需要某个类型能被fmt包以特殊格式输出,就必须实现Stringer这个接口。如果没有实现这个接口,fmt将以默认的方式输出。

注:实现了error接口的对象(即实现了Error() string的对象),使用fmt输出时,会调用Error()方法,因此不必再定义String()方法。

interface变量存储的类型

interface的变量可以存任意类型的数值(该类型实现了interface)。那么怎么反向知道这个变量实际保存的是哪个类型的对象呢?目前有两种方法。

  • Comma-ok断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量:value,ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

package main
import (
    "fmt"
    "strconv"
)

type Element interface{}
type List []Element

type Person struct {
    name string
    age  int
}

func (p Person) String() string {
    return "name:" + p.name + " - age:" + strconv.Itoa(p.age)
}

func main() {
    list := make(List, 4)
    list[0] = 1       //an int
    list[1] = "Hello" //a string
    list[2] = Person{"Dennis", 65}
    list[3] = []int{1, 2, 3}

    for i, e := range list {
        // if v, ok := e.(int); ok {
        //  fmt.Printf("list[%d] is an int and its value is %d\n", i, v)
        // } else if v, ok := e.(string); ok {
        //  fmt.Printf("list[%d] is a string and its value is %s\n", i, v)
        // } else if v, ok := e.(Person); ok {
        //  fmt.Printf("list[%d] is a Person and its value is %s\n", i, v)
        // } else {
        //  fmt.Printf("list[%d] is a different type\n", i)
        // }
        switch v := e.(type) {
        case int:
            fmt.Printf("list[%d] is an int and its value is %d\n", i, v)
        case string:
            fmt.Printf("list[%d] is a string and its value is %s\n", i, v)
        case Person:
            fmt.Printf("list[%d] is a Person and its value is %s\n", i, v)
        default:
            fmt.Printf("list[%d] is a different type\n", i)
        }
    }
}

这里需要强调的是:element.(type) 这个语法不能在switch以外的任何逻辑里面使用,在switch外面判断一个类型就使用comma-ok。

嵌入interface

就像Struct的匿名字段一样,相同的逻辑也可以用到interface。如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

源码包container/heap里面就有这样的定义。

type Interface {
    sort.Interface //嵌入字段sort.Interface
    Push(x interface{})
    Pop() interface{}
}

sort.Interface就是嵌入字段,把sort.Interface的所有method隐式包含进来。也就是下面三个方法。

type Interface interface {
    Len() int
    Less(i,j int) bool
    Swap(i,j int)
}

另一个例子就是io包下的io.ReadWrite,它包含了io包下面的Reader和Writer两个interface。

type ReadWrite interface {
    Reader
    Writer
}

反射

反射是指在程序运行期对程序本身进行访问和修改的能力,即可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind),如果是结构体变量,还可以获取到结构体本身的信息(字段与方法),通过反射,还可以修改变量的值,可以调用关联的方法。一般用reflect包。

官方文档详细说明

使用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.14
v := reflect.ValueOf(x)
v.SetFloat(6.2)

如果要修改相应的值,必须写成下面这样:

var x float64 = 3.14
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(6.2)

注意:

  • 在编译期间,无法对反射代码进行一些错误提示。
  • 反射影响性能

类型与种类的区别:

  • Type是原生数据类型: int、string、bool、float32 ,以及 type 定义的类型,对应的反射获取方法是 reflect.Type 中 的 Name()
  • Kind是对象归属的品种:Int、Bool、Float32、Chan、String、Struct、Ptr(指针)、Map、Interface、Fune、Array、Slice、Unsafe Pointer等

并发

goroutine

func say(s string) {
    for i := 0; i < 5; i++ {
        runtime.Gosched()
        fmt.Println(s)
    }
}

func main() {
    go say("world") // 开启一个新的 Groutine 执行
    say("hello")    // 当前 Groutine 执行
}

上面多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。

runtime.Gosched()表示让CPU把时间片让给别人,下次某个时候继续恢复执行该goroutine。

默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显示调用runtime.GOMAXPROCS(n)告诉调度器同时使用多个线程。GOMAXPROCS设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n<1,不会改变当前设置。以后Go语言的新版本中调度得到改进后,这将被移除。

共享内存并发机制

Go程序可以使用通道进行多个goroutine间的数据交换,但是这仅仅是数据同步中的一种方法。Go语言与其他语言如C、Java一样,也提供了同步机制,在某些轻量级的场合,原子访问(sync/atomic包),互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。

贴士:利用通道优雅的实现了并发通信,但是其内部的实现依然使用了各种锁,因此优雅代码的代价是性能的损失。

func TestCounterWaitGroup(t *testing.T) {
    var mut sync.Mutex
    counter := 0
    var wg sync.WaitGroup
    for i := 0; i < 5000; i++ {
        wg.Add(1) // 每启一个协程WaitGroup就+1
        go func() {
            defer func() { // 加锁的时候一般都要配合 defer 来释放锁
                mut.Unlock()
            }()
            mut.Lock()
            counter++
            wg.Done() // 完成协程任务,主动告诉WaitGroup结束
        }()
    }
    wg.Wait()                       // 等每个WaitGroup都 done 才不会继续阻塞
    t.Logf("counter = %d", counter) // counter = 5000
}

CSP并发机制 - channel

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine之间如何进行数据的通信?Go语言提供了一个很好的通信机制channel。channel可以与Unixshell中的双向管道做类比,通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make创建channel。

ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})

channel 通过操作符 <-来接收和发送数据

ch <- v   // 发送v到channel ch
v := <-ch // 从ch中接收数据,并赋值给v

示例代码:

func sum(a []int, c chan int) {
    sum := 0
    for _, v := range a {
        sum += v
    }
    c <- sum
}

func main() {
    a := []int{7, 2, 8, -9, 4, 0}
    c := make(chan int)

    go sum(a[:len(a)/2], c) // groutine1
    go sum(a[len(a)/2:], c) // groutine2

    // 先取groutine2的赋值给x,groutine1的赋值给y
    x, y := <-c, <-c
    fmt.Println(x, y, x+y) //-5 17 12
}

通讯的两方必须同时都在channel上才能完成这次交互,任何一方不在的时候,另一方被阻塞等待,直到另一方准备好。

select

多渠道的选择

select {
case ret := <-retCh1:
    t.Logf("result %s", ret)
case ret := <-retCh2:
    t.Logf("result %s", ret)
default:
    t.Error("No noe returned")
}

当运行到select的section时,只要case后面任何一个等待的channel阻塞事件处于非阻塞状态(即channel里有消息),那么就会执行该channel对应的case所定义的部分。

  • 各个case无先后顺序,并不是retCh1在前就先执行retCh1的;
  • 各个case都是阻塞的;
  • 如果没有default,且没有符合的case那么就会阻塞在select的section;

超时控制

select {
case ret := <-retCh:
    t.Logf("result %s", ret)
case <-time.After(time.Second * 1)
    t.Error("time out")
}

channel的关闭和广播

ch <- value
close(ch)
  • 向关闭的channel发送数据,会导致panic
  • v,ok <-ch;ok 为 bool 值,true表示正常接受,false表示通道关闭;如果channel关闭了,没有ok那么返回的将是channel的零值
  • 所有的channel接收者都会在channel关闭时,立刻从阻塞等待状态返回(唤醒)且上述ok值为false。这个广播机制常被利用,进行向多个订阅者同时发送信号。 如:退出信号。
func dataProducer(ch chan int, wg *sync.WaitGroup) {
    go func() {
        for i := 0; i < 10; i++ {
            ch <- i
        }
        close(ch)
        // ch <- 10  //panic: send on closed channel
        wg.Done()
    }()
}

func dataReceiver(ch chan int, wg *sync.WaitGroup) {
    go func() {
        for {
            if data, ok := <-ch; ok {
                fmt.Println(data)
            } else {
                break
            }
        }
        wg.Done()
    }()
}

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)
    wg.Add(1)
    dataProducer(ch, &wg)
    wg.Add(1)
    dataReceiver(ch, &wg)
    wg.Add(1)
    dataReceiver(ch, &wg)
}

取消任务

func isCancelled(cancelChan chan struct{}) bool {
    select {
    case <-cancelChan:
        return true
    default:
        return false
    }
}

func cancel(cancelChan chan struct{}) {
    close(cancelChan)
}

func main() {
    cancelChan := make(chan struct{}, 0)
    for i := 0; i < 5; i++ {
        go func(i int, cancelChan chan struct{}) {
            for {
                if isCancelled(cancelChan) {
                    break
                }
                time.Sleep(time.Millisecond * 500)
            }
            fmt.Println(i, "Cancelled")
        }(i, cancelChan)
    }
    cancel(cancelChan)
    time.Sleep(time.Second * 1)
}

Context

关联任务的取消(树状关系的任务)

  • 根Context:通过context.Background()创建
  • 子Context:context.WithCancel(parentContext)创建
    • ctx,cancel:=context.WithCancel(context.Background())
  • 当前Context被取消时,基于他的子context都会被取消
  • 接收取消通知<-ctx.Done()
func isCanceled(ctx context.Context) bool {
    select {
    case <-ctx.Done():
        return true
    default:
        return false
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(i int, ctx context.Context) {
            for {
                if isCanceled(ctx) {
                    break
                }
                time.Sleep(time.Millisecond * 50)
            }
            fmt.Println(i, "canceled")
            wg.Done()
        }(i, ctx)
    }
    cancel()
    wg.Wait()
}

once

仅运行一次

type SingletonObj struct {
}

var once sync.Once
var obj *SingletonObj

func GetSingletonObj() *SingletonObj {
    once.Do(func() {
        fmt.Println("Create obj.")
        // obj = &SingletonObj{}
        obj = new(SingletonObj)
    })
    return obj
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            obj := GetSingletonObj()
            fmt.Printf("%x\n", unsafe.Pointer(obj))
            wg.Done()
        }()
    }
    wg.Wait()
}

任意任务完成

func FirstResponse() string {
    numOfRunner := 10
    // 用buffer channel 防止协程一直被阻塞
    ch := make(chan string, numOfRunner)
    for i := 0; i < numOfRunner; i++ {
        go func(i int) {
            ret := runTask(i)
            // channel 的机制:往里面放消息后,在接收这个channel的接收者就会从阻塞中唤醒
            ch <- ret
        }(i)
    }
    return <-ch // 这里return 就是尝试从channel中获取数据,一旦有数据就立即return
}

所有任务完成

当然可以用 sync.WaitGroup包来等待所有任务完成,现在用csp的channel来实现

func AllResponse() string {
    numOfRunner := 10
    ch := make(chan string, numOfRunner)
    for i := 0; i < numOfRunner; i++ {
        go func(i int) {
            ret := runTask(i)
            ch <- ret
        }(i)
    }
    finalRet := ""
    for j := 0; j < numOfRunner; j++ {
        finalRet += <-ch + "\n"
    }
    return finalRet
}

对象池

利用Buffered Channels 实现对象池

type Reusableobj struct {
}

type ObjPool struct {
    bufChan chan *Reusableobj //用于缓冲可重用对象
}

func NewObjPool(numOfObj int) *ObjPool {
    objPool := ObjPool{}
    objPool.bufChan = make(chan *Reusableobj, numOfObj)
    for i := 0; i < numOfObj; i++ {
        objPool.bufChan <- &Reusableobj{}
        // objPool.bufChan <- new(Reusableobj)
    }
    return &objPool
}

func (p *ObjPool) GetObj(timeout time.Duration) (*Reusableobj, error) {
    select {
    case ret := <-p.bufChan:
        return ret, nil
    case <-time.After(timeout): // 超时控制
        return nil, errors.New("timeout")
    }
}

func (p *ObjPool) ReleaseObj(obj *Reusableobj) error {
    select {
    case p.bufChan <- obj:
        return nil
    default:
        return errors.New("overflow") // 满了放不进去也会阻塞
    }
}

对象池调用示例

sync.Pool 对象缓存

每个 Processor 对于sync.Pool都分为两个部分:私有对象(协程安全)、共享池(协程不安全)

注:

  1. 私有对象只能缓存一个对象,因此它不是池;
  2. 共享池获取都需要锁;

sync.Pool 对象获取逻辑

  • 尝试从私有对象获取
  • 私有对象不存在,尝试从当前 Processor 的共享池获取
  • 如果当前 Processor 共享池是空的,那么就尝试去其他 Processor 的共享池获取
  • 如果所有子池都是空的,最后就用用户指定的 New 函数产生一个新的对象返回

sync.Pool 对象放回逻辑

  • 如果私有对象不存在则保存为私有对象
  • 如果私有对象存在,放入当前 Processor 子池的共享池中

使用 sync.Pool

pool := &sync.Pool{
    New: func() interface{} {
        return 0
    },
}

array := pool.Get().(int)
...
pool.Put(10)

sync.Pool实例代码

sync.Pool对象的生命周期

  • GC 会清除 sync.Pool 缓存的对象
  • 对象的缓存有效期为下一次 GC 之前

GC 是系统调度的,通常是无法干预的,无法知道对象的生命周期。那也也无法控制一个连接的生命周期。

sync.Pool 总结

  • 适用于通过复用,降低复杂对象的创建和GC代价
  • 协程安全,会有锁的开销
  • 生命周期受 GC 影响,不适合于做连接池等,需自己管理生命周期的资源的池化

注: sync.Pool会有锁的开销,因此就要考量:锁带来的开销大还是创建复杂对象带来的开销大,这将决定使用 sync.Pool 是否可以优化你的程序。

单元测试

文件名以xxx_test.go结尾,方法以TestXxx(t *testing.T){} 开头

func TestX(t *testing.T) {
    //要测试的代码片段
}

内置单元测试框架

  • Fail,Error: 该测试失败,该测试继续,其他测试继续执行
  • FailNow,Fatal: 该测试失败,该测试中止,其他测试继续执行
  • 命令行(代码覆盖率) ch24\unit_test> go test -v -cover
  • 断言 安装:ch24\unit_test> go get -u github.com/stretchr/testify/assert

可能遇到的问题

【Golang】解决Go test执行单个测试文件提示未定义问题

根本原因

其实从看看上面的这段提示:build failed,构建失败,我们应该就能看出一下信息。go test与其他的指定源码文件进行编译或运行的命令程序一样(参考:go run和go build),会为指定的源码文件生成一个虚拟代码包——“command-line-arguments”,对于运行这次测试的命令程序来说,测试源码文件getinfo_test.go是属于代码包“command-line-arguments”的,可是它引用了其他包中的数据并不属于代码包“command-line-arguments”,编译不通过,错误自然发生了。

如果有多个文件,在指定目录直接运行 go test -v -cover 不用指定文件即可

Benchmark

func BenchmarkConcatStringByAdd(b *testing.B){
    //与性能测试无关的代码
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        //测试代码
    }
    b.StopTimer()
    //与性能测试无关的代码
}
package benchmark

import (
    "bytes"
    "testing"
)

func BenchmarkConcatStringByAdd(b *testing.B) {
        elems := []string{"h", "e", "l", "l", "o"}
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                ret := ""
                for _, elem := range elems {
                        ret += elem
                }
        }
        b.StopTimer()
}

func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
        elems := []string{"h", "e", "l", "l", "o"}
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                var buf bytes.Buffer
                for _, elem := range elems {
                        buf.WriteString(elem)
                }
        }
        b.StopTimer()
}

在命令行 [root@VM_0_14_centos go]# go test ./benchmark_test.go -bench=.

go test -bench=. -benchmem

-bench= <相关benchmark测试>

注: Windows 下使用go test 命令行时,-bench=.应写为-bench="."

PS C:\learn\go_learn\src\ch24\benchmark> go test .\concat_string_test.go -bench="."
goos: windows
goarch: amd64
BenchmarkConcatStringByAdd-6             9184879               127 ns/op
BenchmarkConcatStringByBytesBuffer-6    18808599                63.6 ns/op
PASS
ok      command-line-arguments  3.260s
PS C:\learn\go_learn\src\ch24\benchmark> go test .\concat_string_test.go -bench="." -benchmem
goos: windows
goarch: amd64
BenchmarkConcatStringByAdd-6             9327139               126 ns/op              16 B/op          4 allocs/op
BenchmarkConcatStringByBytesBuffer-6    18653004                62.5 ns/op            64 B/op          1 allocs/op
PASS
ok      command-line-arguments  3.238s

BDD in Go

项目网站: https://github.com/smartystreets/goconvey

安装: go get -u github.com/smartystreets/goconvey/convey

package bdd

import (
    "testing"

    . "github.com/smartystreets/goconvey/convey"
)

func TestSpec(t *testing.T) {
    Convey("Given 2 even numbers", t, func() {
        a := 6
        b := 4

        Convey("When add the two numbers", func() {
            c := a + b
            Convey("Then the result is still even", func() {
                So(c%2, ShouldEqual, 0)
            })
        })
    })
}

反射编程

一 反射简介

反射是指在程序运行期对程序本身进行访问和修改的能力,即可以在运行时动态获取变量的各种信息,比如变量的类型(type),类别(kind),如果是结构体变量,还可以获取到结构体本身的信息(字段与方法),通过反射,还可以修改变量的值,可以调用关联的方法。

反射常用在框架的开发上,一些常见的案例,如JSON序列化时候tag标签的产生,适配器函数的制作等,都需要用到反射。反射的两个使用常见使用场景:

  • 不知道函数的参数类型:没有约定好参数、传入类型很多,此时类型不能统一表示,需要反射
  • 不知道调用哪个函数:比如根据用户的输入来决定调用特定函数,此时需要依据函数、函数参数进行反射,在运行期间动态执行函数

Go程序的反射系统无法获取到一个可执行文件空间中或者是一个包中的所有类型信息,需要配合使用标准库中对应的词法、语法解析器和抽象语法树( AST) 对源码进行扫描后获得这些信息。

贴士:

  • C,C++没有支持反射功能,只能通过 typeid 提供非常弱化的程序运行时类型信息。
  • Java、 C#等语言都支持完整的反射功能。
  • Lua、JavaScript类动态语言,由于其本身的语法特性就可以让代码在运行期访问程序自身的值和类型信息,因此不需要反射系统。

注意:

  • 在编译期间,无法对反射代码进行一些错误提示。
  • 反射影响性能

二 反射是如何实现的

反射是通过接口的类型信息实现的,即反射建立在类型的基础上:当向接口变量赋予一个实体类型的时候,接口会存储实体的类型信息。

Go中反射相关的包是reflect,在该包中,定义了各种类型,实现了反射的各种函数,通过它们可以在运行时检测类型的信息、改变类型的值。

变量包括type、value两个部分(所以 nil != nil ),type包括两部分:

  • static type:在开发时使用的类型,如int、string
  • concrete type:是runtime系统使用的类型

类型能够断言成功,取决于 concrete type ,如果一个reader变量,如果 concrete type 实现了 write 方法,那么它可以被类型断言为writer。

Go中,反射与interface类型相关,其type是 concrete type,只有interface才有反射!每个interface变量都有一个对应的pair,pair中记录了变量的实际值和类型(value, type)。即一个接口类型变量包含2个指针,一个指向对应的 concrete type ,另一个指向实际的值 value。

示例:

var r io.Reader             // 定义了一个接口类型
r, err := os.OpenFile()     // 记录接口的实际类型、实际值

var w io.Writer             // 定义一个接口类型
w = r.(io.Writer)           // 赋值时,接口内部的pair不变,所以 w 和 r 是同一类型

三 Go中反射初识

3.1 reflect包的两个函数

reflect 提供了2个重要函数:

  • ValueOf():获取变量的值,即pair中的 value
  • TypeOf():获取变量的类型,即pair中的 concrete type
    type Person struct {
        Name string
        Age int
    }
    p := Person{ "lisi", 13}

    fmt.Println(reflect.ValueOf(p))         // {lisi 13}  变量的值
    fmt.Println(reflect.ValueOf(p).Type())  // main.Person 变量类型的对象名

    fmt.Println(reflect.TypeOf(p))          //  main.Person 变量类型的对象名

    fmt.Println(reflect.TypeOf(p).Name())   // Person:变量类型对象的类型名
    fmt.Println(reflect.TypeOf(p).Kind())   // struct:变量类型对象的种类名

    fmt.Println(reflect.TypeOf(p).Name() == "Person") // true
    fmt.Println(reflect.TypeOf(p).Kind() == reflect.Struct) //true

类型与种类的区别:

  • Type是原生数据类型: int、string、bool、float32 ,以及 type 定义的类型,对应的反射获取方法是 reflect.Type 中 的 Name()
  • Kind(Go内置的枚举常量)是对象归属的品种:Int、Bool、Float32、Chan、String、Struct、Ptr(指针)、Map、Interface、Fune、Array、Slice、Unsafe Pointer等

四 利用反射编写灵活的代码

按名字访问结构的成员

reflect.ValueOf(*e).FieldByName("Name") //获取Name字段的值

按名字访问结构的方法

reflect.ValueOf(*e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(1)})
type Employee struct {
    EmployeeID string
    Name       string `json:"name"` //Struct Tag
    Age        int    `json:"age"`  //Struct Tag
}

func (e *Employee) UpdateAge(newVal int) {
    e.Age = newVal  
}

func TestInvokeByName(t *testing.T) {
    e := &Employee{"1", "Mike", 30}
    //按名字获取成员
    t.Logf("Name:value(%[1]v),Type(%[1]T) ", reflect.ValueOf(*e).FieldByName("Name"))

    if nameField, ok := reflect.TypeOf(*e).FieldByName("Name"); !ok {
        t.Error("Failed to get 'Name' field.")
    } else {
        t.Log("Tag:json", nameField.Tag.Get("json"))
    }

    reflect.ValueOf(e).MethodByName("UpdateAge").Call([]reflect.Value{reflect.ValueOf(10)})
    t.Log("Update Age:", e)
}

注意reflect.Type 和 reflect.Value 都有FieldByName 方法,注意他们的区别

DeepEqual 比较切片和map

type Customer struct {
    CookieID string
    Name     string
    Age      int
}

func main() {
    a := map[int]string{1: "one", 2: "two", 3: "three"}
    b := map[int]string{1: "one", 2: "two", 4: "four"}

    // fmt.Println(a == b) // map can only be compared to nil
    fmt.Println(reflect.DeepEqual(a, b)) // 输出 false

    s1 := []int{1, 2, 3}
    s2 := []int{1, 2, 3}
    s3 := []int{2, 3, 1}
    // fmt.Println(s1 == s2) //slice can only be compared to nil
    fmt.Printf("s1 == s2 ? %t\n", reflect.DeepEqual(s1, s2)) // 输出 s1 == s2 ? true
    fmt.Printf("s2 == s3 ? %t\n", reflect.DeepEqual(s2, s3)) // 输出 s2 == s3 ? false

    c1 := Customer{"1", "Mike", 40}
    c2 := Customer{"1", "Mike", 40}
    fmt.Println(reflect.DeepEqual(c1, c2)) // 输出 true
}

万能程序

type Employee struct {
    EmployeeID string
    Name       string `json:"name"` //Struct Tag
    Age        int    `json:"age"`  //Struct Tag
}

func (e *Employee) UpdateAge(newVal int) {
    e.Age = newVal
}

type Customer struct {
    CookieID string
    Name     string
    Age      int
}

func fillBySettings(st interface{}, settings map[string]interface{}) error {
    if reflect.TypeOf(st).Kind() != reflect.Ptr {
        if reflect.TypeOf(st).Elem().Kind() != reflect.Struct {
            return errors.New("the first param should be a pointer to the struct type.")
        }
    }
    if settings == nil {
        return errors.New("the setting should not be nil.")
    }
    var (
        field reflect.StructField
        ok    bool
    )
    for k, v := range settings {
        if field, ok = (reflect.ValueOf(st)).Elem().Type().FieldByName(k); !ok {
            continue
        }
        if field.Type == reflect.TypeOf(v) {
            vstr := reflect.ValueOf(st)
            vstr = vstr.Elem()
            vstr.FieldByName(k).Set(reflect.ValueOf(v))
        }
    }
    return nil
}

func TestFillNameAndAge(t *testing.T) {
    settings := map[string]interface{}{"Name": "Mike", "Age": 20}
    e := Employee{}
    if err := fillBySettings(&e, settings); err != nil {
        t.Fatal(err)
    }
    t.Log(e)

    c := new(Customer)
    if err := fillBySettings(c, settings); err != nil {
        t.Fatal(err)
    }
    t.Log(*c)
}

unsafe 库

深度解密Go语言之unsafe

相比于 C 语言中指针的灵活,Go 的指针多了一些限制。但这也算是 Go 的成功之处:既可以享受指针带来的便利,又避免了指针的危险性。

  • 限制一:Go的指针不能进行数学运算。
  • 限制二:不同类型的指针不能相互转换。
  • 限制三:不同类型的指针不能使用==或!=比较。
  • 限制四:不同类型的指针变量不能相互赋值。

前面所说的指针是类型安全的,但它有很多限制。Go 还有非类型安全的指针,这就是 unsafe 包提供的 unsafe.Pointer。在某些情况下(和外部的C程序实现的高效库交互的时候可能会用到unsafe),它会使代码更高效,当然,也更危险。Go 源码中也是大量使用 unsafe 包。

“不安全”行为的危险性

Go是不支持不同类型指针类型的强制转换的,但是通过

type MyInt int

func TestConvert(t *testing.T) {
    i := 10
    f := *(*float64)(unsafe.Pointer(&i))
    fmt.Println(unsafe.Pointer(&i)) // 0xc0000122f8
    fmt.Println(f)                  // 5e-323

    i2 := *(*int)(unsafe.Pointer(&i))
    fmt.Println(i2) // 10

    a := []int{1, 2, 3, 4}
    b := *(*[]MyInt)(unsafe.Pointer(&a))
    fmt.Println(b) // [1 2 3 4]
}

atomic 原子操作

一 原子操作理解

通过对互斥锁的合理使用,我们可以使一个 goroutine 在执行临界区中的代码时,不被其他的
goroutine 打扰。不过,虽然不会被打扰,但是它仍然可能会被中断(interruption)。

我们已经知道,对于一个 Go 程序来说,Go 语言运行时系统中的调度器,会恰当地安排其中所
有的 goroutine 的运行。不过,在同一时刻,只可能有少数的 goroutine 真正地处于运行状态,
并且这个数量是固定的。

所以,为了公平起见,调度器总是会频繁地换上或换下这些 goroutine。换上的意思是,让一个
goroutine 由非运行状态转为运行状态,并促使其中的代码在某个 CPU 核心上执行。

这个中断的时机有很多,任何两条语句执行的间隙,甚至在某条语句执行的过程中都是可以的。
即使这些语句在临界区之内也是如此。所以,我们说,互斥锁虽然可以保证临界区中代码的串行
执行,但却不能保证这些代码执行的原子性(atomicity)。

在众多的同步工具中,真正能够保证原子性执行的只有原子操作(atomic operation)。原子操
作在进行的过程中是不允许中断的。在底层,这会由 CPU 提供芯片级别的支持,所以绝对有
效。即使在拥有多 CPU 核心,或者多 CPU 的计算机系统中,原子操作的保证也是不可撼动
的。

这使得原子操作可以完全地消除竞态条件,并能够绝对地保证并发安全性。并且,它的执行速度
要比其他的同步工具快得多,通常会高出好几个数量级。不过,它的缺点也很明显。

更具体地说,正是因为原子操作不能被中断,所以它需要足够简单,并且要求快速。 你可以想
象一下,如果原子操作迟迟不能完成,而它又不会被中断,那么将会给计算机执行指令的效率带
来多么大的影响。因此,操作系统层面只对针对二进制位或整数的原子操作提供了支持。

Go 语言的原子操作当然是基于 CPU 和操作系统的,所以它也只针对少数数据类型的值提供了
原子操作函数。这些函数都存在于标准库代码包sync/atomic中。

我一般会通过下面这道题初探一下应聘者对sync/atomic包的熟悉程度。我们今天的问题是:sync/atomic包中提供了几种原子操作?可操作的数据类型又有哪些?

这里的典型回答是:

sync/atomic包中的函数可以做的原子操作有:加法(add)、比较并交换(compare and
swap,简称 CAS)、加载(load)、存储(store)和交换(swap)。

这些函数针对的数据类型并不多。但是,对这些类型中的每一个,sync/atomic包都会有一套
函数给予支持。这些数据类型有:int32、int64、uint32、uint64、uintptr,以及
unsafe包中的Pointer。不过,针对unsafe.Pointer类型,该包并未提供进行原子加法操作
的函数。

此外,sync/atomic包还提供了一个名为Value的类型,它可以被用来存储任意类型的值。

二 常用原子操作

2.1 原子运算:增/减

增加函数的函数名前缀都是Add开头

    // 原子性的把一个int32类型变量 i32 增大3 ,下列函数返回值必定是已经被修改的值
    newi32 := atomic.AddInt32(&i32, 3)      // 传入指针类型因为该函数需要获得数据的内存位置,以施加特殊的CPU指令

常见的增/减原子操作函数:

  • atomic.AddInt32
  • atomic.AddInt64
  • atomic.AddUint32
  • atomic.AddUint64
  • atomic.AddUintptr

注意:

  • 如果需要执行减少操作,可以这样书写 atomic.AddInt32(&i32, -3)
  • 对uint32执行增加NN(代表负整数,增加NN也可以理解为减少-NN):atomic.AddUint32(&ui32, ^uint32(-NN-1)) # 原理:补码
  • 对uint64以此类推
  • 不存在atomic.AddPointer的函数,因为unsafe.Poniter类型的值无法被增减
2.2 原子运算:比较与替换

比较并替换即“Compare And Swap”,简称CAS。该类原子操作名称都以CompareAndSwap为前缀。

举例:

    // 参数一:被操作数 参数二和参数三代表被操作数的旧值和新值
    func CompareAndSwapInt32(addr *int32, old, new int32)(swap bool)

CAS的一些特点:

  • CAS与锁相比,明显不同是它总是假设操作值未被改变,一旦确认这个假设为真,立即进行替换。所以锁的做法趋于悲观,CAS的做法趋于乐观。
  • CAS的优势:可以在不创建互斥量和不形成临界区的情况下,完成并发安全的值替换操作,可以大大减少性能损耗。
  • CAS的劣势:在被操作之被频繁变更的情况下,CAS操作容易失败,有时候需要for循环判断返回结构的bool来进行多次尝试
  • CAS操作不会阻塞协程,但是仍可能使流程执行暂时停滞(这种停滞极短)

应用场景:并发安全的更新一些类型的值,可以优先选择CAS操作。

2.3 原子读取:载入

为了原子的读取数值,Go提供了一系列载入函数,名称以Load为前缀。

CAS与载入的配合示例:

// value 增加 num
func addValue(value,num int32) {
    for {
        v := atomic.LoadInt32(&value)
        if atomic.ComapreAndSwapInt32(&value, v, (v + num)) {
            break
        }
    }
}
2.4 原子写入:存储

在原子存储时,任何CPU都不会进行针对同一个值的读写操作,此时不会出现并发时候,别人读取到了修改了一半的值。

Go的sync/atomic包提供的存储函数都是以Store为前缀。

示例:

// 参数一位被操作数据的指针 参数二是要存储的新值
atomic.StoreInt32(i *int3, v int32)     

Go原子存储的特点:存储操作总会成功,因为不关心被操作值的旧值是什么,这与CAS有明显区别。

2.5 交换

交换与CAS操作相似,但是交换不关心被操作数据的旧值,而是直接设置新值,不过会返回被操作值的旧值。交换操作比CAS操作的约束更少,且比载入操作功能更强。
在Go中,交换操作都以Swap为前缀,示例:

// 参数一是被操作值指针  参数二是新值  返回值为旧值
atomic.SwapInt32(i *int32, v int32)         

指针的原子操作

并发读写一个共享缓存的时候,为了读写的线程安全

在写的时候,写到另外一块地方,完全写完之后,用一个原子操作把我们读的那快地方和写的地方,重新指向一下。Buffer指向新写好的地方,这样再读的时候就会读新写好的内容,所以切换的时候要一个线程安全的特性,这时候可以用atomic 来做。

示例代码:

func TestAtomic(t *testing.T) {
    var shareBufPtr unsafe.Pointer
    writeDataFn := func() {
        data := []int{}
        for i := 0; i < 100; i++ {
            data = append(data, i)
        }
        atomic.StorePointer(&shareBufPtr, unsafe.Pointer(&data))
    }
    readDataFn := func() {
        data := atomic.LoadPointer(&shareBufPtr)
        fmt.Println(data, *(*[]int)(data))
    }

    var wg sync.WaitGroup
    writeDataFn()
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            for i := 0; i < 10; i++ {
                writeDataFn()
                time.Sleep(time.Microsecond * 100)
            }
            wg.Done()
        }()
        wg.Add(1)
        go func() {
            for i := 0; i < 10; i++ {
                readDataFn()
                time.Sleep(time.Microsecond * 100)
            }
            wg.Done()
        }()
    }

}

三 原子值

Go还提供了sync/atomic.Value类型的结构体,用于存储需要原子读写的值。该类型与第二章中的原子操作最大的区别是,可接受的值类型不限。

示例:

    var atomicV atomic.Value

该结构体包含的方法:

  • Load:原子的读取原子值实例的值,返回interface{}类型结果
  • Store:原子的向原子值实例中存储值,接受一个interface{}类型参数(不能是nil),无返回结果

注意:

  • 如果一个原子值没有通过Store方法存储值,那么其Load方法总是返回nil
  • 原子值实例一旦存储了一个类型的值,后续再次Store存储时,存储的值必须也是原有的类型

尤其注意:atomic.Value变量(指针类型变量除外)声明后,值不应该被赋值到别处,比如赋值给别的变量,作为参数值传入函数,作为结果值从函数返回等等,这样会有安全隐患。 因为结构体值的复制不但会生产该值的副本,还会生成其中字段的副本,会造成并发安全保护失效。

Go常见架构模式的实现

Go常见架构模式的实现

Pipe-Filter 架构

  • 非常适合与数据处理及数据分析系统
  • Filter 封装数据处理的功能
  • 松耦合:Filter 只跟数据(格式)耦合
  • Pipe 用于连接 Filter 传递数据或者在异步处理过程中缓冲数据流进程内同步调用时,pipe演变为数据在方法调用间传递

示例:

输入 字符串"1,2,3",经过SplitFilter => ["1","2","3"] 经过ToIntFilter => [1,2,3] => 经过SumFilter => 6

micro-kernel(微内核) 架构

image

  • 特点:易于扩展、错误隔离、保持架构一致性
  • 要点:内核包含公共流程或通用逻辑、将可变或可扩展部分规划为扩展点、抽象扩展点行为,定义接口、利用插件进行扩展

示例:

image

简单示例代码:

agent.go

package microkernel

import (
    "context"
    "errors"
    "fmt"
    "strings"
    "sync"
)

const (
    Waiting = iota
    Running
)

var WrongStateError = errors.New("can not take the operation in the current state")

type CollectorsError struct {
    CollectorErrors []error
}

func (ce CollectorsError) Error() string {
    var strs []string
    for _, err := range ce.CollectorErrors {
        strs = append(strs, err.Error())
    }
    return strings.Join(strs, ";")
}

type Event struct {
    Source  string
    Content string
}

type EventReceiver interface {
    OnEvent(evt Event)
}

type Collector interface {
    Init(evtReceiver EventReceiver) error
    Start(agtCtx context.Context) error
    Stop() error
    Destory() error
}

type Agent struct {
    collectors map[string]Collector
    evtBuf     chan Event
    cancel     context.CancelFunc
    ctx        context.Context
    state      int
}

func (agt *Agent) EventProcessGroutine() {
    var evtSeg [10]Event
    for {
        for i := 0; i < 10; i++ {
            select {
            case evtSeg[i] = <-agt.evtBuf:
            case <-agt.ctx.Done():
                return
            }
        }
        fmt.Println(evtSeg)
    }

}

func NewAgent(sizeEvtBuf int) *Agent {
    agt := Agent{
        collectors: map[string]Collector{},
        evtBuf:     make(chan Event, sizeEvtBuf),
        state:      Waiting,
    }

    return &agt
}

func (agt *Agent) RegisterCollector(name string, collector Collector) error {
    if agt.state != Waiting {
        return WrongStateError
    }
    agt.collectors[name] = collector
    return collector.Init(agt)
}

func (agt *Agent) startCollectors() error {
    var err error
    var errs CollectorsError
    var mutex sync.Mutex

    for name, collector := range agt.collectors {
        go func(name string, collector Collector, ctx context.Context) {
            defer func() {
                mutex.Unlock()
            }()
            err = collector.Start(ctx)
            mutex.Lock()
            if err != nil {
                errs.CollectorErrors = append(errs.CollectorErrors,
                    errors.New(name+":"+err.Error()))
            }
        }(name, collector, agt.ctx)
    }
    if len(errs.CollectorErrors) == 0 {
        return nil
    }
    return errs
}

func (agt *Agent) stopCollectors() error {
    var err error
    var errs CollectorsError
    for name, collector := range agt.collectors {
        if err = collector.Stop(); err != nil {
            errs.CollectorErrors = append(errs.CollectorErrors,
                errors.New(name+":"+err.Error()))
        }
    }
    if len(errs.CollectorErrors) == 0 {
        return nil
    }

    return errs
}

func (agt *Agent) destoryCollectors() error {
    var err error
    var errs CollectorsError
    for name, collector := range agt.collectors {
        if err = collector.Destory(); err != nil {
            errs.CollectorErrors = append(errs.CollectorErrors,
                errors.New(name+":"+err.Error()))
        }
    }
    if len(errs.CollectorErrors) == 0 {
        return nil
    }
    return errs
}

func (agt *Agent) Start() error {
    if agt.state != Waiting {
        return WrongStateError
    }
    agt.state = Running
    agt.ctx, agt.cancel = context.WithCancel(context.Background())
    go agt.EventProcessGroutine()
    return agt.startCollectors()
}

func (agt *Agent) Stop() error {
    if agt.state != Running {
        return WrongStateError
    }
    agt.state = Waiting
    agt.cancel()
    return agt.stopCollectors()
}

func (agt *Agent) Destory() error {
    if agt.state != Waiting {
        return WrongStateError
    }
    return agt.destoryCollectors()
}

func (agt *Agent) OnEvent(evt Event) {
    agt.evtBuf <- evt
}

agent_test.go

package microkernel

import (
    "context"
    "errors"
    "fmt"
    "testing"
    "time"
)

type DemoCollector struct {
    evtReceiver EventReceiver
    agtCtx      context.Context
    stopChan    chan struct{}
    name        string
    content     string
}

func NewCollect(name string, content string) *DemoCollector {
    return &DemoCollector{
        stopChan: make(chan struct{}),
        name:     name,
        content:  content,
    }
}

func (c *DemoCollector) Init(evtReceiver EventReceiver) error {
    fmt.Println("initialize collector", c.name)
    c.evtReceiver = evtReceiver
    return nil
}

func (c *DemoCollector) Start(agtCtx context.Context) error {
    fmt.Println("start collector", c.name)
    for {
        select {
        case <-agtCtx.Done():
            c.stopChan <- struct{}{}
            break
        default:
            time.Sleep(time.Millisecond * 50)
            c.evtReceiver.OnEvent(Event{c.name, c.content})
        }
    }
}

func (c *DemoCollector) Stop() error {
    fmt.Println("stop collector", c.name)
    select {
    case <-c.stopChan:
        return nil
    case <-time.After(time.Second * 1):
        return errors.New("failed to stop for timeout")
    }
}

func (c *DemoCollector) Destory() error {
    fmt.Println(c.name, "released resources.")
    return nil
}

func TestAgent(t *testing.T) {
    agt := NewAgent(100)
    c1 := NewCollect("c1", "1")
    c2 := NewCollect("c2", "2")
    agt.RegisterCollector("c1", c1)
    agt.RegisterCollector("c2", c2)
    if err := agt.Start(); err != nil {
        fmt.Printf("start error %v\n", err)
    }
    fmt.Println(agt.Start())
    time.Sleep(time.Second * 1)
    agt.Stop()
    agt.Destory()
}

JSON

内置的 JSON 解析

利用反射实现,通过 FeildTag 来标识对应的 json 值, 因为用了反射所以性能不是很好

struct_def.go

type BasicInfo struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

type JobInfo struct {
    Skills []string `json:"skills"`
}

type Employee struct {
    BasicInfo BasicInfo `json:"basic_info"`
    JobInfo   JobInfo   `json:"job_info"`
}

json_test.go

package json

import (
    "encoding/json"
    "fmt"
    "testing"
)

var jsonStr = `{
 "basic_info":{
    "name":"Mike",
    "age":20
 },
 "job_info":{
     "skills":["Java","Go","C"]
 }
}`

func TestEmbeddedJson(t *testing.T) {
    e := new(Employee)
    err := json.Unmarshal([]byte(jsonStr), e)
    if err != nil {
        t.Error(err)
    }
    fmt.Println(*e) // {{Mike 20} {[Java Go C]}}
    if v, err := json.Marshal(e); err == nil {
        // fmt.Println(v) // [123 34 98 97 115 105 99 95 105 110 102 111 34 58 123 34 110 97 109 101 34 58 34 77 105 107 101 34 44 34 97 103 101 34 58 50 48 125 44 34 106 111 98 95 105 110 102 111 34 58 123 34 115 107 105 108 108 115 34 58 91 34 74 97 118 97 34 44 34 71 111 34 44 34 67 34 93 125 125]
        fmt.Println(string(v)) // {"basic_info":{"name":"Mike","age":20},"job_info":{"skills":["Java","Go","C"]}}
    } else {
        t.Error(err)
    }
}

EasyJOSN

EasyJOSN 采用代码生成而非反射

安装: go get -u github.com/mailru/easyjson/...

使用: easyjson -all <file>.go

ps: windows PS C:\learn\go_learn\src> C:\Users\kenny\go\bin\easyjson.exe -all .\ch29\json\struct_def.go

package json

import (
    "fmt"
    "testing"
)

var jsonStr = `{
 "basic_info":{
    "name":"Mike",
    "age":20
 },
 "job_info":{
     "skills":["Java","Go","C"]
 }
}`

func TestEasyJson(t *testing.T) {
    e := Employee{}
    e.UnmarshalJSON([]byte(jsonStr))
    fmt.Println(e)
    if v, err := e.MarshalJSON(); err != nil {
        t.Error(err)
    } else {
        fmt.Println(string(v))
    }
}

HTTP服务

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write(([]byte("Hello World!")))
    })
    http.HandleFunc("/time", func(w http.ResponseWriter, r *http.Request) {
        t := time.Now()
        timeStr := fmt.Sprintf("{\"time\":\"%s\"}", t)
        w.Write(([]byte(timeStr)))
        // w.WriteHeader(200)
    })
    // http.ListenAndServe(addr, handler)
    // http.ListenAndServeTLS(addr, certFile, keyFile, handler)
    http.ListenAndServe(":8080", nil)
}

启动后:

访问 http://127.0.0.1:8080/ 显示: Hello World!

访问 http://127.0.0.1:8080/time 显示: {"time":"2020-11-09 11:59:50.1888855 +0800 CST m=+421.853937001"}

访问 http://127.0.0.1:8080/time/1 显示: Hello World!

Default Router

func (sh serverHandler) ServeHttp(rw ResonseWriter, req *Request){
    handler := sh.srv.Handler
    if handler == nil {
        handler = DefaultServeMux // 使用缺省的Router
    }
    if req.RequestURI == "*" && req.Method == "OPTIONS" {
        handler = globalOptionsHandler{}
    }
    handler.ServeHTTP(rw, req)
}

路由规则

  • URL 分为两种,末尾是/:表示一个子树,后面可以跟其他子路径;末尾不是/,表示一个叶子,固定的路径
    • 以结尾的URL可以匹配它的任何子路径,比如/images会匹配/images/cute-cat.jpg
  • 它采用最长匹配原则,如果有多个匹配,一定采用匹配路径最长的那个进行处理
  • 如果没有找到任何匹配项,会返回404错误

构建Restful服务

更好的 Router

示例代码:

package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}

面向资源的架构(Resource Oriented Architecture)

resource_oriented_architecture.go

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/julienschmidt/httprouter"
)

// Employee info
type Employee struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
}

var employeeDB map[string]*Employee

func init() {
    // 实际是从数据库获取数据
    employeeDB = map[string]*Employee{}
    employeeDB["Mike"] = &Employee{"e-1", "Mike", 35}
    employeeDB["Rose"] = &Employee{"e-2", "Rose", 45}
}

//Index function
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

//GetEmployeeByName Get Employee By Name
func GetEmployeeByName(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    qName := ps.ByName("name")
    var (
        ok       bool
        info     *Employee
        infoJson []byte
        err      error
    )
    if info, ok = employeeDB[qName]; !ok {
        w.Write([]byte("{\"error\":\"Not Found\"}"))
        return
    }
    // 这里用内置的json解析只是示例
    if infoJson, err = json.Marshal(info); err != nil {
        w.Write([]byte(fmt.Sprintf("\"error\":\"%s\"", err)))
        return
    }
    w.Write(infoJson)
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/employee/:name", GetEmployeeByName)

    log.Fatal(http.ListenAndServe(":8080", router))
}

别让性能被所“锁”住

关于性能优化的话题,第一个要提到的就是锁了,很多程序的性能问题就是由锁导致的。Go 语言提供的读写锁,互斥的写锁间的切换是有性能消耗的。

大多数同学认为大多数情况下都是读,读锁之间由于不互斥,所以应该没有很大的性能消耗。

但事实上真的是这样吗? 我们来看一个例子:

lock_test.go

package lock

import (
    "fmt"
    "sync"
    "testing"
)

var cache map[string]string

const NUM_OF_READER int = 40
const READ_TIMES = 100000

func init() {
    cache = make(map[string]string)

    cache["a"] = "aa"
    cache["b"] = "bb"
}

func lockFreeAccess() {

    var wg sync.WaitGroup
    wg.Add(NUM_OF_READER)
    for i := 0; i < NUM_OF_READER; i++ {
        go func() {
            for j := 0; j < READ_TIMES; j++ {
                _, err := cache["a"]
                if !err {
                    fmt.Println("Nothing")
                }
            }
            wg.Done()
        }()
    }
    wg.Wait()
}

func lockAccess() {
    var wg sync.WaitGroup
    wg.Add(NUM_OF_READER)
    m := new(sync.RWMutex)

    for i := 0; i < NUM_OF_READER; i++ {
        go func() {
            for j := 0; j < READ_TIMES; j++ {
                m.RLock()
                _, err := cache["a"]
                if !err {
                    fmt.Println("Nothing")
                }
            }
            m.RUnlock()
            wg.Done()
        }()
    }
    wg.Wait()
}

func BenchmarkLockFree(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        lockFreeAccess()
    }
}

func BenchmarkLock(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        lockAccess()
    }
}
[root@VM_0_14_centos lock]# go test -bench=.
goos: linux
goarch: amd64
pkg: learn/src/ch33/lock
BenchmarkLockFree-2           84          12858862 ns/op
BenchmarkLock-2               12          89777727 ns/op
PASS
ok      learn/src/ch33/lock     2.272s

可以看出LockFree 比 Lock 快 8倍左右。

看一下 cpuprofile:

[root@VM_0_14_centos lock]# go test -bench=. -cpuprofile=cpu.prof
goos: linux
goarch: amd64
pkg: learn/src/ch33/lock
BenchmarkLockFree-2           82          13391473 ns/op
BenchmarkLock-2               21          86614909 ns/op
PASS
ok      learn/src/ch33/lock     3.164s
[root@VM_0_14_centos lock]# go tool pprof cpu.prof 
File: lock.test
Type: cpu
Time: Nov 11, 2020 at 6:25pm (CST)
Duration: 3.16s, Total samples = 5.78s (183.00%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top -cum
Showing nodes accounting for 5.76s, 99.65% of 5.78s total
Dropped 5 nodes (cum <= 0.03s)
      flat  flat%   sum%        cum   cum%
     0.28s  4.84%  4.84%      3.61s 62.46%  learn/src/ch33/lock.lockAccess.func1
     2.21s 38.24% 43.08%      2.50s 43.25%  runtime.mapaccess2_faststr
     2.45s 42.39% 85.47%      2.45s 42.39%  sync.(*RWMutex).RLock
     0.53s  9.17% 94.64%      2.15s 37.20%  learn/src/ch33/lock.lockFreeAccess.func1
     0.17s  2.94% 97.58%      0.17s  2.94%  runtime.add
     0.07s  1.21% 98.79%      0.12s  2.08%  runtime.(*bmap).keys (inline)
     0.05s  0.87% 99.65%      0.05s  0.87%  runtime.isEmpty (inline)
(pprof) list lockAccess
Total: 5.78s
ROUTINE ======================== learn/src/ch33/lock.lockAccess.func1 in /root/go_learn/src/ch33/lock/locak_test.go
     280ms      3.61s (flat, cum) 62.46% of Total
         .          .     41:   wg.Add(NUM_OF_READER)
         .          .     42:   m := new(sync.RWMutex)
         .          .     43:
         .          .     44:   for i := 0; i < NUM_OF_READER; i++ {
         .          .     45:           go func() {
      50ms       50ms     46:                   for j := 0; j < READ_TIMES; j++ {
      40ms      2.49s     47:                           m.RLock()
     180ms      1.06s     48:                           _, err := cache["a"]
      10ms       10ms     49:                           if !err {
         .          .     50:                                   fmt.Println("Nothing")
         .          .     51:                           }
         .          .     52:                   }
         .          .     53:                   m.RUnlock()
         .          .     54:                   wg.Done()

可以看到在 m.RLock() 也是有一定的性能消耗的。

谈到RWLock,我们知道Go内置的Map是不支持线程安全的,Map又是我们常用的一种集合结构。

sync.Map

需要并发读写时,一般的做法是加锁,但这样性能并不高,每次锁的时候都会锁住整个Map,锁的范围很大,锁冲突的几率非常的高,性能也相对较低。Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构。

sync.Map 的为了减少锁冲突,采用了空间换时间的方案,并且采用指针的方式间接实现值的映射,所以存储空间会较 built-in map大。

怎样保证并发安全字典中的键和值的类型正确性?
并发安全字典如何做到尽量避免使用锁?

sync.Map类型在内部使用了大量的原子操作来存取键和值,并使用了两个原生的map作为存储
介质。

其中一个原生map被存在了sync.Map的read字段中,该字段是sync/atomic.Value类型的。

由read字段的类型可知,sync.Map在替换只读字典的时候根本用不着锁。另外,这个只读字典在存储键值对的时候,还在值之上封装了一层。它先把值转换为了unsafe.Pointer类型的值,然后再把后者封装,并储存在其中的原生字典中。如此一来,在变更某个键所对应的值的时候,就也可以使用原子操作了。

sync.Map中的另一个原生字典由它的dirty字段代表。它存储键值对的方式与read字段中的原
生字典一致,它的键类型也是interface{},并且同样是把值先做转换和封装后再进行储存
的。我们暂且把它称为脏字典。

sync.Map在查找指定的键所对应的值的时候,总会先去只读字典中寻找,并不需要锁定互斥锁。只有当确定“只读字典中没有,但脏字典中可能会有这个键”的时候,它才会在锁的保护下去访问脏字典。

相对应的,sync.Map在存储键值对的时候,只要只读字典中已存有这个键,并且该键值对未被标记为“已删除”,就会把新值存到里面并直接返回,这种情况下也不需要用到锁。否则,它才会在锁的保护下把键值对存储到脏字典中。

对于删除键值对,sync.Map会先去检查只读字典中是否有对应的键。如果没有,脏字典中可能
有,那么它就会在锁的保护下,试图从脏字典中删掉该键值对。

除此之外,还有一个细节需要注意,只读字典和脏字典之间是会互相转换的。在脏字典中查找键
值对次数足够多的时候,sync.Map会把脏字典直接作为只读字典,保存在它的read字段中,然
后把代表脏字典的dirty字段的值置为nil。

在这之后,一旦再有新的键值对存入,它就会依据只读字典去重建脏字典。这个时候,它会把只
读字典中已被逻辑删除的键值对过滤掉。理所当然,这些转换操作肯定都需要在锁的保护下进
行。

综上所述,sync.Map的只读字典和脏字典中的键值对集合并不是实时同步的,它们在某些时间
段内可能会有不同。

由于只读字典中键的集合不能被改变,所以其中的键值对有时候可能是不全的。相反,脏字典中
的键值对集合总是完全的,并且其中不会包含已被逻辑删除的键值对。

因此,可以看出,sync.Map 适合读多写少,且 key 相对稳定的情况下,并发安全字典的性能往往会更好。

在几个写操作当中,新增键值对的操作对并发安全字典的性能影响是最大的,其次是删除操作,
最后才是修改操作。

那么读写差不多都很频繁的情况下,有没有一个更好的选择呢?

Concurrent Map

在 java里面有一个 ConcurrentHashMap, Go build-in中并没有实现。

先看看它的原理:

image

可以看到ConcurrentMap 封装了多个带读写锁的go原生map。它把一个大的map划分成多个小的map,这样不同的读写操作很大概率上是访问不同的小的map及不同的细粒度的锁。这样就大大降低了访问同一区域的概率也就降低了锁冲突的概率提高读写速度。

Go实现ConcurrentMap:https://github.com/streamrail/concurrent-map

比对三种所的性能

具体测试代码:https://github.com/AltairAki/go_learning_note/tree/master/ch33/maps

NumOfReader = 100,NumOfWrite = 100

BenchmarkSyncMap/map_with_RWLock-2                   201           5817536 ns/op
BenchmarkSyncMap/map_with_SyncMap-2                   99          11775846 ns/op
BenchmarkSyncMap/map_with_ConcurrentMap-2            391           3088181 ns/op

NumOfReader = 100,NumOfWrite = 200

BenchmarkSyncMap/map_with_RWLock-2                   894           1488886 ns/op
BenchmarkSyncMap/map_with_SyncMap-2                  786           1657194 ns/op
BenchmarkSyncMap/map_with_ConcurrentMap-2           1357            884853 ns/op

NumOfReader = 200,NumOfWrite = 100

BenchmarkSyncMap/map_with_RWLock-2                   189           6418151 ns/op
BenchmarkSyncMap/map_with_SyncMap-2                   85          13389606 ns/op
BenchmarkSyncMap/map_with_ConcurrentMap-2            324           3954245 ns/op

NumOfReader = 100,NumOfWrite = 10

BenchmarkSyncMap/map_with_RWLock-2                   615           1761675 ns/op
BenchmarkSyncMap/map_with_SyncMap-2                  769           1577373 ns/op
BenchmarkSyncMap/map_with_ConcurrentMap-2           1432            863170 ns/op

别让性能被“锁”住总结

GC友好的代码

避免内存分配和复制

1.复杂对象尽量传递引用

  • 数组的传递
  • 结构体的传递

示例代码:

pass_ref_test.go

package pass_ref

import "testing"

const NumOfElems = 1000

type Content struct {
    Detail [10000]int
}

func withValue(arr [NumOfElems]Content) int {
    // fmt.Println(&arr[2])
    return 0
}

func withReference(arr *[NumOfElems]Content) int {
    // b:= *arr
    // fmt.Println(&arr[2])
    return 0
}

func TestFn(t *testing.T) {
    var arr [NumOfElems]Content
    withValue(arr)
    withReference(&arr)
}

func BenchmarkPassingArrayWithValue(b *testing.B) {
    var arr [NumOfElems]Content

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        withValue(arr)
    }
    b.StopTimer()
}

func BenchmarkPassingArrayWithReference(b *testing.B) {
    var arr [NumOfElems]Content

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        withReference(&arr)
    }
    b.StopTimer()
}
[root@VM_0_14_centos pass_ref]# go test -bench=.
BenchmarkPassingArrayWithValue-2              46          28612301 ns/op
BenchmarkPassingArrayWithReference-2    1000000000               0.365 ns/op
PASS
ok      learn/src/ch34/gc_friendly/pass_ref     1.948s

可以看出数组的值传递与传递引用有非常大的差距。

打开 GC 日志

只要在程序执行之前加上环境变量 GODEBUG=gctrace=1, 如:

GODEBUG=gctrace=1 go test -bench=.
GODEBUG=gctrace=1 go run main.go

日志详细信息参考: https://godoc.org/runtime

go tool trace

go trace 也可以像 profile 一样在代码中进行细粒度的追踪

package main
import(
    "os"
    "runtime/trace"
)

func main(){
    f,err := os.Create("trace.out")
    if err != nil {
        panic(err)
    }
    defer f.Close

    err = trace.Start(f)

    if err != nil {
        panic(err)
    }
    defer trace.Stop()
    // Your program here
}

测试程序生成trace文件

go test -bench=. -trace=trace.out

可视化 trace 信息

go tool trace trace_val.out

2. 初始化合适的大小(切片)

切片是可以自增长的,但是自动扩容是由代价的(会不断的去分配新的内存并且拷贝数据性能会很低)

示例代码:

auto_growing_test.go

package auto_growing

import "testing"

const numOfElems = 100000
const times = 1000

func TestAutoGrow(t *testing.T) {
    for i := 0; i < times; i++ {
        s := []int{}
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

func TestProperInit(t *testing.T) {
    for i := 0; i < times; i++ {
        s := make([]int, 0, numOfElems)
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

func TestOverSizeInit(t *testing.T) {
    for i := 0; i < times; i++ {
        s := make([]int, 0, 800000)
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkAutoGrow(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := []int{}
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkProperInit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, numOfElems)
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

func BenchmarkOverSizeInit(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := make([]int, 0, numOfElems*8)
        for j := 0; j < numOfElems; j++ {
            s = append(s, j)
        }
    }
}

查看benchmark:

BenchmarkAutoGrow-2                 1486            827854 ns/op
BenchmarkProperInit-2               7251            157576 ns/op
BenchmarkOverSizeInit-2             1393            770699 ns/op

可以看到初始化合适的大小性能会比其他两个高很多,因此在使用slice的时候,如果可以知道slice的大小,最好去初始化到合适的大小或者可以考虑使用数组。

3. 复用内存-减少内存的分配

高可用性服务设计

1.高效字符串连接

package concat_string

import (
    "bytes"
    "fmt"
    "strconv"
    "strings"
    "testing"
)

const numbers = 100

func BenchmarkSprintf(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < numbers; j++ {
            s = fmt.Sprintf("%v%v", s, j)
        }
    }
    b.StopTimer()
}

// Go 1.10 以后引入builder
func BenchmarkStringBuilder(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        for j := 0; j < numbers; j++ {
            builder.WriteString(strconv.Itoa(j))
        }
        _ = builder.String()
    }
    b.StopTimer()
}

func BenchmarkStringBytesBuf(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var buf bytes.Buffer
        for j := 0; j < numbers; j++ {
            buf.WriteString(strconv.Itoa(j))
        }
        _ = buf.String()
    }
    b.StopTimer()
}

func BenchmarkStringAdd(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var s string
        for j := 0; j < numbers; j++ {
            s += strconv.Itoa(j)
        }
    }
    b.StopTimer()
}
BenchmarkSprintf-2                 45969             26651 ns/op
BenchmarkStringBuilder-2          933669              1377 ns/op
BenchmarkStringBytesBuf-2         730882              1697 ns/op
BenchmarkStringAdd-2              143427              8268 ns/op

可以看出性能最好的是StringBuilder。

2. 面向错误的设计

“Once you accept that failures will happen, you have the ability to design your system's reaction to the failures.”

ps: 一旦你接受失败会发生,你就有能力设计你的系统对失败的反应

2.1隔离

隔离出错的部分,减少对其他部分的影响,使系统还能以一定程度的功能工作,还能被用户使用。

隔离错误 - 设计

image

Micro Kernel 中其中一个plugin crash掉后不影响其他与之无关的plugin。

隔离错误 - 部署

微服务 - 服务拆分

image

单体服务很难做到错误隔离的。

服务拆分后,某一个服务挂掉了,依赖于它的服务可以进行一些降级措施,使得仍然能够工作。

比如网站页面的查询服务挂掉了,我们可以把上一次cache的商品信息返回,然后总是显示cache的商品信息,而不支持进一步的查询了。

虽然有部分功能损失了,但是整体网站大部分功能还是可用的。

逻辑结构的重用 VS 部署结构的隔离

image

尽管 Data Transform Service 的代码是可以复用的,但是 Log Data Collector 的日志数据过大导致 Data Transform Service 挂掉。那么 Critical Data Collector 也会无法工作。

2.2冗余

负载均衡

image

单点失效

image

两台负载均衡,平均的QPS是1500,每台的最大QPS是1000

如果其中一个失效之后,那么所有的1500的QPS都会打到一台机器上,导致另外一台也失效。

2.3限流

除了部署更多的机器,还可以基于每台机器的处理能力,做出一个限定。

token bucket:

image

令牌桶简易版实现

限流器系列2 - Token Bucket 令牌桶

2.4 慢响应

A quick rejection is better than a slow response.

比如我们使用连接池,当所有的连接都被使用完以后,所有的query都非常的慢。导致所有的连接都在query,这时候再来的请求,被block住阻塞住。导致系统无响应。

不要无休止的等待,给阻塞操作都加上一个期限

2.5 错误传递

image

断路器一般配合服务降级完成,切断失败的service,防止错误的传递。

3. 面向恢复的设计

“A priori prediction of all failure modes is not possible.”

ps:所有失败模式的是不可预知的。

3.1 有效的健康检查

  • 注意僵尸进程
    • 池化资源耗尽
    • 死锁

很多的健康检查都是进行 http/tcp 的 ping 或者干脆检查进程在不在。

如上面提到的由于慢响应导致池化资源耗尽,虽然可以 ping 通但是无法提供服务。

ping 一个关键路径的检查系统,看能不能返回一个正常的结果来预防僵尸进程。

还有就是如下面代码:

defer func() {
    if err := recover(); err != nil {
        log.Error("recovered panic",err)
    }
}

只能告诉用户系统不可用,并不能真的恢复。

对于未知的错误,你也没有recover它的一个真正的方法。

干脆退掉你的进程,让supervisor的重启你的进程。

3.2 构建可恢复的系统

  • 拒绝单体系统

  • 面向错误和恢复的设计

    • 在依赖服务不可用时,可以继续存活

    • 快速启动

    • 无状态 (如果有状态,不能及时的迁移状态的话也会影响系统)

3.3 与客户端协商

服务器:“我太忙了,请慢点发送数据”

Client: “好,我一分钟后再发送”

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇