Go Programing Language 布道贴



  • 这应该是团队Go语言的第一帖,这里我想向团队推广Go语言,因为它实在是很好用,尤其是网络与并发方面,媲美C的效率,但是实现简洁简单,减轻心智负担。

    这是go的吉祥物,地鼠Gopher,也是Go程序员的自称:
    0_1550754163359_67ff747a34b52ccf15c5dd6f643c4a11.png

    Why Go ?Why not Go ?

    • 首先Go的作者大名鼎鼎
    1. Ken Tomphson:Unix创造者和C语言的发明者
    2. Rob Pike: 贝尔实验室高级研究员,开发了UTF-8和Plan9系统,Inferno和Limbo的创造者。
    3. Russ Cox: MIT System实验室的高材生,Plan9计划参与者。
    4. Robert Grieseme:Chromium JavaScript V8 Engine 架构师。
    • 再其次,Go语言生态已经成长起来,Kubernetes, Docker都是100%的Go Project。国内各大厂都在使用Go语言,字节跳动(头条)的推送引擎使用Go,每天handling 千亿级调用。滴滴的调度平台和大部分核心业务都是Powered By Go。知乎的后端微服务在最近也进行了大规模Go重构。(bilibili 也是除了少数web 端业务逻辑之外全栈Go哟~)

    • Go 是静态编译语言,会被直接编译为本机可执行代码,在这一点上效率已经超越所有动态语言。良好的Go代码的运行效率与C++相当(非-O2)(远超JVM系列语言)。其性能主要损失在维护类型安全和内存安全上。

    • Go的栈是可伸缩的,称为动态栈。你甚至可以返回一个局部变量的指针,Go的运行时会智能地保证它指向的内存不被释放。即你根本不需要考虑溢出,越界等问题。程序员不需要和内存分配作斗争,只需要专注于朴素的数据结构与算法完成目标

    • 首先,Go带有GC(garbage collect),因此根本不存在内存泄漏和段错误(Go 的早期版本GC效率被诟病,这是因为Go还很年轻,Go的作者们主要讲精力集中于编程范式,java这个年纪的时候还烂成一坨)(目前GO BUILD VER 1.11.2的GC效率已经和java GC同级)

    • Go有指针但禁用了危险的指针运算,但仍然提供了*unsafe.uintptr来还原危险的指针运算(仅提供给头脑清醒的程序员)。

    • 类型系统超级简洁,支持面向对象,却没有java又臭又长的类声明,却有足够表达力

    • 支持多返回值不定参数,不需要再别扭地定义一个结构体返回,也不需要纠结传参麻烦

    • 错误处理方式优雅(针对大型工程而言,网上有关于go错误处理方式别扭的言论,只能说这样的人自身水平不足)

    • 超快编译速度,10w+源文件的project编译仅需12s

    • 不需要写makefile,也不需要指定库和头文件包含目录,编译只需一句go build。自动分析依赖构建。

    • 语言内置强制代码规范,所有人写出来的代码一个风格,工程化的语言。

    • 语言内置单元测试框架,工程化的又一体现。



  • Go features

    Go 语言基本继承了C的全部思想,但是加入了现代化的工程思想和内存管理模型。并且以CSP顺序通信模型在语言级别支持并发。(后文有介绍)

    原生支持并发

    它的并发基础是协程(可理解为用户态的线程),通常我们知道Unix环境下,一个线程的启动总有一个基准栈,一般是2Mb大小,当上千个线程启动时,系统资源迅速耗尽。此外,当进行线程切换时,无可避免地涉及到从用户态到内核态再切换回来的过程,这对内核的负担也很大。而Go提供了Goroutine(协程,运行于用户态,Go自行实现了调度器)的支持,每个Goroutine 的启动栈仅有2-4 KB,十分轻量,且所有Goroutine运行于用户态,由Go runtime调度,内核负担小。通常可以启动成千的Goroutine。



  • Go的并发模型

    Go是以CSP顺序通信模型构建的并发模型。这与常见的c++的基于异步回调的libevent家族系列并发模型大不相同。

    有关CSP(Communication Sequential Process )顺序通信模型的paper见此处:CSDN下载

    Channel通信

    Go语言提供了go关键字来启动goroutine,而chan关键字则提供了不同go程之间的通信原语,这有点类似Unix上常用的pipe特性。用go社区的惯用语结束这一段:不要用共享内存来通信,而应该使用通信来共享内存。



  • go channel 详解

    channel 是 golang 中最核心的 feature 之一,因此理解 Channel 的原理对于学习和使用 golang 非常重要。

    channel 是 goroutine 之间通信的一种方式,可以类比成 Unix 中的进程的通信方式管道(Pipe)。

    CSP 模型

    在讲 channel 之前,有必要先提一下 CSP 模型,传统的并发模型主要分为 Actor 模型和 CSP 模型,CSP 模型全称为 communicating sequential processes,CSP 模型由并发执行实体(进程,线程或协程),和消息通道组成,实体之间通过消息通道发送消息进行通信。和 Actor 模型不同,CSP 模型关注的是消息发送的载体,即通道,而不是发送消息的执行实体。关于 CSP 模型的更进一步的介绍,有兴趣的同学可以阅读论文 Communicating Sequential Processes,Go 语言的并发模型参考了 CSP 理论,其中执行实体对应的是 goroutine, 消息通道对应的就是 channel。

    channel 介绍

    channel 提供了一种通信机制,通过它,一个 goroutine 可以想另一 goroutine 发送消息。channel 本身还需关联了一个类型,也就是 channel 可以发送数据的类型。例如: 发送int 类型消息的 channel 写作 chan int

    channel 创建

    channel 使用内置的 make 函数创建,下面声明了一个 chan int 类型的 channel:
    ch := make(chan int)

    channel和 hash map 类似,make 创建了一个底层数据结构的引用,当赋值或参数传递时,只是拷贝了一个 channel 引用,指向相同的 channel 对象。和其他引用类型一样,channel 的空值为 nil 。使用 == 可以对类型相同的 channel 进行比较,只有指向相同对象或同为 nil 时,才返回 true

    channel 的读写操作

    ch := make(chan int)
    
    // write to channel
    ch <- x
    
    // read from channel
    x <- ch
    
    // another way to read
    x = <- ch
    

    channel 一定要初始化后才能进行读写操作,否则会永久阻塞。

    关闭 channel

    golang 提供了内置的 close 函数对 channel 进行关闭操作。

    ch := make(chan int)
    
    close(ch)
    

    有关 channel 的关闭,你需要注意以下事项:

    • 关闭一个未初始化(nil) 的 channel 会产生 panic
    • 重复关闭同一个 channel 会产生 panic
    • 向一个已关闭的 channel 中发送消息会产生 panic
    • 从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
    • 关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息。

    Panic是Go语言内置的错误类型。

    ch := make(chan int, 10)
    ch <- 11
    ch <- 12
    
    close(ch)
    
    for x := range ch {
        fmt.Println(x)
    }
    
    x, ok := <- ch
    fmt.Println(x, ok)
    
    
    -----
    output:
    
    11
    12
    0 false
    

    channel 的类型

    channel 分为不带缓存的 channel带缓存的 channel。

    无缓存的 channel

    从无缓存的 channel 中读取消息会阻塞,直到有 goroutine 向该 channel 中发送消息;同理,向无缓存的 channel 中发送消息也会阻塞,直到有 goroutine 从 channel 中读取消息。

    通过无缓存的 channel 进行通信时,接收者收到数据 happens before 发送者 goroutine 唤醒

    有缓存的 channel

    有缓存的 channel 的声明方式为指定 make 函数的第二个参数,该参数为 channel 缓存的容量
    ch := make(chan int, 10)

    有缓存的 channel 类似一个阻塞队列(采用环形数组实现)。当缓存未满时,向 channel 中发送消息时不会阻塞,当缓存满时,发送操作将被阻塞,直到有其他 goroutine 从中读取消息;相应的,当 channel 中消息不为空时,读取消息不会出现阻塞,当 channel 为空时,读取操作会造成阻塞,直到有 goroutine 向 channel 中写入消息。

    ch := make(chan int, 3)
    
    // blocked, read from empty buffered channel
    <- ch
    
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    ch <- 3
    
    // blocked, send to full buffered channel
    ch <- 4
    

    通过 len 函数可以获得 chan 中的元素个数,通过 cap 函数可以得到 channel 的缓存长度。
    channel 的用法
    goroutine 通信
    看一个 effective go 中的例子:

    c := make(chan int)  // Allocate a channel.
    
    // Start the sort in a goroutine; when it completes, signal on the channel.
    go func() {
        list.Sort()
        c <- 1  // Send a signal; value does not matter.
    }()
    
    doSomethingForAWhile()
    <-c
    

    主 goroutine 会阻塞,直到执行 sort 的 goroutine 完成。
    range 遍历
    channel 也可以使用 range 取值,并且会一直从 channel 中读取数据,直到有 goroutine 对改 channel 执行 close 操作,循环才会结束。

    // consumer worker
    ch := make(chan int, 10)
    for x := range ch{
        fmt.Println(x)
    }
    

    等价于

    for {
        x, ok := <- ch
        if !ok {
            break
        }
        
        fmt.Println(x)
    }
    

    配合 select 使用

    select 用法类似与 IO 多路复用,可以同时监听多个 channel 的消息状态,看下面的例子

    select {
        case <- ch1:
        ...
        case <- ch2:
        ...
        case ch3 <- 10;
        ...
        default:
        ...
    }
    

    select 可以同时监听多个 channel 的写入或读取
    执行 select 时,若只有一个 case 通过(不阻塞),则执行这个 case 块
    若有多个 case 通过,则随机挑选一个 case 执行
    若所有 case 均阻塞,且定义了 default 模块,则执行 default 模块。若未定义 default 模块,则 select 语句阻塞,直到有 case 被唤醒。
    使用 break 会跳出 select 块。

    1. 设置超时时间
    ch := make(chan struct{})
    
    // finish task while send msg to ch
    go doTask(ch)
    
    timeout := time.After(5 * time.Second)
    select {
        case <- ch:
            fmt.Println("task finished.")
        case <- timeout:
            fmt.Println("task timeout.")
    }
    
    1. quite channel
      有一些场景中,一些 worker goroutine 需要一直循环处理信息,直到收到 quit 信号
    msgCh := make(chan struct{})
    quitCh := make(chan struct{})
    for {
        select {
        case <- msgCh:
            doWork()
        case <- quitCh:
            finish()
            return
    }
    

    单向 channel

    即只可写入或只可读的channel,事实上 channel 只读或只写都没有意义,所谓的单向 channel 其实知识声明时用,比如
    func foo(ch chan<- int) <-chan int {...}

    chan<- int 表示一个只可写入的 channel,<-chan int 表示一个只可读取的 channel。上面这个函数约定了 foo 内只能从向 ch 中写入数据,返回只一个只能读取的 channel,虽然使用普通的 channel 也没有问题,但这样在方法声明时约定可以防止 channel 被滥用,这种预防机制发生在编译期间。



  • Go的相关学习资源



  • 国内使用Go语言的小坑

    由于Go是Google开发的,但Google的全部服务在中国因为众所周知的原因无法访问,因此有部分准标准库无法正常下载,常导致第三方包构建失败。但Go又是使用命令行工具管理package,常用代理软件是无法代理终端的。这时候就需要手动设置http_proxy和https_proxy环境变量了。
    win平台:
    set http_proxy=127.0.0.1:1080
    set https_proxy=127.0.0.1:1080
    go get -u -v xxxx/xxxx/xxxxx
    linux平台:
    export http_proxy=127.0.0.1:1080
    export https_proxy=127.0.0.1:1080
    go get -u -v xxxx/xxx/xxxxx



  • 666 打算入坑,有机会以后交流一波~



  • @realnumber hhh,欢迎新gopher入坑



  • @arktische 貌似我们组(悦然心动)也有产品是用golang写的,但至今只接触了js的代码,今年要接触新产品,感觉马上就要入坑了!!


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待