《Go语言实战》 - 1~3

第1章 关于 Go 语言的介绍

编译

编译Go程序时,编译器只会关注那些直接被引用的库。

JiaJia: 这是不是意味着 Go 不支持反射? Go 语言有提供反射功能的 reflect 包。 => Go 语言如何保证反射的包会被编译?

goroutine

Go 语言对并发的支持是这门语言最重要的特性之一。 goroutine 很像线程,但是它占用的内存远少于线程,使用它需要的代码更少。
goroutine 是可以与其它 goroutine 并行执行的函数,同时也会与主程序(程序的入口)并行执行。

func log(msg string) {
    ...
}

go log('发生了可怕的事情')
  

通道

通道(channel)是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。
需要强调的是,通道并不提供跨 goroutine 的数据访问保护机制。
如果读和写是由不同的 goroutine 完成的,每个 goroutine 依旧需要额外的同步动作。

Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型,就能复用所有的功能。

接口

接口用于描述类型的行为。如果一个类型的实例实现了一个接口,意味着这个实例可以执行一组特定的行为。
你甚至不需要去声明这个实例实现某个接口,只需要实现这组行为就好。
这和传统的面向对象编程语言的接口系统有本质的区别。
Go 语言的接口更小,只倾向于定义一个单一的动作。

JiaJia:会不会导致出现接口定义相同,但是其功能完全不同的情况?

你好,Go

package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
  

小结:

  • Go 语言是现代的、快速的,带有一个强大的标准库。
  • Go 语言内置对并发的支持。
  • Go 语言使用接口作为代码复用的基础模块。

第2章 快速开始一个 Go 程序

示例代码:

git clone https://github.com/goinaction/code.git
  

main 包

package main

import (
    "log"
    "os"
    _ "./matchers"
    "./search"s
)

// init is called prior to main.
func init() {
    // Change the device for logging to stdout.
    log.SetOutput(os.Stdout)
}

// main is the entry point for the program.
func main() {
    // Perform the search for the specified term.
    search.Run("president")
}
  

如果 main 函数不在 main 包里,构建工具就不会生成可执行的文件。

_ "./matchers" 是为了让 Go 语言对包做初始化操作,但是并不使用包里的标识符。
为了让程序的可读性更强,Go 编译器不允许声明导入某个包却不使用。
下划线让编译器接收这类导入,并且调用对应包内的所有代码文件里定义的 init 函数。

程序中每个代码文件里的 init 函数都会在 main 函数执行前调用。

search 包

package search

import (
    "log"
    "sync"
)

// A map of registered matchers for searching.
var matchers = make(map[string]Matcher)

// Run performs the search logic.
func Run(searchTerm string) {
    // Retrieve the list of feeds to search through.
    feeds, err := RetrieveFeeds()
    if err != nil {
        log.Fatal(err)
    }

    // Create an unbuffered channel to receive match results to display.
    results := make(chan *Result)

    // Setup a wait group so we can process all the feeds.
    var waitGroup sync.WaitGroup

    // Set the number of goroutines we need to wait for while
    // they process the individual feeds.
    waitGroup.Add(len(feeds))

    // Launch a goroutine for each feed to find the results.
    for _, feed := range feeds {
        // Retrieve a matcher for the search.
        matcher, exists := matchers[feed.Type]
        if !exists {
            matcher = matchers["default"]
        }

        // Launch the goroutine to perform the search.
        go func(matcher Matcher, feed *Feed) {
            Match(matcher, feed, searchTerm, results)
            waitGroup.Done()
        }(matcher, feed)
    }

    // Launch a goroutine to monitor when all the work is done.
    go func() {
        // Wait for everything to be processed.
        waitGroup.Wait()

        // Close the channel to signal to the Display
        // function that we can exit the program.
        close(results)
    }()

    // Start displaying results as they are available and
    // return after the final result is displayed.
    Display(results)
}

// Register is called to register a matcher for use by the program.
func Register(feedType string, matcher Matcher) {
    if _, exists := matchers[feedType]; exists {
        log.Fatalln(feedType, "Matcher already registered")
    }

    log.Println("Register", feedType, "matcher")
    matchers[feedType] = matcher
}
  

与第三方不同,从标准库中导入代码时,只需要给出要导入的包名。
编译器查找包的时候,总是会到 GOROOTGOPATH 环境变量引用的位置去查找。

没有定义在任何函数作用域内的变量,会被当作包级变量

当代码导入一个包时,程序可以直接访问这个包中任意一个公开的标识符(以大写字母开头)。
以小写字母开头的标识符是不公开的,不能被其它包中的代码直接访问。
但是,其它包可以间接访问不公开的标识符。例如,一个函数可以返回一个未公开类型的值,那么这个函数的任何调用者,哪怕调用者不是在这个包里声明的,都可以访问这个值。

JiaJia: 在 Java 中,不公开的类型在外部是无法访问的。虽然通过 lombokvar 关键字在 IDE 中不会报错,但是编译时仍然会报 无法从外部程序包中对其进行访问 的错误。

在 Go 语言中,所有变量都被初始化为其零值

  • 数值类型:0
  • 字符串类型:空字符串
  • 布尔类型:false
  • 指针:nil
  • 引用类型:所引用的底层数据结构会被初始化为对应的零值,但是,被声明为其零值的引用类型的变量,会返回 nil 作为其值

Go 语言使用关键字 func 声明函数,关键字后面紧跟着函数名、参数及返回值。

简化变量声明运算符:= 。这个运算符用于声明一个变量,同时给这个变量赋予初始值。

根据经验,如果需要声明初始值为零值的变量,应该使用 var 关键字声明变量;
如果提供确切的非零值初始化变量或者使用函数返回值创建变量,应该使用简化变量声明运算符。

这个程序使用 sync 包的 WaitGroup 跟踪所有启动的 goroutine

JiaJia: 类似于 Java 中的 CountDownLatch

关键字 range 可以用于迭代组、字符串、切片、映射和通道。
使用 for range 迭代切片时,每次迭代会返回两个值。
第一个值是迭代的元素在切片的索引位置;
第二个值是元素值的一个副本。

下划线标识符的作用是占位符。
如果要调用的函数返回多个值,而又不需要其中的某个值,就可以用下划线占位符将其忽略。

使用 go 关键字启动一个 goroutine ,并对这个 goroutine 做并发调度。

匿名函数是指没有明确声明名字的函数。

指针变量可以方便地在函数之间共享数据。

在 Go 语言中,所有的变量都以值的方式传递。

Go 语言支持闭包
因为闭包,函数可以直接访问那些没有作为参数传入的变量。
匿名函数并没有拿到这些变量的副本,而是直接访问外层函数作用域中声明的这些变量本身。

feed.go

package search

import (
    "encoding/json"
    "os"
)

const dataFile = "data/data.json"

// Feed contains information we need to process a feed.
type Feed struct {
    Name string `json:"site"`
    URI  string `json:"link"`
    Type string `json:"type"`
}

// RetrieveFeeds reads and unmarshals the feed data file.
func RetrieveFeeds() ([]*Feed, error) {
    // Open the file.
    file, err := os.Open(dataFile)
    if err != nil {
        return nil, err
    }

    // Schedule the file to be closed once
    // the function returns.
    defer file.Close()

    // Decode the file into a slice of pointers
    // to Feed values.
    var feeds []*Feed
    err = json.NewDecoder(file).Decode(&feeds)

    // We don't need to check for errors, the caller can do this.
    return feeds, err
}
  

因为 Go 编译器可以根据赋值运算符右边的值来推导类型,声明常量的时候不需要指定类型。

结构类型
每个字段的声明最后 ` 引号里的部分被称作标记(tag)。

返回 error 类型值来表示函数是否调用成功,这种用法很常见。

关键字 defer 会安排随后的函数调用在函数返回时才执行。
关键字 defer 可以缩短打开文件和关闭文件之间间隔的代码行数,有助提高代码可读性,减少错误。

JiaJia:类似于 Java 中的 finally

interface{} 类型的参数可以接收任何类型的值。
interface{} 类型在 Go 语言里很特殊,一般会配合 reflect 包里提供的反射功能一起使用。

match.go

package search

import (
    "log"
)

// Result contains the result of a search.
type Result struct {
    Field   string
    Content string
}

// Matcher defines the behavior required by types that want
// to implement a new search type.
type Matcher interface {
    Search(feed *Feed, searchTerm string) ([]*Result, error)
}

// Match is launched as a goroutine for each individual feed to run
// searches concurrently.
func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
    // Perform the search against the specified matcher.
    searchResults, err := matcher.Search(feed, searchTerm)
    if err != nil {
        log.Println(err)
        return
    }

    // Write the results to the channel.
    for _, result := range searchResults {
        results <- result
    }
}

// Display writes results to the console window as they
// are received by the individual goroutines.
func Display(results chan *Result) {
    // The channel blocks until a result is written to the channel.
    // Once the channel is closed the for loop terminates.
    for result := range results {
        log.Printf("%s:\n%s\n\n", result.Field, result.Content)
    }
}
  
type Matcher interface {
  

使用 interface 关键字声明接口类型
一个接口的行为最终由这个接口类型中声明的方法决定。

按照 Go 语言的命名惯例,如果接口类型只包含一个方法,那么这个类型的名字以 er 结尾。
如果接口类型内部声明了多个方法,其名字需要与其行为关联。
如果要让一个用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型里声明的所有方法。

default.go

package search

// defaultMatcher implements the default matcher.
type defaultMatcher struct{}

// init registers the default matcher with the program.
func init() {
    var matcher defaultMatcher
    Register("default", matcher)
}

// Search implements the behavior for the default matcher.
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
    return nil, nil
}
  
type defaultMatcher struct{}
  

空结构再创建实例时,不会分配任何内存。这种结构很适合创建没有任何状态的类型。

func (m defaultMatcher) Search
  

如果声明函数的时候带有接收者,则意味着声明了一个方法。这个方法会和指定的接收者的类型绑在一起。

无论是使用接收者类型的值或者只想这个类型值的指针来调用这个方法,编译器都会正确地引用或者解引用对应的值。

因为大部分方法在调用后都需要维护接收者的值的状态,所以,一个最佳实践是,将方法的接收者声明为指针。

func (m *defaultMatcher) Search
  

与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大的不同

  • 使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时候被调用。
  • 使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。

JiaJia:这里感觉有些绕。不太能理解为什么会有这样的限制。
本来这里我类比下Java或者C#,值类型接收者的方法有点相当于静态方法,指针类型接收者的方法相当于实例方法。
通过接口类型的值调用时,跟这个情况比较吻合。
但是通过接收者的值或者指针都能访问指针类型接收者的方法,就有点违背这个逻辑了。
通过接收者的值访问指针类型接收者的方法时,相当于未创建实例却可以访问实例方法。
这个有点难以理解。
或者说这种情况下 Go 语言自动创建了实例?
如果是这样,那为什么通过接口类型的值调用时就不能自动创建实例了呢?
丢失了接口变量的类型?通过错误信息看貌似并没有。

rss.go

package matchers

import (
    "encoding/xml"
    "errors"
    "fmt"
    "log"
    "net/http"
    "regexp"

    "../search"
)

type (
    // item defines the fields associated with the item tag
    // in the rss document.
    item struct {
        XMLName     xml.Name `xml:"item"`
        PubDate     string   `xml:"pubDate"`
        Title       string   `xml:"title"`
        Description string   `xml:"description"`
        Link        string   `xml:"link"`
        GUID        string   `xml:"guid"`
        GeoRssPoint string   `xml:"georss:point"`
    }

    // image defines the fields associated with the image tag
    // in the rss document.
    image struct {
        XMLName xml.Name `xml:"image"`
        URL     string   `xml:"url"`
        Title   string   `xml:"title"`
        Link    string   `xml:"link"`
    }

    // channel defines the fields associated with the channel tag
    // in the rss document.
    channel struct {
        XMLName        xml.Name `xml:"channel"`
        Title          string   `xml:"title"`
        Description    string   `xml:"description"`
        Link           string   `xml:"link"`
        PubDate        string   `xml:"pubDate"`
        LastBuildDate  string   `xml:"lastBuildDate"`
        TTL            string   `xml:"ttl"`
        Language       string   `xml:"language"`
        ManagingEditor string   `xml:"managingEditor"`
        WebMaster      string   `xml:"webMaster"`
        Image          image    `xml:"image"`
        Item           []item   `xml:"item"`
    }

    // rssDocument defines the fields associated with the rss document.
    rssDocument struct {
        XMLName xml.Name `xml:"rss"`
        Channel channel  `xml:"channel"`
    }
)

// rssMatcher implements the Matcher interface.
type rssMatcher struct{}

// init registers the matcher with the program.
func init() {
    var matcher rssMatcher
    search.Register("rss", matcher)
}

// Search looks at the document for the specified search term.
func (m rssMatcher) Search(feed *search.Feed, searchTerm string) ([]*search.Result, error) {
    var results []*search.Result

    log.Printf("Search Feed Type[%s] Site[%s] For URI[%s]\n", feed.Type, feed.Name, feed.URI)

    // Retrieve the data to search.
    document, err := m.retrieve(feed)
    if err != nil {
        return nil, err
    }

    for _, channelItem := range document.Channel.Item {
        // Check the title for the search term.
        matched, err := regexp.MatchString(searchTerm, channelItem.Title)
        if err != nil {
            return nil, err
        }

        // If we found a match save the result.
        if matched {
            results = append(results, &search.Result{
                Field:   "Title",
                Content: channelItem.Title,
            })
        }

        // Check the description for the search term.
        matched, err = regexp.MatchString(searchTerm, channelItem.Description)
        if err != nil {
            return nil, err
        }

        // If we found a match save the result.
        if matched {
            results = append(results, &search.Result{
                Field:   "Description",
                Content: channelItem.Description,
            })
        }
    }

    return results, nil
}

// retrieve performs a HTTP Get request for the rss feed and decodes the results.
func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
    if feed.URI == "" {
        return nil, errors.New("No rss feed uri provided")
    }

    // Retrieve the rss feed document from the web.
    resp, err := http.Get(feed.URI)
    if err != nil {
        return nil, err
    }

    // Close the response once we return from the function.
    defer resp.Body.Close()

    // Check the status code for a 200 so we know we have received a
    // proper response.
    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
    }

    // Decode the rss feed document into our struct type.
    // We don't need to check for errors, the caller can do this.
    var document rssDocument
    err = xml.NewDecoder(resp.Body).Decode(&document)
    return &document, err
}
  

小结:

  • 每个代码文件都属于一个包,而包名应该与代码文件所在的文件夹同名。
  • Go 语言提供了多种声明和初始化变量的方式。如果变量的值没有显示初始化,编译器会将变量初始化为零值。
  • 使用指针可以在函数间或者 goroutine 间共享数据。
  • 通过启动 goroutine 和使用通道完成并发和同步。
  • Go 语言提供了内置函数来支持 Go 语言内部的数据结构。
  • 标准库包含很多包,能做很多很有用的事。
  • 使用 Go 接口编程可以通用的代码和框架。

第3章 打包和工具链

在 Go 语言中,包是个非常重要的概念。
其设计理念是使用包来封装不同语义单元的功能。

所有的 .go 文件,除了空行和注释,都应该在第一行声明自己所属的包。
每个包都在单独的目录里。
同一个目录下的所有 .go 文件必须声明同一个包名。

给包命名的惯例是使用包所在目录的名字。
给包及其目录命名时,应该使用简洁、清晰且全小写的名字。

main 包

在 Go 语言中,命名为 main 的包具有特殊的含义。
所有用 Go 语言编译的可执行程序都必须有一个名叫 main 的包。
main() 函数是程序的入口。
程序编译时,会使用声明为 main 包的代码所在的目录的目录名作为二进制可执行文件的文件名。

导入

使用 import 语句导入包。
编译器会使用 Go 环境变量设置的路径,通过引入的相对路径来查找磁盘上的包。
标准库中的包会在安装 Go 的位置找到。Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。
GOPATH 指定的这些目录就是开发者的个人工作空间。

一旦编译器找到一个满足 import 语句的包,就停止进一步的查找。
编译器会首先查找 Go 的安装目录,然后才会按顺序查找 GOPATH 变量里列出的目录。

Go 语言的工具链本身支持从 DVCS(Distributed Version Control Systems)网站获取源代码。
这个过程使用 go get 命令完成。

import "github.com/spf13/viper"
  

命名导入

导入的多个包具有相同的名字时,可以使用命名导入

import (
    "fmt"
    myfmt "mylib/fmt"
)
  

导入未在代码中使用的包时,可以使用空白标识符_)。

import (
    _ "fmt"
)
  

init 函数

所有被编译器发现的 init 函数都会安排在 main 函数之前执行。
init 函数用在设置包、初始化变量或者其它要在程序运行前优先完成的引导工作。

使用 Go 的工具

查看 Go 提供的命令:

PS C:\go\src\hello> go
Go is a tool for managing Go source code.

Usage:

        go <command> [arguments]

The commands are:

        bug         start a bug report
        build       compile packages and dependencies
        clean       remove object files and cached files
        doc         show documentation for package or symbol
        env         print Go environment information
        fix         update packages to use new APIs
        fmt         gofmt (reformat) package sources
        generate    generate Go files by processing source
        get         add dependencies to current module and install them
        install     compile and install packages and dependencies
        list        list packages or modules
        mod         module maintenance
        run         compile and run Go program
        test        test packages
        tool        run specified go tool
        version     print Go version
        vet         report likely mistakes in packages

Use "go help <command>" for more information about a command.

Additional help topics:

        buildmode   build modes
        c           calling between Go and C
        cache       build and test caching
        environment environment variables
        filetype    file types
        go.mod      the go.mod file
        gopath      GOPATH environment variable
        gopath-get  legacy GOPATH go get
        goproxy     module proxy protocol
        importpath  import path syntax
        modules     modules, module versions, and more
        module-get  module-aware go get
        module-auth module authentication using go.sum
        module-private module configuration for non-public modules
        packages    package lists and patterns
        testflag    testing flags
        testfunc    testing functions

Use "go help <topic>" for more information about that topic.
  

编译 & 清除

PS C:\go\src\hello> go build .\hello.go
PS C:\go\src\hello> go clean .\hello.go
  

大部分 Go 工具命令都会接受一个包名作为参数。

有些命令可以简写。如 build ,此时默认使用当前目录来编译。

PS C:\go\src\hello> go build
  

可以直接指定包,此时还可以使用通配符。三个点表示匹配所有字符串。

PS C:\go\src> go build .\github.com\goinaction\code\chapter3\wordcount
PS C:\go\src> go build .\github.com\goinaction\code\chapter3\wordcount\...
  

文件在当前目录时,可以使用段路径:文件名 或者 .

PS C:\go\src\github.com\goinaction\code\chapter3\wordcount> go build .\wordcount.go
PS C:\go\src\github.com\goinaction\code\chapter3\wordcount> go build .
  

编译和运行一起执行时可以使用 run 命令。

PS C:\go\src\hello> go run .\hello.go
Hello World!
  

go vet: 帮助开发人员检测代码的常见错误:

  • Printf 类函数调用时,类型匹配错误的参数;
  • 定义常用的方法时,方法签名的错误;
  • 错误的结构标签;
  • 没有指定字段名的结构字面量。

go fmt:自动格式指定的源代码并保存。

go doc:在终端上打印文档。

godoc:启动一个 Web 服务器,通过点击的方式查看 Go 语言的包的文档。支持开发者自己写的代码。

godoc -http:6060
  

代码文档规则:

  • 在标识符之前,把想要的文档作为注释加入到代码中。
  • 如果想给包写一段文字量比较大的文档,可以在工程里包含一个叫做 doc.go 的文件,使用同样的包名,并把包的介绍使用注释加在包声明之前。
  • 多行注释使用 \* *\ 符号。

创建代码库以分享:

  • 包应该在代码库的根目录中。
  • 包可以非常小。
  • 对代码执行 go fmt
  • 给代码写文档。

依赖管理

  • Keith Rarikgodep
  • Daniel Theophanesvendor
  • Gustavo Niemeyergopkg.in

godepvendor 使用第三方 verdoring 导入路径重写特性解决依赖问题。

gb

gb 是一个由 Go 社区成员开发的全新的构建工具。
gb 工程会区分开发人员写的代码和开发人员需要依赖的代码(第三方代码)。
gb 一个最好的特点是,不需要重写导入路径。
gb 工具首先会在 $PROJECT/srv/ 目录查找代码,如果找不到,会在 $PROJECT/vendor/src 目录里查找。
自己写的代码在工程目录的 src 目录中,第三方依赖代码在工程目录的 verdor/src 目录中。

gb 工程和 Go 官方工具链(包括 go get)并不兼容。

gb build all
  

更多 gb 特性可以访问 getgb.io

小结

  • 在 Go 语言中包是组织代码的基本单位。
  • 环境变量 GOPATH 决定了 Go 源代码在磁盘上被保存、编译和安装的位置。
  • 可以为每个工程设置不同的 GOPATH,以保持源代码和依赖的隔离。
  • go 工具是在命令行上工作的最好工具。
  • 开发人员可以使用 go get 来获取别人的包并将其安装到自己的 GOPATH 指定的目录。
  • 想要为别人创建包很简单,只要把源代码放到公用代码库,并遵守一些简单规则就可以。
  • Go 语言在设计时将分享代码作为语言的核心特性和驱动力。
  • 推荐使用依赖管理工具来管理依赖。
  • 由很多社区开发的依赖管理工具,如 godepvendorgb