V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
lcj2class
V2EX  ›  Go 编程语言

写给新手的 Go 开发指南

  •  2
     
  •   lcj2class · 2019-07-22 14:29:05 +08:00 · 3731 次点击
    这是一个创建于 1981 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接

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

    https://golang.org/doc/ 其他入门问题,官网也有文档
    Foreverdxa
        27
    Foreverdxa  
       2019-07-23 12:10:41 +08:00 via Android
    go 不错,跟 JavaScript 一样好使
    richzhu
        28
    richzhu  
       2019-07-23 13:09:16 +08:00
    谢谢楼主大佬,看完有疑问,请问 `var _ blob.Fetcher = (*CachingFetcher)(nil)` 是什么意思,实在看不懂,可以解释一下嘛?
    lcj2class
        29
    lcj2class  
    OP
       2019-07-30 12:14:40 +08:00
    @richzhu #28 https://golang.org/doc/faq#guarantee_satisfies_interface 可以看看这个解释
    其实就是在做类型转化时,通过强制指定类型,来判断是否实现了某个接口
    T3RRY
        30
    T3RRY  
       2019-08-02 11:47:40 +08:00
    +1
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1041 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 19:45 · PVG 03:45 · LAX 11:45 · JFK 14:45
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.