Golang问题归纳

简单

golang里的数组和切片有了解过吗

  • 数组和切片都是拥有相同类型一组元素集合

  • 数组为固定长度,切片为可变长度

  • 数组不可扩容,切片可以,切片扩容,如果不足1024每次扩容为两倍扩容,如果高于1024,为1.25倍扩容

  • 切片实际底层指向的也是一个数组的指针,切片的底层的结构体可以看到,有 指针 容量长度

  • 数组是值类型,将一个数组赋值给另一个数组时,传递的是一份深拷贝,赋值和函数传参操作都会复制整个数组数据,会占用额外的内存;切片是引用类型,将一个切片赋值给另一个切片时,传递的是一份浅拷贝,赋值和函数传参操作只会复制len和cap,但底层共用同一个数组,不会占用额外的内存。

for range (slice和map)时遇到的“坑”

func main() {
    s := []int{1, 2, 3}
    m := make(map[int]*int)
    for i, v := range s {
        // m[i] = &v 这样是不行的,
        // 因为for range 迭代过程是根据slice中的变量遍历出来的新变量,遍历出来的值都是同一地址。所以应该用一个变量进行接受,这样每个不同值的地址就不同了
        n := v
        m[i] = &n //这样才是正确的姿势
    }
    fmt.Println(s) // [1 2 3]
    for _, v := range m {
        fmt.Println(*v)// 1 2 3
    }
}

多个切片如果共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片

func main() {
    slice1 := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"}
    Q2 := slice1[3:6]
    fmt.Println(Q2, len(Q2), cap(Q2)) // [4 5 6] 3 9
    Q3 := slice1[5:8]
    fmt.Println(Q3, len(Q3), cap(Q3)) // [6 7 8] 3 7
    Q3[0] = "Unknown"
    fmt.Println(Q2, Q3) // [4 5 Unknown] [Unknown 7 8]

    a := []int{1, 2, 3, 4, 5}
    shadow := a[1:3]
    fmt.Println(shadow, a) // [2 3] [1 2 3 4 5]
    shadow = append(shadow, 100)
    // 会修改指向数组的所有切片
    fmt.Println(shadow, a) // [2 3 100] [1 2 3 100 5]
}

使用 func copy(dst, src []Type) int 解决

func main() {
    slice1 := []string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"}
    Q2 := make([]string, 3)
    copy(Q2, slice1[3:6])
    fmt.Println(Q2, len(Q2), cap(Q2)) // [4 5 6] 3 3
    Q3 := make([]string, 3)
    copy(Q3, slice1[5:8])
    fmt.Println(Q3, len(Q3), cap(Q3)) // [6 7 8] 3 3
    Q3[0] = "Unknown"
    fmt.Println(Q2, Q3) // [4 5 6] [Unknown 7 8]

    a := []int{1, 2, 3, 4, 5}
    shadow := make([]int, 2)
    copy(shadow, a[1:3])
    fmt.Println(shadow, a) // [2 3] [1 2 3 4 5]
    shadow = append(shadow, 100)
    fmt.Println(shadow, a) // [2 3 100] [1 2 3 4 5]
}

实现slice线程安全有两种方式

  1. 通过加锁实现slice线程安全,适合对性能要求不高的场景

func main() {
    var lock sync.Mutex //互斥锁
    a := make([]int, 0)
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            lock.Lock()
            defer lock.Unlock()
            a = append(a, i)
        }(i)
    }
    wg.Wait()
    fmt.Println(len(a)) // 10000
}
  1. 通过channel实现slice线程安全,适合对性能要求高的场景

func main() {
    buffer := make(chan int)
    a := make([]int, 0)
    // 消费者
    go func() {
        for v := range buffer {
            a = append(a, v)
        }
    }()
    // 生产者
    var wg sync.WaitGroup
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            buffer <- i
        }(i)
    }
    wg.Wait()

    fmt.Println(len(a)) // 10000
}

数组怎么转集合

func main() {
    m := make(map[int]int)
    arr := []int{1, 2, 3, 4, 5}
    for i, v := range arr {
        m[i] = v
    }
    fmt.Println(m)
}

介绍一下通道

  • 通道用于协程之间数据的传递,通道有三种类型,读和写的单向通道和双向通道

  • 通道可以控制协程的并发数

map取一个key,然后修改这个值,原map数据的值会不会变化

func main() {
    ma := make(map[int]int)
    ma[1] = 123
    updateMapValue(ma)

    fmt.Println(ma[1]) //321
}

func updateMapValue(ma map[int]int) {
    ma[1] = 321
}

channel有缓冲和无缓冲在使用上有什么区别?

无缓冲的与有缓冲channel有着重大差别:一个是同步的 一个是非同步的

比如

ch1:=make(chan int) 无缓冲

ch2:=make(chan int,1) 有缓冲

ch1<-1 无缓冲的

不仅仅是 向 c1 通道放 1 而是 一直要有别的协程 <-ch1 接手了 这个参数,那么ch1<-1之后的代码才会继续执行下去,要不然就一直阻塞着

而 ch2<-1 则不会阻塞,因为缓冲大小是1 (其实是缓冲大小为0)只有当 放第二个值的时候 第一个还没被人拿走,这时候才会阻塞

打个比喻

无缓冲的 就是一个送信人去你家门口送信 ,你不在家 他不走,你一定要接下信,他才会走。

无缓冲保证信能到你手上

有缓冲的 就是一个送信人去你家仍到你家的信箱 转身就走 ,除非你的信箱满了 他必须等信箱空下来。

有缓冲的 保证 信能进你家的邮箱

如何判断channel是否关闭

用 select 和 <-ch 来结合可以解决这个问题

ok的结果和含义:

true:读到数据,并且通道没有关闭。

false:通道关闭,无数据读到。

func main() {
    ch := make(chan int, 10)
    for i := 1; i <= 9; i++ {
        ch <- i
    }
    close(ch)
    for i := 0; i < 10; i++ {
        select {
        case v, ok := <-ch:
            if ok {
                fmt.Println(v)
            } else {
                fmt.Println("关掉了")
            }
        default:
            fmt.Println("没啥事")
        }
    }
}

需要注意:

  • case 的代码必须是 _, ok:= <- ch 的形式,如果仅仅是 <- ch 来判断,是错的逻辑,因为主要通过 ok的值来判断;

  • select 必须要有 default 分支,否则会阻塞函数,我们要保证一定能正常返回;

  • 写入channel的时候判断其是否已经关闭,此时如果 channel 关闭,写入时触发panic: send on closed channel

make 与 new 的区别

简单的说,new只分配内存,make用于slice,map,和channel的初始化。

func main() {
    var v *int

    *v = 8

    fmt.Println(*v)
    // 会报错
    // panic: runtime error: invalid memory address or nil pointer dereference
}

解决:

func main() {
    var v *int
    v = new(int)

    *v = 8

    fmt.Printf("%d\n", *v)
}

go语言的引用类型有什么?

  • map:golang中map是一种无序的、键值对的集合,其是通过key检索数据,且key类似于索引,指向数据的值,golang中常使用hash表来实现map。

  • pointers:golang中golang是指计算机内存中变量所在的内存地址,使用pointers可以节省内存,但golang中pointers不能进行偏移和运算,只能读取指针的位置。

  • slice:golang中slice是对数组的抽象,相对于数组,slice的长度是不固定的,可以追加元素,且在追加元素时可以增大slice的容量。

  • channel:golang中channel是指管道,是一种用于实现并行计算方程间通信的类型,允许线程间通过发送和接收来传输指定类型的数据,初始值为nil。

  • interface:golang中interface是指接口,是一组方法签名的集合,可以使用接口来识别一个对象够进行的操作。

  • function:golang中function是指函数,function不支持嵌套、重载和默认参数,但无需声明原型,常使用func关键字定义函数。

管道关闭是否能读写

如果只定义管道,那么管道ch为nil,此时不能关闭管道,此时关闭管道会报错。只有将管道ch初始化之后,才能正常关闭管道。

func main() {
    //定义管道
    var ch chan int
    //make才能初始化
    ch = make(chan int)
    //关闭管道
    close(ch)
}

不能重复关闭管道,不可以,会报

panic: close of closed channel

关闭管道后再来读

func main() {
    //定义管道
    var ch chan int
    //初始化管道,缓存能力为3
    ch = make(chan int, 3)
    ch <- 123
    //关闭管道
    close(ch)
    go func() {
        x := <-ch
        fmt.Println("读到", x) // 读到 123

        x, ok := <-ch
        fmt.Println("读到", x, ok) // 读到 0 false
    }()

    time.Sleep(time.Second)
    fmt.Println("GAME OVER") // GAME OVER
}

因为我们给管道的第二个参数设置为3,这就让管道有了缓存能力。而关闭管道之前已经将数据123存入了管道,之后再读取管道内数据是能够读取到的

关闭管道后再来写

不能再往管道写数据,会报

panic: send on closed channel

问等待所有goroutine结束,怎么做

可以用无缓冲的Channel来实现当前等待goroutine的操作

func main() {
    waitChan := make(chan int) // 构建一个无缓冲的Channel

    goroutineCount := 10
    for i := 0; i < goroutineCount; i++ {
        go func(tag int) {
            do(tag)
            waitChan <- 1 // 往通道写入数据
        }(i)
    }
    goroutineDoneCount := 0
    for range waitChan { // 阻塞等待数据写入
        goroutineDoneCount++
        if goroutineDoneCount == goroutineCount {
            // 收到与开启goroutineCount相等时跳出循环
            break
        }
    }
    close(waitChan)
    fmt.Println("All goroutine finish")
}

func do(tag int) {
    fmt.Printf("Do by tag[%d] \n", tag)
}
Do by tag[0]
Do by tag[4]
Do by tag[9]
Do by tag[5]
Do by tag[6]
Do by tag[7]
Do by tag[8]
Do by tag[3]
Do by tag[1]
Do by tag[2]
All goroutine finish

当然channel阻塞并不是最佳方案,首先需要知道确定个数的goroutine,同时稍不注意就极易产生死锁

使用go标准库sync,其中提供了专门的解决方案sync.WaitGroup 用于等待一个goroutines集合的结束

func main() {
    goroutineWaitGroup := sync.WaitGroup{} // 构建一个waitGroup
    goroutineCount := 10
    for i := 0; i < goroutineCount; i++ {
        goroutineWaitGroup.Add(1) // 添加WaitGroup计数器
        go func(tag int) {
            defer goroutineWaitGroup.Done() // defer标记当前函数作用域执行结束后 释放一个计数器
            do(tag)
        }(i)
    }
    goroutineWaitGroup.Wait() //阻塞,直到WaitGroup中的计数器为0
    fmt.Println("All goroutine finish")
}

func do(tag int) {
    fmt.Printf("Do by tag[%d] \n", tag)
}
Do by tag[0]
Do by tag[1]
Do by tag[3]
Do by tag[2]
Do by tag[4]
Do by tag[5]
Do by tag[6]
Do by tag[7]
Do by tag[8]
Do by tag[9]
All goroutine finish

defer 是怎么用的

defer是什么

func main() {
    fmt.Println("start")
    defer fmt.Println(1)
    defer fmt.Println(2)
    defer fmt.Println(3)
    fmt.Println("end")
}
start
end
3
2
1

defer的执行机制

练习

/*
defer 延迟执行,时机在第一步和第二步之间
return 不是原子操作,
第一步:返回值赋值
// defer
第二步:真正的RET指令返回
*/

func main() {
    fmt.Println(f0())
    fmt.Println(f1())
    fmt.Println(f2())
    fmt.Println(f3())
    fmt.Println(f4())
    fmt.Println(f5())
    fmt.Println(f6())
    fmt.Println(f7())
    fmt.Println(f8(0))
}

func f0() int {
    x := 5
    defer func() {
        x++ //该步骤修改的是变量x,不是返回值,该函数的关键点在于,返回值<未被命名>,函数的runtime应该为:
        // 1,变量x被赋值5,2、返回值被赋值5 3、defer延迟执行修改变量x为6,4、最后执行return命令,返回了被赋值5的返回值
    }()
    return x // 5
}

func f1() (x int) {
    defer func() { x++ }() // runtime:1、函数的返回值命名为x,初始值为0 ,return的非原子操作导致return语句的第一步使返回值x被赋值为5;
    // 2、执行defer操作,使变量x(即在内存地址上跟返回值为同一个)经过++运算变为6;3、执行真正的return操作返回返回值x,即为6
    return 5 //6
}

func f2() (x int) {
    a := 1
    defer func() { //函数的runtime为:函数的返回值命名为x,初始值为0;变量a被赋值1;
        x++ // return语句的实际执行步骤:
        // 1、命名为x的返回值重新被赋值 x=a=1,
    }() // 2、执行defer语句,x++, x = 1+1 = 2
    // 3、执行真正的return指令,返回x为2
    return a // 2
}

func f3() (y int) {
    x := 5
    defer func() {
        x++
    }()
    return x //5 函数的runtime:函数的返回值命名为y,初始值为0;函数体中变量x被赋值5;
    // return语句的实际执行步骤:
    // 1、y = x = 5
    // 2、defer执行:x=5+1=6
    // 3、return真正执行,返回值y=5
}

func f4() (x int) {
    defer func(x int) {
        x++
    }(x)
    return 5 //函数的runtime:函数的返回值命名为x,初始值为0;
    // return语句的实际执行步骤:
    // 1、返回值被赋值x = 5
    // 2、defer执行:x作为函数参数传入匿名函数,执行 x=5+1=6,但是 "函数传参修改的是副本"
    // 3、return真正执行,返回值x=5
}

func f5() int {
    x := 0
    defer func(x int) {
        x++
    }(x)
    return 5 // 5
}

func f6() (x int) {
    defer func(x *int) {
        *x++
    }(&x)
    return 5 // 6//函数的runtime:函数的返回值命名为x,初始值为0;
    // return语句的实际执行步骤:
    // 1、返回值被赋值x = 5
    // 2、defer执行:x的指针作为函数参数传入匿名函数,执行 x=5+1=6,
    // 3、return真正执行,返回值x=6
}

func f7() (x int) {
    defer func(x int) int {
        x++ // 传递的是x的副本,不会对原值有影响
        return x
    }(x)
    return 5
}

func f8(x int) int {
    // var x  int
    return func(x int) int {
        x++
        return x // 1
    }(x)
    // return
}

go 语言的 panic 如何恢复

func main() {
    fmt.Println("c")
    defer func() { // 必须要先声明defer,否则不能捕获到panic异常
        fmt.Println("d")
        if err := recover(); err != nil {
            fmt.Println(err) // 这里的err其实就是panic传入的内容
        }
        fmt.Println("e")
    }()
    f()              //开始调用f
    fmt.Println("f") //这里开始下面代码不会再执行
}

func f() {
    fmt.Println("a")
    panic("异常信息")
    fmt.Println("b") //这里开始下面代码不会再执行
}
c
a
d
异常信息
e

go的init函数是什么时候执行的

init()函数会在包被初始化后自动执行,并且在main()函数之前执行,但是需要注意的是init()以及main()函数都是无法被显式调用的。

那么init()是不是最先执行的呢?

答案是否定的,首先,在他之前会进行全局变量的初始化。

当我们导入其他包时,会先初始化导入的包,

而初始化包时,会先加载全局变量,而后从上到下加载init()函数,

当被导入的包的init()函数执行完毕后,执行调用方的全局变量加载,init()函数的顺序加载,之后执行main()函数。

runtime提供常见的方法

获取goroot和os

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // 获取 GOROOT 目录:
    fmt.Println("GOROOT-->",runtime.GOROOT()) // GOROOT--> D:\Go

    // 获取操作系统
    fmt.Println("os/platform-->",runtime.GOOS) // GOOS--> windows,mac系统
}

获取CPU数量,和设置CPU数量

package main

import (
    "fmt"
    "runtime"
)

func init() {
    // 1.获取逻辑cpu的数量
    fmt.Println("逻辑CPU的核数:", runtime.NumCPU()) // 逻辑CPU的核数: 16
    // 2.设置go程序执行的最大的:[1,256]
    n := runtime.GOMAXPROCS(runtime.NumCPU())
    fmt.Println(n) // 16
}

func main() {}

Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("goroutine。。。")
        }
    }()
    for i := 0; i < 4; i++ {
        // 让出时间片,先让别的协议执行,它执行完,再回来执行此协程
        runtime.Gosched()
        fmt.Println("main。。")
    }
}

Goexit的使用(终止协程)

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // 创建新建的协程
    go func() {
        fmt.Println("goroutine开始。。。")

        // 调用了别的函数
        fun()

        fmt.Println("goroutine结束。。")
    }() // 别忘了()

    // 睡一会儿,不让主协程结束
    time.Sleep(3 * time.Second)
}

func fun() {
    defer fmt.Println("defer。。。")

    // return           // 终止此函数
    runtime.Goexit() // 终止所在的协程

    fmt.Println("fun函数。。。")
}
goroutine开始。。。
defer。。。

go什么场景使用接口

当有一个将根据输入数据运行的函数时。输入数据将决定使用哪种方法

package main

import (
    "fmt"
)

type SayHelloIntFace interface {
    SayHello()
}

type Person struct{}

// SayHello 此方法与 Person 类相关联
func (person Person) SayHello() {
    fmt.Printf("Hello!")
}

type Dog struct{}

// SayHello 此方法与 Dog 类相关联
func (dog Dog) SayHello() {
    fmt.Printf("woof! woof!")
}

func greeting(i SayHelloIntFace) {
    i.SayHello()
}

func main() {
    //实例化对象
    person := Person{}
    dog := Dog{}

    var i SayHelloIntFace

    fmt.Println("\nPerson : ")
    i = person
    greeting(i)

    fmt.Println("\n\nDog : ")
    i = dog
    greeting(i)
}

还有你的程序要使用接口的另一个地方是当您的程序在运行时无法确定输入数据类型时

例如,程序需要 JSON 数据,并且希望取消编组 JSON 数据以进行映射

知道顶级键是字符串,但在运行时不是较低级别的值数据类型

它可以是字符串、整数或符文。接口类型非常适合这种情况

package main

import (
    "encoding/json"
    "fmt"
)

func main() {
    //给定一个可能复杂的 JSON 对象
    msg := "{\"assets\" : {\"old\" : 123}}"

    // 我们只知道我们的顶级键是字符串 - “assets”

    // 但较低级别的值可以是整数、符文或字符串

    // 因此,我们创建了一个使用 interface{} 类型
    mp := make(map[string]interface{})

    //将 JSON 解码为我们的map
    err := json.Unmarshal([]byte(msg), &mp)
    if err != nil {
        println(err)
        return
    }

    //看看map现在有什么
    fmt.Printf("mp is now: %+v\n", mp)

    // 迭代map,将元素一一打印出来

    // 注意:这里必须尊重 mp 否则范围将失败
    for key, value := range mp {
        fmt.Println("key:", key, "value:", value)
    }
}

信令用wss还是ws?

WS(WebSocket )是不安全的 ,容易被窃听,因为任何人只要知道你的ip和端口,任何人都可以去连接通讯

WSS(Web Socket Secure)是WebSocket的加密版本

用Channel和两个协程实现数组相加

package main

import (
    "fmt"
    "sync"
)

type tool interface {
    addArr(a, b []int) []int
}

type Tool struct {
    wg sync.WaitGroup
}

// 用channel和两个goroutine实现数组相加
func (t *Tool) addArr(a, b []int) []int {
    ch := make(chan int)
    c := make([]int, len(a))

    t.wg.Add(2)

    go func() {
        defer t.wg.Done()
        for i := range b {
            temp := <-ch
            c[i] = temp + b[i]
        }
    }()

    go func() {
        defer t.wg.Done()
        for i := range a {
            // time.Sleep(time.Second)
            ch <- a[i]
        }
    }()

    return c
}

func main() {
    a := []int{2, 4, 6, 8}
    b := []int{1, 3, 5, 7}
    t := &Tool{}
    ans := t.addArr(a, b)
    t.wg.Wait()
    fmt.Println(ans)
}

go利用channel通信的方式

package main

import (
    "fmt"
    "time"
)

func watch(c chan int) {
    if <-c == 1 {
        fmt.Println("hello")
    }
}

func watchWithFor(c chan int) {
    for {
        select {
        case a := <-c:
            if a == 1 {
                fmt.Println("hello")
            }
        default:
            fmt.Print("default")
        }
    }
}

func main() {
    c := make(chan int)
    go watchWithFor(c)
    time.Sleep(time.Second)
    c <- 1
    time.Sleep(time.Second)
}

// output: hello

golang常用并发编程的几种模式

扇入扇出

  • 所谓的扇入是指将多路通道聚合到一条通道中处理,Go语言最简单的扇入就是使用select聚合多条通道服务;

  • 所谓的扇出是指将一条通道发散到多条通道中处理。在Go语言里面实现就是使用go关键字启动多个goroutine并发处理。

  • 扇入就是合,扇出就是分。

  • 当生产者的速度很慢时,需要使用扇入技术聚合多个生产者满足消费者,比如很耗时的加密/解密服务;

  • 当消费者的速度很慢时,需要使用扇出技术,比如Web服务器并发请求处理。扇入和扇出是Go并发编程中常用的技术。

扇入扇出和pipeline(管道)的最大区在于,管道是串行的,但是扇入扇出是并行的。并行是指,一个管道可以接收其它多个数据源的输入,前提是管道对于多个数据源的输入顺序是不敏感的。

管道的核心思想在于,每个单独的输入都有一个单独goroutine处理,并写入同一个数据源。

参考:

Golang 并发模式:扇入、扇出 - 知乎 (zhihu.com)

GoLang使用Goroutine+Channel实现流水线处理,扇入扇出思想解决流水线上下游供需不平衡_AirGo.的博客-CSDN博客

package main

import (
    "fmt"
    "math/rand"
)

func GenerateIntA() chan int {
    ch := make(chan int, 10)
    go func() {
        for {
            ch <- rand.Int()
        }
    }()
    return ch
}

func GenerateIntB() chan int {
    ch := make(chan int, 10)
    go func() {
        for {
            ch <- rand.Int()
        }
    }()
    return ch
}

func GenerateInt() chan int {
    ch := make(chan int, 20)
    go func() {
        // 使用select的扇入技术(Fan in)增加生成的随机源
        for {
            select {
            case ch <- <-GenerateIntA():
            case ch <- <-GenerateIntB():
            }
        }
    }()
    return ch
}

func main() {
    ch := GenerateInt()
    for i := 0; i < 100; i++ {
        fmt.Println(<-ch)
    }
}

优胜劣汰模式 场景:执行远程访问,远程服务响应不可靠的时候,同时开启go程,只取最快返回的,可以提高程序性能,但是占用资源会高一些

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func job() int {
    rand.Seed(time.Now().Unix())
    ret := rand.Intn(5)
    time.Sleep(time.Second * time.Duration(ret)) // 模拟业务访问延迟
    return ret
}

func main() {
    c := make(chan int)
    for i := 0; i < 5; i++ {
        go func() {
            c <- job()
        }()
    }
    fmt.Printf("最快的用了%d s", <-c)
}

生产者模式

package main

import (
    "fmt"
    "time"
)

func Producer(out chan int) {
    defer close(out)
    for i := 0; i < 5; i++ {
        out <- i * 2
        time.Sleep(time.Second * 2)
    }
}

func Consumer(out chan int) (r chan struct{}) {
    r = make(chan struct{})
    go func() {
        defer close(r)
        defer func() {
            r <- struct{}{}
        }()
        for item := range out {
            fmt.Println(item) // 模拟业务逻辑
        }
    }()
    return r
}

func main() {
    c := make(chan int)
    go Producer(c)
    r := Consumer(c)
    <-r
}

goroutine 并发执行顺序的控制

package main

import (
    "fmt"
    "sync"
)

var ch1 = make(chan struct{}, 1)
var ch2 = make(chan struct{}, 1)

func job1() {
    fmt.Println("job1...")
    ch1 <- struct{}{}
}

func job2() {
    <-ch1 // 阻塞等待 job1 执行完成
    fmt.Println("job2...")
    ch2 <- struct{}{}
}

func job3() {
    <-ch2 // 阻塞等待 job2 执行完成
    fmt.Println("job3...")
}
func do(fns ...func()) *sync.WaitGroup {
    wg := &sync.WaitGroup{}
    for _, fn := range fns {
        wg.Add(1)
        go func(f func()) {
            defer wg.Done()
            f()
        }(fn)
    }
    return wg
}
func main() {
    wg := do(job1, job2, job3)
    wg.Wait()
}

退出通知机制

  • 读取已经关闭的通道不会引起阻塞,也不会导致panic,而是立即返回该通道存储类型的零值

  • 关闭select 监听的某个通道能使select立即感知此种通知,并能够进行相应的处理

package main

import (
    "fmt"
    "math/rand"
    "runtime"
)

// GenerateInt 是一个随机数生成器
func GenerateInt(done chan struct{}) chan int {
    ch := make(chan int)
    go func() {
    Label:
        for {
            select {
            case ch <- rand.Int():
            // 增加一路监听,对退出通知信号done的监听
            case <-done:
                break Label
            }
        }
        // 收到通知后,关闭通道ch
        close(ch)
    }()

    return ch
}

func main() {
    done := make(chan struct{})
    ch := GenerateInt(done)

    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(<-ch)

    // 发送通知,告知生产者停止生产
    close(done)

    fmt.Println(<-ch)
    fmt.Println(<-ch)

    // 此时,生产者已经退出
    println("Num Goroutine =", runtime.NumGoroutine())

}

// output:
// 5577006791947779410
// 8674665223082153551
// 6129484611666145821
// 0
// 0
// Num Goroutine = 1

channel配合协程的通信

协程之间的通信,需要用到协程计数器:sync.WaitGroup和通信工具:channel

  • 协程计数器:用来判断该协程中函数是否运行完毕,若完毕则-1,直至为0

  • Channel:是用来负责每个协程的信号,实现协程之间的通信和控制

package main

import (
    "fmt"
    "sync"
)

func printZ(wg *sync.WaitGroup, chanz chan string, chanl chan string) {
    // 当前循环结束,说明该协程结束,协程计数器-1
    defer wg.Done()
    // 协程结束,关闭该协程channel
    defer close(chanz)
    for i := 0; i < 5; i++ {
        fmt.Println(<-chanz)
        chanl <- "李四"
    }
}

func printL(wg *sync.WaitGroup, chanl chan string, chanw chan string) {
    // 当前循环结束,说明该协程结束,协程计数器-1
    defer wg.Done()
    // 协程结束,关闭该协程channel
    defer close(chanl)
    for i := 0; i < 5; i++ {
        fmt.Println(<-chanl)
        chanw <- "王五"
    }
}

func printW(wg *sync.WaitGroup, chanw chan string, chanz chan string) {
    // 当前循环结束,说明该协程结束,协程计数器-1
    defer wg.Done()
    // 协程结束,关闭该协程channel
    defer close(chanw)
    for i := 0; i < 5; i++ {
        fmt.Println(<-chanw)
        // 最后判断是否继续向chanl中塞名字
        switch {
        case i == 4:
            fmt.Println("main finished!")
        default:
            chanz <- "张三"
        }
    }
}

func main() {
    // 创建计数器
    wg := sync.WaitGroup{}
    // 初始化创建协程的个数
    wg.Add(3)
    // 申明三种协程所需要的channel
    var chanz = make(chan string, 1)
    var chanl = make(chan string, 1)
    var chanw = make(chan string, 1)
    // 初始化第一个channel
    chanz <- "张三"
    // 开启三个协程,
    // 每个协程中只有当前channel里面的数据用完了
    // 再赋值给下一个channel信号
    // 以保证三个协程按顺序执行
    go printZ(&wg, chanz, chanl)
    go printL(&wg, chanl, chanw)
    go printW(&wg, chanw, chanz)
    // 等待直到协程计数器归0,再结束主程序
    wg.Wait()
}

// 输出:
// 李四
// 张三
// 王五
// 李四
// 张三
// 王五
// 李四
// 张三
// 王五
// 李四
// 张三
// 王五
// 李四
// 张三
// 王五
// main finished!

有对项目和系统做性能测试吗?(benchmark 和 pprodf)

go 性能优化之 benchmark + pprof

go结构体和结构体指针的区别

package main

type MyStruct struct {
    Name string
}

func (s MyStruct) SetName1(name string) {
    s.Name = name
}

func (s *MyStruct) SetName2(name string) {
    s.Name = name
}

整体有以下几个考虑因素,按重要程度顺序排列:

  1. 在使用上的考虑:方法是否需要修改接收器?如果需要,接收器必须是一个指针。

  1. 在效率上的考虑:如果接收器很大,比如:一个大的结构,使用指针接收器会好很多。

  1. 在一致性上的考虑:如果类型的某些方法必须有指针接收器,那么其余的方法也应该有指针接收器,所以无论类型如何使用,方法集都是一致的。

回到上面的例子中,从功能使用角度来看:

  • 如果 SetName2 方法修改了 s 的字段,调用者是可以看到这些字段值变更的,因为其是指针引用,本质上是同一份。

  • 相对 SetName1 方法来讲,该方法是用调用者参数的副本来调用的,本质上是值传递,它所做的任何字段变更对调用者来说是看不见的。

另外对于基本类型、切片和小结构等类型,值接收器是非常廉价的。

因此除非方法的语义需要指针,那么值接收器是最高效和清晰的。在 GC 方面,也不需要过度关注。出现时再解决就好了。

让 Go panic 的十种方法

让 Go panic 的十种方法

结构体创建优化

以下面的结构为例,咱们看看下面的结构体:

package main

import (
    "fmt"
    "time"
    "unsafe"
)

type People struct {
    ID          int64     // Sizeof: 8 byte  Alignof: 8  Offsetof: 0
    Gender      int8      // Sizeof: 1 byte  Alignof: 1  Offsetof: 8
    NickName    string    // Sizeof: 16 byte Alignof: 8 Offsetof: 16
    Description string    // Sizeof: 16 byte Alignof: 8 Offsetof: 32
    IsDeleted   bool      // Sizeof: 1 byte  Alignof: 1  Offsetof: 48
    Created     time.Time // Sizeof: 24 byte Alignof: 8  Offsetof: 56
}

func main() {
    p := People{}
    fmt.Println(unsafe.Sizeof(p))
}

// output
// 80

从上面的输出可以看出打印结果为 80 字节,但是所有字段加起来是66 字节。那额外的 14 个字节是怎么来的呢?想必大部分同学也很清楚。64 位CPU处理器每次可以以 64 位(8 字节)块的形式传输数据。32 位 CPU的话则是32 位(4 字节)。

第一个字段ID占用 8 个字节,Gender字段占用了1 个字节并有 7 个未使用的字节。

第二个和第三个字段为字符串类型为16字节,接下来是IsDeleted字段,它需要 1 个字节并有 7 个未使用的字节。

最好的情况是是按字段的大小从大到小对字段进行排序。对上述结构体进行排序,大小减少到 72 个字节。最后两个字段 Gender 和 IsDeleted 被放在同一个块中,从而将未使用的字节数从 14 (2x7) 减少到 6 (1 x 6),在此过程中节省了 8 个字节。

package main

import (
    "fmt"
    "time"
    "unsafe"
)

type People struct {
    CreatedAt   time.Time // 24 bytes
    NickName    string    // 16 bytes
    Description string    // 16 bytes
    ID          int64     // 8 bytes
    Gender      int8      // 1 byte
    IsDeleted   bool      // 1 byte
}

func main(){
    p := People{}
    fmt.Println(unsafe.Sizeof(p))
}

// output
// 72

对于数字类型,有下面的大小保证:

类型

占用字节大小

byte, uint8, int8

1

uint16, int16

2

uint32, int32, float32

4

uint64, int64, float64, complex64

8

complex128

16

保证以下最小对齐属性:

  • 对于任何类型的变量x:unsafe.Alignof(x)至少为 1。

  • 对于struct 类型的变量x:unsafe.Alignof(x)是所有字段字节对齐的最大值unsafe.Alignof(x.f),但至少为 1。

  • 对于数组类型的变量x :unsafe.Alignof(x)与数组元素类型的变量的对齐方式相同。

如果struct或数组类型不包含大小大于零的字段(或元素),则其大小为零。两个不同的零大小变量在内存中可能具有相同的地址。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值