The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
lcj2class

写给新手的 Go 开发指南

  •  2
     
  •   lcj2class · Jul 22, 2019 · 4525 views
    This topic created in 2506 days ago, the information mentioned may be changed or developed.

    原文链接

    转眼加入蚂蚁已经三个多月,这期间主要维护一 Go 写的服务器。虽然用的时间不算长,但还是积累了一些心得体会,这里总结归纳一下,供想尝试 Go 的同学参考。 本文首先会介绍 Go 设计理念,然后是开发环境,最后是语言特性。

    简介

    一般来说,编程语言都会有一个 slogan 来表示它们的特点。比如提到 Clojure,一般会想到这么几个词汇:lisp on JVM、immutable、persistent ; Java 的话我能想到的是企业级开发、中规中矩。对于 Go,官网介绍到:

    Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.

    提取几个关键词:open (开放)、simple (简洁)、reliable (可靠)、efficient (高效)。这也可以说是它的设计目标。除了上面这些口号外,初学者还需要知道 Go 是一门命令式的静态语言(是指在编译时检查变量类型是否匹配),与 Java 属于同一类别。

    Imperative Functional
    Dynamic Python/Ruby/Javascript Lisp/Scheme/Clojure
    Static Java/C++/Rust/Go OCaml/Scala/Haskell

    由于 Hello World 太简洁,不具备展示 Go 的特点,所以下面展示一段访问 httpbin,打印 response 的完整代码。

    package main
    
    import (
        "fmt"
        "io/ioutil"
        "net/http"
    )
    
    func main() {
        // http://httpbin.org/#/Anything/get_anything
        r, err := http.Get("http://httpbin.org/anything?hello=world")
        if err != nil {
            panic(err)
        }
        defer resp.Body.Close()
    
        body, err := ioutil.ReadAll(r.Body)
        if err != nil {
            panic(err)
        }
        fmt.Printf("body = %s\n", string(body))
    }
    
    

    上面的代码片段包括了 Go 的主要组成:包的声明与引用、函数定义、错误处理、流程控制、defer

    开发环境

    通过上面的代码片段,可以看出 Go 语言 simple (简洁)的特点,所以找一个最熟悉的文本编辑器,一般通过配置插件,都可以达到快速开发的目的。很久之前我就已经把所有文本编辑放到 Emacs 上,这里介绍下我的配置。

    除了 go-mode 这个 major mode,为了配置像 源码跳转、API 自动补全、查看函数文档等现代 IDE 必备功能,需要安装以下命令

    
    go get -u github.com/rogpeppe/godef
    go get -u github.com/nsf/gocode # for go-eldoc/company-go
    go get -u golang.org/x/tools/cmd/goimports
    go get -u github.com/kisielk/errcheck
    go get -u github.com/lukehoban/go-outline # for go-imenu
    

    然后再按照 setup-go.el 里的配置,就拥有了一个功能完备的开发环境。

    Emacs Go 开发环境

    不像 Java 语言需要运行时,Go 支持直接将整个项目 build 成一个二进制文件,方便部署,而支持交叉编译,不过在开发时,直接 go run XXX.go 更为便利,截止到 Go 1.12 ,还不支持 REPL,官方有提供在线版的 Playground 供分享、调试代码。

    我个人的习惯是建一个 go-app 项目,每个要测试的逻辑放到一个 test 里面去,这样就可以使用 go test -v -run XXX 来运行。之所以不选用 go run,是因为一个目录下只允许有一个 main 的 package,多个 IDE 会提示错误。

    数据类型

    一般编程语言,数据类型分为基本的与复杂的两类。 基本的一般比较简单,表示一个值,Go 里面就有 string, bool, int8, int32(rune), int64, float32, float64, byte(uint8) 等基本类型 复杂类型一般表示多个值或具有某些高级用法,Go 里面有:

    • pointer Go 里只支持取地址 & 与间接访问 * 操作符,不支持对指针进行算术操作
    • struct 类似于 C 语言里面的 struct,Java 里面的对象
    • function 函数在 Go 里是一等成员
    • array 大小固定的数组
    • slice 动态的数组
    • map 哈希表
    • chan 用于在多个 goroutine 内通信
    • interface 类似于 Java 里面的接口,但是与 Java 里的用法不一样

    下面将重点介绍 Go 里特有或用途最广的数据类型。

    struct/interface

    Go 里面的 struct 类似于 Java 里面的 Object,但是并没有继承,仅仅是对数据的一层包装(抽象)。相对于其他复杂类型,struct 是值类型,也就是说作为函数参数或返回值时,会拷贝一份值,值类型分配在 stack 上,与之相对的引用类型,分配在 heap 上。 初学者一般会有这样的误区,认为传值比传引用要慢,实则不然,具体涉及到 Go 如何管理内存,这里暂不详述,感兴趣到可以阅读:

    BenchmarkByPointer-8    20000000                86.7 ns/op
    BenchmarkByValue-8      50000000                31.9 ns/op
    

    所以一般推荐直接使用值类型的 struct,如果确认这是瓶颈了,可以再尝试改为引用类型(&struct )

    如果说 struct 是对状态的封装,那么 interface 就是对行为的封装,相当于对外的契约( contract )。而且 Go 里面有这么一条最佳实践

    Accept interfaces, return concrete structs. (函数的参数尽量为 interface,返回值为 struct )

    这样的好处也很明显,作为类库的设计者,对其要求的参数尽量宽松,方便使用,返回具体值方便后续的操作处理。一个极端的情况,可以用 interface{} 表示任意类型的参数,因为这个接口里面没有任何行为,所以所有类型都是符合的。又由于 Go 里面不支持范型,所以interface{}是唯一的解决手段。

    相比较 Java 这类面向对象的语言,接口需要显式( explicit )继承(使用 implements 关键字),而在 Go 里面是隐式的( implicit ),新手往往需要一段时间来体会这一做法的巧妙,这里举一例子来说明:

    Go 的 IO 操作涉及到两个基础类型:Writer/Reader,其定义如下:

    type Reader interface {
            Read(p []byte) (n int, err error)
    }
    
    type Writer interface {
            Write(p []byte) (n int, err error)
    }
    

    自定义类型如果实现了这两个方法,那么就实现了这两个接口,下面的 Example 就是这么一个例子:

    type Example struct {
    }
    func (e *Example) Write(p byte[]) (n int, err error) {
    }
    func (e *Example) Read(p byte[]) (n int, err error) {
    }
    

    由于隐式继承过于灵活,在 Go 里面可能会看到如下代码

    var _ blob.Fetcher = (*CachingFetcher)(nil)
    

    这是通过将 nil 强转为 *CachingFetcher,然后在赋值时,指定 blob.Fetcher 类型,保证 *CachingFetcher 实现了 blob.Fetcher 接口。这在重构项目代码时非常有用。

    map/slice

    Map/Slice 是 Go 里面最常用的两类数据结构,属于引用类型。 slice 是长度不固定的数组,类似于 Java 里面的 List

    // map 通过 make 进行初始化
    // 如果提前知道 m 大小,建议通过 make 的第二个参数指定,避免后期的数据移动、复制
    m := make(map[string]string, 10)
    // 赋值
    m["zhangsan"] = "teacher"
    // 读取指定值,如不存在,返回其类型的默认值
    v := m["zhangsan"]
    // 判断指定 key 知否在 map 内
    v, ok := m["zhangsan"]
    
    // slice 通过 make 进行初始化
    s := make([]int)
    // 增加元素
    s = append(s, 1)
    
    // 也可以通过 make 第二个参数指定大小
    s := make([]int, 10)
    for i:=0;i<10;i++ {
        s[i] = i
    }
    // 也可以使用三个参数的 make 初始化 slice
    // 第二个参数为初始化大小,第三个为最大容量
    // 需要通过 append 增加元素
    s := make([]int, 0 ,10)
    s = append(s, 1)
    

    chan/goroutine

    作为一门新语言,Goroutine 是 Go 提出的并发解决方案,相比传统 OS 级别的线程,它有以下特点

    1. 轻量,完全在用户态调度(不涉及 OS 状态直接的转化)
    2. 资源占用少,启动快
    3. 目前,Goroutine 调度器不保证公平( fairness ),抢占( pre-emption )也支持的非常有限,一个空的 for{} 可能会一直不被调度出去。

    一般可以使用 chan/select 来进行 Goroutine 之间的调度。chan 类似于 Java 里面的 BlockingQueue,且能保证 Goroutine-safe,也就是说多个 Goroutine 并发进行读写是安全的。

    chan 里面的元素默认为 1 个,也可以在创建时指定缓冲区大小,读写支持堵塞、非堵塞两种模式,关闭一个 chan 后,再写数据时会 panic。

    // chan 与 slice/map 一样,使用 make 初始化
    ch := make(chan int, 2)
    
    // blocking read
    v := <-ch
    // nonblocking read 当 ch 内没有数据或已经被关闭时,ok 为 false
    v, ok := <-ch
    // blocking write
    ch <- v
    // nonblocking write, 需要注意 default 分支不能省略,否则会堵塞住
    select {
        case ch<-v:
        default:
    }
    

    chan 作为 Go 内一重要数据类型,看似简单,实则暗藏玄妙,用时需要多加留意,这里不再展开叙述,后面打算专门写一篇文章去介绍,感兴趣的可以阅读下面的文章:

    • Curious Channels
    • Prosumer 基于 buffered chan 实现的生产者消费者,核心点在于关闭 chan 只意味着生产者不能再发送数据,消费者无法获知 chan 是否已经关闭,需要用其他方式去通信。

    语言特性

    Go 相比 Java 来说,语言特性真的是少太多。推荐 Learn X in Y minutes 这个网站,快速浏览一遍即可掌握 Go 的语法。Go 的简洁程度觉得和 JavaScript 差不多,但却是一门静态语言,具有强类型,这两点又让它区别于一般的脚本语言。

    错误处理

    Go 内没有 try catch 机制,而且已经明确拒绝了这个 Proposal,而是通过返回值的方式来处理。

    f, err := os.Open(filename)
    if err != nil {
        return …, err  // zero values for other results, if any
    }
    

    Go 的函数一般通过返回多值的方式来传递 error (且一般是第二个位置),实际项目中一般使用 pkg/errors 去处理、包装 err。

    依赖管理

    Go 的依赖管理,相比其他语言较弱。 在 Go 1.11 正式引入的 modules 之前,项目必须放在 $GOPATH/src/xxx.com/username/project 内,这样 Go 才能去正确解析项目依赖,而且 Go 社区没有统一的包托管平台,不像 Java 中 maven 一样有中央仓库的概念,而是直接引用 Git 的库地址,所以在 Go 里,一般会使用 github.com/username/package 的方式来表示。 go get 是下载依赖但命令,但一个个去 get 库不仅仅繁碎,而且无法固化依赖版本信息,所以 dep 应运而生,添加新依赖后,直接运行 dep ensure 就可以全部下下来,而且会把当前依赖的 commit id 记录到 Gopkg.lock 里面,这就能解决版本不固定的问题。

    但 modules 才是正路,且在 1.13 版本会默认开启,所以这里只介绍它的用法。

    # 首先导出环境变量
    export GO111MODULE=on
    # 在一个空文件夹执行 init,创建一个名为 hello 的项目
    go mod init hello
    # 这时会在当前文件夹内创建 go.mod ,内容为
    
    module hello
    
    go 1.12
    # 之后就可以编写 Go 文件,添加依赖后,执行 go run/
    # 依赖会自动下载,并记录在 go.mod 内,版本信息记录在 go.sum
    

    更多用法可以参考官方示例,这里只是想说明目前 Go 内的工具链大部分已经支持,但是 godoc 还不支持

    GC

    Go 也是具有垃圾回收的语言,但相比于 JVM,Go GC 可能显得及其简单,从 Go 1.10 开始,Go GC 采用 Concurrent Mark & Sweep (CMS) 算法,且不具有分代、compact 特性。读者如果对相关名词不熟悉,可以阅读:

    而且 Go 里面调整 GC 的参数只有一个 GOGC,表示下面的比率

    新分配对象 / 上次 GC 后剩余对象

    默认 100,表示新分配对象达到之前剩余对象大小时,进行 GC。GOGC=off 可以关闭 GC,SetGCPercent 可以动态修改这个比率。

    在启动一个 Go 程序时,可以设置 GODEBUG=gctrace=1 来打印 GC 日志,日志具体含义可参考 pkg/runtime,这里不再赘述。对调试感兴趣的可以阅读:

    总结

    Go 最初由 Google 在 2007 为解决软件复杂度、提升开发效率的一试验品,到如今不过十二年,但无疑已经家喻户晓,成为云时代的首选。其面向接口的特有编程方式,也非常灵活,兼具动态语言的简洁与静态语言的高效,推荐大家尝试一下。Go Go Go!

    Go

    扩展阅读

    30 replies    2019-08-02 11:47:40 +08:00
    Mistwave
        1
    Mistwave  
       Jul 22, 2019 via iPhone   ❤️ 8
    人家是 open source,你直接给整 open 了
    d5
        2
    d5  
       Jul 22, 2019
    @Mistwave 一楼有点精辟
    Vegetable
        3
    Vegetable  
       Jul 22, 2019
    @Mistwave 你怎么看待 Golang 自称是一门开放的语言?
    laravel
        4
    laravel  
       Jul 22, 2019
    就喜欢看有人夸 go 语言的
    Taigacute
        5
    Taigacute  
       Jul 22, 2019   ❤️ 1
    这种文章...入门又不够细 深入的干货又没有。。挺尴尬的。真的想写个 tutorial 就开找个地方开系列。
    linKnowEasy
        6
    linKnowEasy  
       Jul 22, 2019
    @Mistwave #1 哈哈哈哈哈, 快乐源泉 + 1
    Taigacute
        7
    Taigacute  
       Jul 22, 2019   ❤️ 1
    还有啊 nsf/gocode 是哪个年代? 早都不维护了啊。你在 go 1.10 以下还行。
    loading
        8
    loading  
       Jul 22, 2019 via Android
    @Taigacute 现在 go 编辑完代码保存时自动重新 go run 是用哪个方法了,方法太多了,想知道大佬的最佳实践。
    www5070504
        9
    www5070504  
       Jul 22, 2019
    复杂类型直接说容器类型不就行了么。。。open 真的是承包笑点了 hhhh
    Taigacute
        10
    Taigacute  
       Jul 22, 2019   ❤️ 1
    @loading 你是想要的热更新吧?
    loading
        11
    loading  
       Jul 22, 2019 via Android
    @Taigacute 我不太理解热更新,我是指开发阶段调试的时候,那个也叫热更新?
    crayygy
        12
    crayygy  
       Jul 22, 2019
    @loading #11 意思是 hot reload 吧?不过我不熟悉 Go server 开发,不知道这个要怎么实现
    ArJun
        13
    ArJun  
       Jul 22, 2019
    go 要是能像 java 一样普及就好了
    altboy
        14
    altboy  
       Jul 22, 2019
    讲个 go 入门,不明白为啥要介绍自己的 IDE 配置😓
    scofieldpeng
        15
    scofieldpeng  
       Jul 22, 2019   ❤️ 1
    作为一个写了 4 年 go,也快速培训过一些其他语言转 go 的,看到你的文章很尴尬,如果我用你的文章来给“新手”讲,估计他根本都不想学 go 了
    zzlettle
        16
    zzlettle  
       Jul 22, 2019 via iPad
    我觉得 ok
    不过这个知识点没看明白
    var _ blob.Fetcher = (*CachingFetcher)(nil)
    能详细解释下吗
    xcaptain
        17
    xcaptain  
       Jul 22, 2019
    我最近打算用 wire 重构我的一个 api,但是网上找不到很多这方面的例子,作者如果有心可以研究下如何用 wire 实现一个带 DI 的服务,我现在感觉手动依赖注入和自动依赖注入没太大差别
    Taigacute
        18
    Taigacute  
       Jul 22, 2019   ❤️ 1
    @loading 就是 reload reupdate .也能这么叫。我自己写了个包 监测文件改动。
    hmxxmh
        19
    hmxxmh  
       Jul 22, 2019 via Android
    楼上这么多说尴尬的,能不能出个教程😃,刚入门 go 的小白一枚。最近在看采集器……
    pzzrudlf
        20
    pzzrudlf  
       Jul 22, 2019 via Android
    @scofieldpeng 大佬的 经历深刻
    goodspb
        22
    goodspb  
       Jul 23, 2019
    楼主博客的主题是啥?分享一下
    ruin2016
        23
    ruin2016  
       Jul 23, 2019
    支持下楼主, github 已 star., 内容对于我这种初学者来说挺不错,对应的案例都会敲一次,在理解一次。感谢为 GO 在国内的普及做的努力.
    xmai
        24
    xmai  
       Jul 23, 2019   ❤️ 1
    unicloud
        25
    unicloud  
       Jul 23, 2019 via iPhone
    我觉得楼主写得很好。那些说尴尬的,每个人的经验和认知不一样,感谢楼主的分享。
    reus
        26
    reus  
       Jul 23, 2019
    @hmxxmh https://tour.golang.org/welcome/1 官方的 A tour of Go 就很好

    https://golang.org/doc/ 其他入门问题,官网也有文档
    Foreverdxa
        27
    Foreverdxa  
       Jul 23, 2019 via Android
    go 不错,跟 JavaScript 一样好使
    richzhu
        28
    richzhu  
       Jul 23, 2019
    谢谢楼主大佬,看完有疑问,请问 `var _ blob.Fetcher = (*CachingFetcher)(nil)` 是什么意思,实在看不懂,可以解释一下嘛?
    lcj2class
        29
    lcj2class  
    OP
       Jul 30, 2019
    @richzhu #28 https://golang.org/doc/faq#guarantee_satisfies_interface 可以看看这个解释
    其实就是在做类型转化时,通过强制指定类型,来判断是否实现了某个接口
    T3RRY
        30
    T3RRY  
       Aug 2, 2019
    +1
    About   ·   Help   ·   Advertise   ·   Blog   ·   API   ·   FAQ   ·   Solana   ·   3643 Online   Highest 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 5414617a · 628ms · UTC 00:40 · PVG 08:40 · LAX 17:40 · JFK 20:40
    ♥ Do have faith in what you're doing.