第1章 Go语言的介绍

本章主要内容:用Go解决现代计算难题,使用 Go 语言工具。C 和 C++这类语言提供了很快的执行速度,而 Ruby 和 Python 这类语言则擅长快速开发。Go 语言在这两者间架起了桥梁,不仅提供了高性能的语言,同时也让开发更快速。

1.1.1 开发速度

Go 语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。编译 Go 程序时,编译器只会关注那些直接被引用的库,而不是像 Java、C 和 C++那样,要遍历依赖链中所有依赖的库。

1.1.2 并发

Go 语言对并发的支持是这门语言最重要的特性之一。goroutine 很像线程,但是它占用的内存远少于线程,使用它需要的代码更少。通道(channel)是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。这让编程模型更倾向于在 goroutine之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。

1.goroutine

图 1-2 在单一系统线程上执行多个 goroutine

goroutine 是可以与其他 goroutine 并行执行的函数,同时也会与主程序(程序的入口)并执行。在其他编程语言中,你需要用线程来完成同样的事情,而在 Go 语言中会使用同一个线程来执行多个 goroutine。如果想在执行一段代码的同时,并行去做另外一些事情,goroutine 是很好的选择。下面是一个简单的例子:

func log(msg string) {
    //这里是一些记录日志的代码
}
//代码里有些地方检测到了错误
go log("发生了可怕的事情")

2.通道

图 1-3 使用通道在 goroutine 之间安全地发送数据

通道是一种数据结构,可以让goroutine之间进行安全的数据通信。通道可以帮助用户避免其他语言里常见的共享内存访问的问题。通道这一模式保证同一时刻只会有一个goroutine修改数据。通道用于几个运行的goroutine之间发送数据。

1.1.3 Go 语言的类型系统

Go 语言提供了灵活的、无继承的类型系统,Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型,就能复用所有的功能。

1.类型简单

Go 开发者构建更小的类型——Customer 和 Admin,然后把这些小类型组合成更大的类型。图 1-4
展示了继承和组合之间的不同。

2.Go 接口对一组行为建模

图 1-4 继承和组合的对比

Go 语言的接口一般只会描述一个单一的动作。在 Go 语言中,最常使用的接口之一是 io.Reader 。这个接口提供了一个简单的方法,来声明一个类型有数据可以读取。标准库内的其他函数都能理解这个接口。这个接口的定义如下:

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

为了实现 io.Reader 这个接口,你只需要实现一个 Read 方法,这个方法接受一个 byte切片,返回一个整数和可能出现的错误。

1.1.4 内存管理

Go 语言把无趣的内存管理交给专业的编译器去做,而让程序员专注于更有趣的事情。

1.2 你好,Go

用Go语言编写经典的Hello World!应用程序:

package main

import "fmt"

func main() {

fmt.Printf("%s\n","Hello World!");

}

1.3 小结

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

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

本章主要内容:学习如何写一个复杂的 Go 程序;声明类型、变量、函数和方法;启动并同步操作 goroutine;使用接口写通用的代码;处理程序逻辑和错误。

通过一个完整的 Go 语言程序,来看看 Go 语言是如何实现一些功能的。这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,进行网络调用,解码 XML 和 JSON 成为结构化类型数据,并且利用 Go 语言的并发机制保证这些操作的速度。代码存放在这个代码库:
https://github.com/goinaction/code/tree/master/chapter2/sample

2.1 程序架

图 2-1 程序架构流程图

这个应用的代码使用了 4 个文件夹,按字母顺序列出。文件夹 data 中有一个 JSON 文档,其内容是程序要拉取和处理的数据源。文件夹 matchers 中包含程序里用于支持搜索不同数据源的代码。目前程序只完成了支持处理 RSS 类型的数据源的匹配器。文件夹 search 中包含使用不同匹配器进行搜索的业务逻辑,default.go用于搜索数据用的默认匹配器,feed.go用于读取 json 数据文件,
match.go用于支持不同匹配器的接口,search.go执行搜索的主控制逻辑。最后,父级文件夹 sample 中有个 main.go 文件,这是整个程序的入口。

2.2 main 包

main.go文件,每个可执行的 Go 程序都有两个明显的特征。一个特征是第 18 行声明的名为 main 的函数。
构建程序在构建可执行文件时,需要找到这个已经声明的 main 函数,把它作为程序的入口。第二个特征是程序的第 01 行的包名 main 。

package main

import (
    "log"
    "os"
    
    _ "github.com/goinaction/code/chapter2/sample/matchers"
     "github.com/goinaction/code/chapter2/sample/search"
)

//init在main之前调用
func init() {
    //将日志输出到标准输出
    log.SetOutput(os.Stdout)
}

//main是整个程序的入口
func main() {
    //使用特定的项做搜索
    search.Run("president")
}

Go 语言的每个代码文件都属于一个包,main.go 也不例外。包这个特性对于 Go 语言来说很重要,一个包定义一组编译过的代码,包的名字类似命名空间,可以用来间接访问包内声明的标识符。这个特性可以把不同包中定义的同名标识符区别开。

2.3 search 包

这个程序使用的框架和业务逻辑都在 search 包里。由于整个程序都围绕匹配器来运作,这个程序里的匹配器,是指包含特定信息、用于处理某类数据源的实例。在这个示例程序中有两个匹配器。框架本身实现了一个无法获取任何信息的默认匹配器,而在 matchers 包里实现了 RSS 匹配器。RSS匹配器知道如何获取、读入并查找 RSS 数据源。

2.3.1 search.go

package search

import (
        "log"
        "sync"
)

//注册用于搜索的匹配器的映射
var matchers = make(map[string]Matcher)

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

//Run执行搜索逻辑
func Run(searchTherm string) {
    //获取需要搜索的数据源列表

// 第一个返回值是一组 Feed 类型的切片。切片是一种实现了一个动态数组的引用类型。第二个返回值是一个错误值。
    feed, err := RetrieveFeeds()
    if err != nil {
            log.Fatal(err)
    }
    
    //创建一个无缓冲的通道,接收匹配后的结果
    results := make(chan *Result)
    
    //构造一个waitGroup,以便处理所有的数据源
    var waitGroup sync.waitGroup
    
    //设置需要等待处理
    //每个数据源的goroutine数量
    waitGroup.add(len(feeds))
    
    //为每一个数据源启动一个gorpuntine来查找结果
    for _, feed := range feeds {
            //获取一个匹配器用于查找
            matcher, exits := matchers[feed.Type]
            if !exits {
                    matcher = matchers["default"]
            }
            
            //启动一个gorountine来执行搜索
            go func(matcher Matcher, feed *Feed) {
                    Match(matcher, feed, searchTerm, results)
                    waitGroup.done()
            }(matcher, feed)
    }
    
    //启动一个gorountine来监控是否所有的工作都做完了
    go func() {
            //等待所有任务完成
            waitGroup.wait()
            
            //用关闭通道的方式,通知Display函数
            //可以退出程序了
            close(results)
    }()
    
    //启动函数,显示返回的结果,并且
    //在最后一个结果显示完成后返回
    Display(results)
}

//Register调用时,会注册一个匹配器,提供给后面的程序使用
func Register(feedType string, matcher Matcher) {
    if _, exists := matchers[feedType]; exists {
        log.Fatalln(feedType, "Matcher already registered")
    }
    
    log.Println("Register", feedType, "matcher")
    matcher[feedType] = matcher
}

在 Go 语言中,通道(channel)和映射(map)与切片(slice)一样,也是引用类型,不过通道本身实现的是一组带类型的值,这组值用于在 goroutine 之间传递数据。通道内置同步机制,从而保证通信安全。

这个程序使用 sync 包的 WaitGroup 跟踪所有启动的 goroutine。WaitGroup 是一个计数信号量,我们可以利用它来统计所有的
goroutine 是不是都完成了工作。

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

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

我们以 goroutine的方式启动了另一个匿名函数。这个匿名函数没有输入参数,使用闭包访问了 WaitGroup 和results 变量。这个 goroutine 里面调用了 WaitGroup 的 Wait 方法。这个方法会导致 goroutine阻塞,直到 WaitGroup 内部的计数到达 0。之后,goroutine 调用了内置的 close 函数,关闭了通道,最终导致程序终止。

2.3.2 feed.go

package search

import (
    "encoding/json"
    "os"
)

const dataFile = "data/data.json"

//Feed包含我们需要处理的数据源的信息
type Feed struct {
    Name string 'json:"site"'
    URI string 'json:"link"'
    type string 'json:"type"'
}

//RetrieveFeeds读取并反序列化源数据文件
func RetrieveFeeds() ([]*Feed, error) {
    //打开文件
    file, err := os.Open(dataFile)
    if err != nil {
        return nil, err
    }
    
    //当函数返回时
    //关闭文件
    defer file.Close()
    
    //将文件解码到一个切片里
    //这个切片的每一项是一个指向一个Feed类型值的指针
    var feeds []*feed
    err = json.NewDecoder(file).Decode(&feedss)
    
    //这个函数不需要检查错误,调用者会做这件事
    return feeds, err
}

2.3.3 match.go/default.go

search/match.go

package search

import (
    "log"
)

//Result 保存搜索的结果
type Result strut {
    Field string
    Content string
}

//Matcher定义要实现的
//新搜索类型的行为
type Matcher interface {
    Search(feed *Feed, searchTerm string ) ([]*Result, error)
}

//Match函数,为每个数据源单独启动goroutine来执行这个函数
// 并发地执行搜索
func Match(matcher Matcher, feed *Feed, searchTerm string, result chan<- *Result) {
    //对特定的疲累器执行搜索
    searchTerm, err := matcher.Search(feed, searchTerm)
    if err != nil {
        log.Println(err)
        return
    }
    
    //将结果写入通道
    for _, result := range searchTerm {
        result <- result
    }
}

//Display从每个单独的gorountine接收到结果后
//在终端窗口输出
func Dissplay(results chan *Resssult) {
    //通道会一直阻塞,直到有结果写入
    //一旦通道被关闭,for循环就会终止
    for result := range results {
        fmt.Printf("%s:\n%s\n\n", result.Field, result.Content)
    }
}

interface 关键字声明了一个接口,这个接口声明了结构类型或者具名类型需要实现的行为。一个接口的行为最终由在这个接口类型中声明的方法决定。如果要让一个用户定义的类型实现一个接口,这个用户定义的类型要实现接口类型里声明的所有方法。

search/default.go

package search

//defaultMatcer实现了默认匹配器
type defaultMatcher struct{}

// init函数将默认匹配器注册到程序里
func init() {
    var matcher defaultMatcher
    Register("default", matcher)
}

//Search 实现了默认匹配器的行为
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, err) {
    return nil, nil
}

如果声明函数的时候带有接收者,则意味着声明了一个方法。这个方法会和指定的接收者的类型绑在一起。在我们的例子里, Search 方法与 defaultMatcher 类型的值绑在一起。这意味着我们可以使用 defaultMatcher 类型的值或者指向这个类型值的指针来调用 Search 方法。无论我们是使用接收者类型的值来调用这个方,还是使用接收者类型值的指针来调用这个方法,编译器都会正确地引用或者解引用对应的值,作为接收者传递给 Search 方法。

调用方法的例子

//方法声明为使用defaultMatcher类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)

//声明一个指向defaultMatcher类型值的指针
dm := new(defaultMatch)

//编译器会解开dm指针的引用,使用对应的值调用方法
dm.Search(feed, "test"

//方法声明为使用指向defaultMatcher类型值的指针作为接收者
func (m ×defaultMatcher) Search(feed *Feed, searchTerm string)

//声明一个指向defaultMatcher类型的值
var dm defaultMatch

//编译器会自动生成指针引用dm值,使用指针调用方法
dm.Search(feed, "test")

与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大不同,使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时候被调用。使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。

接口方法调用所受限制的例子

//方法声明为使用指向defaultMatcher类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)

//通过interface类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = dm //将值赋值给接口类型
matcher.Search(feed, "test") //使用值来调用接口方法

> go build
cannot use dm (type defaultMatcher) as type Matcher in assignment

//方法声明为使用defaultMatcher类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)

//通过interface类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = &dm //将指针赋值给接口类型
matcher.Search(feed, "test") //使用指针来调用接口方法

> go build
Build Successful

2.4 RSS 匹配器

matchers/rss.go

package matchers

import (
    "encoding/xml"
    "errors"
    "fmt"
    "log"
    "net/http"
    "regexp"
    
    "github.com/goinaction/code/chapter2/sample/search"
)

type (
    //item根据item字段的标签,将定义的字段
    //与rss文挡的字段关联起来
    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根据image字段的标签,将定义的字段
    //与rss文档的字段关联起来
    image struct {
        XMLName xml.Name  ’xml:"image"‘
        URL     string    ’xml:"url"‘
        Title   string    ’xml:"title"‘
        Link    string    ’xml:"link"‘
    }
    
    //channel根据channel字段的标签,将定义的字段
    //与rss文档的字段关联起来
    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定义了与rss文档关联的字段
    rssDocument struct {
        XMLName xml.Name  ’xml:"rss"‘ 
        Channel channel   ’xml:"channel"‘
    }
)

//rssMatcher实现了Matcher接口
type rssMaster struct{}

//init将匹配器注册到程序里
func init() {
    var matcher rssMatcher
    search.Register("rss", matcher)
}

//Search在文档中查找特定的搜索项
func (m rssMatcher) Search(feed *search.Feed, searchTerm string)
                                                ([]*search.Result, error) {

//我们使用关键字 var 声明了一个值为 nil 的切片,切片每一项都是指向 Result 类型值的指针。
    var result []*search.Result
    log.Printf("Search Feed Type[%s] Site[%s] For Uri[%s]\n",
                                            feed.Type, feed.Name, feed.URI)
    //获取要搜索的数据
    document, err := m.retrieve(feed)
    if err != nil {
        return nil, err
    }
    
    for _, channelItem := range document.Channel.Item {
        //检查标题部分是否包含搜索项
        matched, err := regexp.MatchString(searchTerm, channelItem.Title)
        if err != nil {
            return nil, err
        }
        
        //如果找到匹配的项,将其作为结果保存
        if matched {
            results = append(results, &search.Result){
                Field: "Title",
                Content: channelItem.Tile,
            })
        }
        
        //检查描述部分是否包含搜索项
        matched, err = regexp.MatchString(searchTern, channelTerm.Description)
        if err != nil {
            return nil, err
        }
        
         //如果找到匹配的项, 将其作为结果保存
         if matched {
             results = append(results, &search.Result{
                 Field: "Description",
                 Content: channelItem.Description,
             })
         }
    }
    
    return results, nil
}

//retrieve发送HTTP Get请求获取rss数据源并解码
Func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
    if feed.URI == "" {
        return nil, errors.New("No rss feed URI provided")
    }
    
    //从网络获得rss数据源文档
    resp, err := http.Get(feed.URI)
    if err != nil {
        return nil, err
    }
    
    //一旦从函数返回,关闭返回的响应链接
    defer resp.Body.Close()
    
    //检查状态码是不是200,这样就知道
    //是不是收到了正确的响应
    if resp.StatusCode != 200 {
        return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
    }
    
    //将rss数据源文档解码到我们定义的结构类型里
    //不需要检查错误,调用者会做这件事
    var document rssDocument
    err = xml.NewDecoder(resp.Body).Decode(&document)
    return &document, err
}

2.5 小结

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

第 3 章 打包和工具链

本章主要内容:如何组织 Go 代码;使用 Go 语言自带的相关命令;使用其他开发者提供的工具;与其他开发者合作。

在 Go 语言里,包是个非常重要的概念。其设计理念是使用包来封装不同语义单元的功能。这样做,能够更好地复用代码,并对每个包内的数据的使用有更好的控制。所有的.go 文件,除了空行和注释,都应该在第一行声明自己所属的包。每个包都在一个单独的目录里。不能把多个包放到同一个目录中,也不能把同一个包的文件分拆到多个不同目录中。这意味着,同一个目录下的所有.go 文件必须声明同一个包名。

3.1 main包

经典的“Hello World!”程序

hello.go

package main

import "fmt"

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

3.2 导入

如果需要导入多个包,习惯上是将import 语句包装在一个导入块中。strings 包提供了很多关于字符串的操作,如查找、替换或
者变换。

import (
        "fmt"
        "strings"

)

3.2.1 远程导入

Go 工具链会使用导入路径确定需要获取的代码在网络的什么地方。
例如:import "github.com/spf13/viper

3.2.2 命名导入

重命名导入。有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入。

3.3 函数 init

init 函数的用法

package postgres

import (
    "database/sql"
)

func init() {
    //创建一个 postgres 驱动的实例。这里为了展现 init 的作用,没有展现其定义细节。
    sql.Register("postgres", new(PostgresDriver))
}

在使用这个新的数据库驱动写程序时,我们使用空白标识符来导入包,以便新的驱动会包含到 sql 包。如前所述,不能导入不使用的包,为此使用空白标识符重命名这个导入可以让 init函数发现并被调度运行,让编译器不会因为包未被使用而产生错误。我们可以调用 sql.Open 方法来使用这个驱动。

导入时使用空白标识符作为包的别名:

package main

import (
    "database/sql"
    //使用空白标识符导入包,避免编译错误。
    _"github.com/goinaction/code/chapter3/dbdriver/postgres"
)
func main() {
    //调用sql包提供的open方法。该方法能工作的关键在于postgres驱动通过自己的init函数将自身注册到了sql包。
    sql.Open("postgres","mydb")
}

3.4 使用 Go 的工具

build 和 clean 命令会执行编译和清理的工作。go build hello.go

调用 clean 后会删除编译生成的可执行文件。go clean hello.go

使用 io 包的工作

package main
import (
    "fmt"
    "io/ioutil"
    "os"
    
    "github.com/goinaction/code/chapter3/words"
)
// main 是应用程序的入口
func main() {
    filename := os.Args[1]
    
    contents, err := ioutil.ReadFile(filename)
    if err != nil {
        fmt.Println(err)
        return
    }
    text := string(contents)
    count := words.CountWords(text)
    
    fmt.Printf("There are %d words in your text.\n", count)
}

做开发会经常使用 go build 和 go run 命令。go build wordcount.go

go run 命令会先构建 wordcount.go 里包含的程序,然后执行构建后的程序。go run wordcount.go

3.5 进一步介绍 Go 开发工具

vet 命令会帮开发人员检测代码的常见错误:Printf 类函数调用时,类型匹配错误的参数;定义常用的方法时,方法签名的错误;错误的结构标签;没有指定字段名的结构字面量。

使用 go vet工具不能让开发者避免严重的逻辑错误,或者避免编写充满小错的代码。

package main

import "fmt"
func main() {

fmt.Printf("The quick brown fox jumped over lazy dogs", 3.14)
}

这个程序要输出一个浮点数 3.14,但是在格式化字符串里并没有对应的格式化参数。

3.5.2 Go 代码格式化

fmt 命令会自动格式化开发人员指定的源代码文件并保存。

3.5.3 Go 语言的文档

Go 语言有两种方法为开发者生成文档。如果开发人员使用命令行提示符工作,可以在终端上直接使用 go doc 命令来打印文档。

1.从命令行获取文档go doc tar;2.浏览文档godoc -http=:6060

3.6 与其他 Go 开发者合作

以分享为目的创建代码库

1.包应该在代码库的根目录中(使用 go get 的时候,开发人员指定了要导入包的全路径);2.包可以非常小;

3.对代码执行 go fmt;4.给代码写文档。

3.7 依赖管理

最流行的依赖管理工具有godep、vender、gopkg.in 工具

3.7.1 第三方依赖

像 godep 和 vender 这种社区工具已经使用第三方(verdoring)导入路径重写这种特性解决了依赖问题。其思想是把所有的依赖包复制到工程代码库中的目录里,然后使用工程内部的依赖包所在目录来重写所有的导入路径。

3.7.2 对 gb 的介绍

gb 背后的原理源自理解到 Go 语言的 import 语句并没有提供可重复构建的能力。 import语句可以驱动 go get ,但是 import 本身并没有包含足够的信息来决定到底要获取包的哪个修改的版本。 go get 无法定位待获取代码的问题,导致 Go 工具在解决重复构建时,不得不使用复杂且难看的方法。

gb 的创建源于上述理解。gb 既不包装 Go 工具链,也不使用 GOPATH 。gb 基于工程将 Go 工具链工作空间的元信息做替换。这种依赖管理的方法不需要重写工程内代码的导入路径。而且导入路径依旧通过 go get 和 GOPATH 工作空间来管理。gb 工程与 Go 官方工具链(包括 go get )并不兼容。

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

第4章 数组、切片、映射

本章主要内容:数组的内部实现和基础功能;使用切片管理数据集合;使用映射管理键值对。

4.1 数组的内部实现和基础功能

数组是切片和映射的基础数据结构。

4.1.1 内部实现

数组存储的类型可以是内置类型,如整型或者字符串,也可以是某种结构类型。数组的类型信息可以提供每次访问一个元素时需要在内存中移动的距离。

4.1.2 声明和初始化

声明一个数组,并设置为零值;使用数组字面量声明数组;让 Go 自动计算声明数组的长度;声明数组并指定特定元素的值。

4.1.3 使用数组

访问数组元素;访问指针数组的元素;把同样类型的一个数组赋值给另外一个数组;把一个指针数组赋值给另一个指针数组。

数组变量的类型包括数组长度和每个元素的类型。只有这两部分都相同的数组,才是类型相同的数组,才能互相赋值。编译器会阻止类型不同的数组互相赋值。

4.1.4 多维数组

声明二维数组,访问二维数组的元素,同样类型的多维数组赋值,使用索引为多维数组赋值。

4.1.5 在函数间传递数组

使用值传递,在foo函数间传递大数组。每次函数 foo 被调用时,必须在栈上分配 8 MB 的内存。之后,整个数组的值(8 MB 的内存)被复制到刚分配的内存里。只传入指向数组的指针,这样只需要复制 8 字节的数据而不是8 MB 的内存数据到栈上,使用指针在函数间传递大数组,这个操作会更有效地利用内存,性能也更好。不过要意识到,因为现在传递的是指针,所以如果改变指针指向的值,会改变共享的内存。如你所见,使用切片能更好地处理这类共享问题。

4.2 切片的内部实现和基础功能

切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处。

切片有 3 个字段的数据结构,这些数据结构包含 Go 语言需要操作底层数组的元数据,这 3 个字段分别是指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。

4.2.2 创建和初始化

1.make 和切片字面量

使用长度声明一个字符串切片;使用长度和容量声明整型切片。容量小于长度的切片会在编译时报错。

通过切片字面量来声明切片;使用索引声明切片;

2.nil 和空切片

只要在声明时不做任何初始化,就会创建一个 nil 切片。nil 切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时, nil 切片会很好用。例如,函数要求返回一个切片但是发生异常的时候。

利用初始化,通过声明一个切片可以创建一个空切片。空切片在底层数组包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用,例如,数据库查询返回 0 个查询结果时。

4.2.3 使用切片

1.赋值和切片

使用切片字面量来声明切片;使用切片创建切片。

如何计算长度和容量:

对底层数组容量是 k 的切片 slice[i:j]来说
长度: j - i
容量: k - i

修改切片内容可能导致的结果。与切片的容量相关联的元素只能用于增长切片。在使用这部分元素前,必须将其合并到切片的长度里。

2.切片增长

使用 append 向切片增加元素;使用 append 同时增加切片的长度和容量;使用 append 同时增加切片的长度和容量。

3.创建切片时的 3 个索引

使用切片字面量声明一个字符串切片;使用 3 个索引创建切片;

如果试图设置的容量比可用的容量还大,就会得到一个语言运行时错误。内置函数 append 会首先使用可用容量。一旦没有可用容量,会分配一个
新的底层数组。这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片
进行修改,很可能会导致随机且奇怪的问题。对切片内容的修改会影响多个切片,却很难找到问
题的原因。
如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。新切片与原有的底层数组分离后,可以安全地进行后续修改。内置函数 append 也是一个可变参数的函数。这意味着可以在一次调用传递多个追加的值。如果使用 ... 运算符,可以将一个切片的所有元素追加到另一个切片里。

4.迭代切片

使用 for range 迭代切片,当迭代切片时,关键字 range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本。range 创建了每个元素的副本,而不是直接返回对该元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会造成错误。

使用空白标识符(下划线)来忽略索引值。关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,依旧可以使用传
统的 for 循环,有两个特殊的内置函数 len 和 cap ,可以用于处理数组、切片和通道。对于切片,函数 len返回切片的长度,函数 cap 返回切片的容量。

4.2.4 多维切片

组合切片的切片

4.2.5 在函数间传递切片

在函数间传递切片就是要在函数间以值的方式传递切片。由于切片的尺寸很小,在函数间复制和传递切片成本也很低。

4.3 映射的内部实现和基础功能

4.3.1 内部实现

映射是无序的集合,意味着没有办法预测键值对被返回的顺序。无序的原因是映射的实现使用了散列表。映射的散列表包含一组桶。在存储、删除或者查找键值对的时候,所有操作都要先选择一个桶。把操作映射时指定的键传给映射的散列函数,就能选中对应的桶。这个散列函数的目的是生成一个索引,这个索引最终将键值对分布到所有可用的桶里。

4.3.2 创建和初始化

使用 make 声明映射,创建映射时,更常用的方法是使用映射字面量。映射的初始长度会根据初始化时指定的键值
对的数量来确定。
映射的键可以是任何值。这个值的类型可以是内置的类型,也可以是结构类型,只要这个值可以使用 == 运算符做比较。切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误。

使用映射字面量声明空映射:

// 创建一个映射,使用字符串切片作为映射的键
dict := map[[ ]string]int{ };

声明一个存储字符串切片的映射:

// 创建一个映射,使用字符串切片作为值
dict := map[int][ ]string{ }

4.3.3 使用映射

从映射获取值并判断键是否存在:

// 获取键 Blue 对应的值
value, exists := colors["Blue"]

// 这个键存在吗?
if exists {
fmt.Println(value)
}

从映射获取值,并通过该值判断键是否存在:

// 获取键 Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
    fmt.Println(value)
}

使用 range 迭代映射;

从映射中删除一项:

// 删除键为 Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
     fmt.Printf("Key: %s Value: %s\n", key, value)
}

4.3.4 在函数间传递映射

当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改。

4.4 小结
数组是构造切片和映射的基石;
Go 语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据;内置函数 make 可以创建切片和映射,并指定原始的长度和容量。也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值;切片有容量限制,不过可以使用内置的 append 函数扩展容量;映射的增长没有容量或者任何限制;内置函数 len 可以用来获取切片或者映射的长度;内置函数 cap 只能用于切片;通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值;但是切片不能用作映射的键; 将切片或者映射传递给函数成本很小,并且不会复制底层的数据结构。

第 5 章 Go 语言的类型系统

本章主要内容: 声明新的用户定义的类型;使用方法,为类型增加新的行为;了解何时使用指针,何时使用值;通过接口实现多态;通过组合来扩展或改变类型;公开或者未公开的标识符。

Go 语言是一种静态类型的编程语言。值的类型给编译器提供两部分信息:第一部分,需要分配多少内存给这个值(即值的规模)
;第二部分,这段内存表示什么。对于许多内置类型的情况来说,规模和表示是类型名的一部分。

5.1 用户定义的类型

//user 在程序里定义一个用户类型
type user struct {
    name    string
    email   string
    ext     int
    privileged, bool
}

使用结构类型声明变量,并初始化为其零值

声明user类型的变量

var bill user

任何时候,创建一个变量并初始化为其零值,习惯是使用关键字 var 。如果变量被初始化为某个非零值,就配合结构字面量和短变量
声明操作符来创建变量。一个短变量(:=)声明操作符在一次操作中完成两件事情:声明一个变量,并初始化。短变量声明操作符会使用右侧给出的类型信息作为声明变量的类型。

使用结构字面量创建结构类型的值;不使用字段名,创建结构类型的值;

使用其他结构类型声明字段:

//admin需要一个user类型作为管理者,并附加权限
type admin struct {
    person user 
    level string
}

使用结构字面量来创建字段的值:

// 声明 admin 类型的变量
fred := admin{
    person: user{
    name:       "Lisa",
    email:      "lisa@email.com",
    ext:        123,
    privileged: true,
    },
    level: "super",
}

另一种声明用户定义的类型的方法是,基于一个已有的类型,将其作为新类型的类型说明。

基于 int64 声明一个新类型:

type Duration int64

Duration 是一种描述时间间隔的类型,单位是纳秒(ns)。这个类型使用内置的 int64 类型作为其表示。在 Duration类型的声明中,我们把 int64 类型叫作 Duration 的基础类型。不过,虽然 int64 是基础类型,Go 并不认为 Duration 和 int64 是同一种类型。这两个类型是完全不同的有区别的类型。

给不同类型的变量赋值会产生编译错误:

package main

type Duration int64

func mian() {
    var dur Duration
    dur = int64(1000)
}

5.2 方法

方法能给用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字func 和方法名之间增加了一个参数

listing11.go

//这个示例程序展示如何声明
//并使用方法
package main

import (
    "fmt"
)

//user在程序里定义一个用户类型
type user struct {
    name string
    email string
}

//notify使用值接收者实现了一个方法
func (u user) notify() {
    fmt.Printf("Sending User Email To %s<%s>\n",
    u.name,
    u.email)
}

//changeEmail使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
    u.email = email
}

//main是应用程序的入口
func main() {
    //user类型的值可以用来调用
    //使用值接收者声明的方法
    bill := user{"Bill", "bill@email.com"}
    bill.notify()
    
    //指向user类型值的指针也可以用来调用
    //使用值接收者声明的方法
    lisa := &user{"Lisa", "lisa@email.com"}
    lisa.notify()
    
    //user类型的值可以用来调用
    //使用指针接收者声明方法
    bill.changeEmail("bill@newdomain.com")
    bill.notify()
    
    //指向user类型值的指针也可以用来调用
    //使用指针接收者声明的方法
    lisa.changeEmail("lisa@newdomain.com")
    lisa.notify()
}
Go 语言里有两种类型的接收者:值接收者和指针接收者。notify 方法的接收者被声明为 user 类型的值。如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。

使用变量来调用方法:

bill.notify()

这个语法与调用一个包里的函数看起来很类似。但在这个例子里, bill 不是包名,而是变量名。这段程序在调用 notify 方法时,使用 bill 的值作为接收者进行调用,方法 notify会接收到 bill 的值的一个副本。

值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。

5.3 类型的本质

在声明一个新类型之后,声明一个该类型的方法之前,果给这个类型增加或者删除某个值,是要创建一个新值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。

5.3.1 内置类型

内置类型是由语言提供的一组类型,分别是数值类型、字符串类型和布尔类型。当对这些值进行增加或者删除的时候,会创建一个新值。当把这些类型的值传递给方法或者函数时,应该传递一个对应值的副本。

5.3.2 引用类型

Go 语言里的引用类型有如下几个:切片、映射、通道、接口和函数类型。当声明上述类型当声明上述类型的变量时,创建的变量被称作标头(header)值。

5.3.3 结构类型

type Time struct {
    // sec 给出自公元 1 年 1 月 1 日 00:00:00
    // 开始的秒数
    sec int64

// nsec 指定了一秒内的纳秒偏移,
    // 这个值是非零值,
    // 必须在[0, 999999999]范围内
    nsec int32

// loc 指定了一个 Location,
    // 用于决定该时间对应的当地的分、小时、
    // 天和年的值
    // 只有 Time 的零值,其 loc 的值是 nil
    // 这种情况下,认为处于 UTC 时区
    loc *Location
}

Time 结构选自 time 包。当思考时间的值时,你应该意识到给定的一个时间点的时间是不能修改的。

func Now() Time {
    sec, nsec := now()
    return Time{sec + unixToInternal, nsec, Local}
}

展示了 Now 函数的实现。这个函数创建了一个 Time 类型的值,并给调用者返回了 Time 值的副本。这个函数没有使用指针来共享 Time 值。

func (t Time) Add(d Duration) Time {
    t.sec += int64(d / 1e9)
    nsec := int32(t.nsec) + int32(d%1e9)
    if nsec >= 1e9 {
        t.sec++
        nsec -= 1e9
    } else if nsec < 0 {
        t.sec--
        nsec += 1e9
    }
    t.nsec = nsec
    return t
}func (t Time) Add(d Duration) Time {
    t.sec += int64(d / 1e9)
    nsec := int32(t.nsec) + int32(d%1e9)
    if nsec >= 1e9 {
        t.sec++
        nsec -= 1e9
    } else if nsec < 0 {
        t.sec--
        nsec += 1e9
    }
    t.nsec = nsec
    return t
}

这个方法使用值接收者,并返回了一个新的 Time 值。该方法操作的是调用者传入的 Time 值的副本,并且给调用者返回了一个方法内的 Time 值的副本。至于是使用返回的值替换原来的 Time 值,还是创建一个新的 Time 变量来保存结果,是由调用者决定的事情。

golang.org/src/os/file_unix.go:

// File 表示一个打开的文件描述符
type File struct {
    *file
}
// file 是*File 的实际表示
// 额外的一层结构保证没有哪个 os 的客户端
// 能够覆盖这些数据。如果覆盖这些数据,
// 可能在变量终结时关闭错误的文件描述符
type file struct {
    fd int
    name string
    dirinfo *dirInfo // 除了目录结构,此字段为 nil
    nepipe int32 // Write 操作时遇到连续 EPIPE 的次数
}

标准库中声明的 File 类型。这个类型的本质是非原始的。这个类型的值实际上不能安全复制。对内部未公开的类型的注释,解释了不安全的原因。因为没有方法阻止程序员进行复制,所以 File 类型的实现使用了一个嵌入的指针,指向一个未公开的类型。

golang.org/src/os/file.go:
func Open(name string) (file *File, err error) {
    return OpenFile(name, O_RDONLY, 0)
}

展示了 Open 函数的实现,调用者得到的是一个指向 File 类型值的指针。Open 创建了 File 类型的值,并返回指向这个值的指针。如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。即便函数或者方法没有直接改变非原始的值的状态,依旧应该使用共享的方式传递。

func (f *File) Chdir() error {
    if f == nil {
    return ErrInvalid
    }
    if e := syscall.Fchdir(f.fd); e != nil {
    return &PathError{"chdir", f.name, e}
    }
    return nil
}

Chdir 方法展示了,即使没有修改接收者的值,依然是用指针接收者来声明的。因为 File 类型的值具备非原始的本质,所以总是应该被共享,而不是被复制。是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。

5.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力。如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。

5.4.1 标准库

示例程序实现了流行程序 curl 的功能:

listing34.go

//这个示例程序展示如何使用io.Reader和io.Writer接口
//写一个简单版本的curl程序
package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

//init在main函数之前调用
func init() {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ./example2 <url>")
        os.Exit(-1)
    }
}

//main是应用程序的入口
func main() {
    //从web服务器得到响应
    r, err := http.Get(os.Args[1])
    if err != nil {
        fmt.Println(err)
        return
    }
    
    //从Body复制到Stdout
    io.Copy(os.Stdout, r.Body)
    if err := r.Body.close(); err != nil {
        fmt.Println(err)
    }
}

listing35.go

//这个示例程序展示bytes.Buffer也可以
//用于io.Copy函数
package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

//main是应用程序的入口
func main() {
    var b bytes.Buffer
    
    //将字符串写入Buffer
    b.Writer([]byte("Hello"))
    
    //使用Fprintf将字符串拼接到Buffer
    fmt.Fprintf(&b, "world!")
    
    //将Buffer的内容写到Stdout
    io.Copy(os.Stdout, &b)
}

这个程序使用接口来拼接字符串,并将数据以流的方式输出到标准输出设备。

5.4.2 实现

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。

图 5-1 实体值赋值后接口值的简图

图 5-2 实体指针赋值后接口值的简图

5.4.3 方法集
方法集定义了接口的接受规则。

listing36.go

//这个示例程序展示Go语言里如何使用接口

package main

import (
    "fmt"
)

//notifier是一个定义了
//通知类行为的接口
type notifier interface {
    notify()
}

//user在程序里定义一个用户类型
type user struct {
    name string
    email string
}

//notify是使用指针接收者实现的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

//main是应用程序的入口
func main() {
    //创建一个user类型的值,并发送通知
    u := user{"Bill", "bill@email.com"}
    
    sendNotification(u)
    
    // ./listing36.go:32: 不能将u(类型是user)作为
    //                      sendNotification的参数类型notifier:
    // user类型并没有实现notifier
    //                      (notifier方法使用指针接收者声明)
}

//sendNotification接受一个实现了notifier接口的值
//并发送通知
func sendNotification(n notifier) {
    n.notify()
}
程序虽然看起来没问题,但实际上却无法通过编译。用指针接收者来实现接口时为什么 user 类型的值无法实现该接口,需要先了解方法集。方法集定义了一组关联到给定类型的值或者指针的方法。定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联。

规范里描述的方法集

Values      Methods Receivers
-----------------------------------------------
T           (t T)
*T          (t T) and (t *T)

从接收者类型的角度来看方法集

Methods Receivers   Values
-----------------------------------------------
(t T)               T and *T
(t *T)              *T

编译器并不是总能自动获得一个值的地址

package main

import "fmt"

//duration是一个给予int类型的类型
type duration int

//duration是一个基于int类型的类型
type duration int

//使用更可读的方式格式化duration值
func (d *duration) pretty() string {
    return fmt.Springf("Duration:%d", *d)
}

//main是应用程序的入口
func main() {
    duration(42).pretty()
    
    // ./listing46.go:17: 不能通过指针调用duration(42)的方法
    // ./listing46.go:17: 不能获取duration(42)的方法
}
5.4.4 多态

//这个示例程序使用接口展示多态行为
package main

import (
    "fmt"
)

//notifier是一个定义了
//通知类行为的接口
type notifier interface {
    notify()
}

//user在程序定义了一个用户类型
type user struct {
    name string
    email string
}

//notify使用指针接收者实现了notifier接口
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

//admin定义了程序里的管理员
type admin struct {
    name string
    email string
}

//notify使用指针接收者实现了notifier接口
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

//main是应用程序的入口
func main() {
    //创建一个user值并传给sendNotificcation
    bill := user{"Bill", "bill@email.com"}
    sendNotification(&lisa)
}

//sendNotification接受一个实现了notifier接口的值
//并发送通知
func sendNotification(n notifier) {
    n.notify()
}

5.5 嵌入类型

Go 语言允许用户扩展或者修改已有类型的行为。这个功能对代码复用很重要,在修改已有类型以符合新类型的时候也很重要。嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型。通过嵌入类型,与内部类型相关的标识符会提升到外部类型上。

//这个示例程序展示如何将一个类型嵌入另一个类型,以及
//内部类型和外部类型之间的关系
package main

import (
    "fmt"
)

//user在程序里定义一个用户类型
type user struct {
    name string
    email string
}

//notify实现了一个可以通过user类型值的指针
//调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
    u.name,
    u.email)
}

//admin 代表一个拥有权限的管理员用户
type admin struct {
    user //嵌入类型 
    level string 
}

//main是应用程序的入口
func main() {
    //创建一个admin用户
    ad := admin{
        user : user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }
    
    //我们可以直接访问内部类型的方法
    ad.user.notify()
    
    //内部类型的方法也被提升到外部类型
    ad.notify()
}

如何将嵌入类类型应用于接口

package main

import (
    "fmt"
)

//notifier是一个定义了
//通知类行为的接口
type notifier interface {
    notify()
}

//user在程序里定义一个用户类型
type user struct {
    name string
    email string
}

//通过user类型值的指针
//调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
    u.name,
    u.email)
}

//admin代表一个拥有权限的管理员用户
type admin struct {
    user
    level string
}

//main是应用程序的入口
func main() {
    //创建一个admin用户
    ad := admin{
        user: user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }
    
    //给admin用户发送一个通知
    //用于实现接口的内部类型的方法,被提升到
    //外部类型
    sendNotification(&ad)
}

//sendNotification接受一个实现了notifier接口的值
//并发送通话
func sendNotification(n notifier) {
    n.notify()
}

示例程序展示当内部类型和外部类型要实现同一个接口时的做法

package main

import (
    "fmt"
)

//notifier是一个定义了
//通知类行为的接口
type notifier interface {
    notify()
}

//user在程序里定义一个用户类型
type user struct {
    name string
    email string
}

//通过user类型值的指针
//调用的方法
func (u *user) notify() {
    fmt.Printf("Sending user email to %s<%s>\n",
        u.name,
        u.email)
}

//admin代表一个拥有权限的管理员用户
type admin struct {
    user
    level string
}

//通过admin类型值的指针
//调用的方法
func (a *admin) notify() {
    fmt.Printf("Sending admin email to %s<%s>\n",
        a.name,
        a.email)
}

//main是应用程序的入口
func main() {
    //创建一个admin用户
    ad := admin{
        user: user{
            name: "john smith",
            email: "john@yahoo.com",
        },
        level: "super",
    }
    
    //给admin用户发送一个通知
    //接口的嵌入的内部类型实现并没有替升到
    //外部类型
    sendNotification(&ad)
    
    //我们可以直接访问内部类型的方法
    ad.user.notify()
    
    //内部类型的方法没有被提升
    ad.notify()
}

//sendNotification接受一个实现了notifier接口的值
//并发送通知
func sendNotification(n notifier) {
    n.notify()
}

listing60.go 的输出
Sending admin email to john smith<john@yahoo.com>
Sending user email to john smith<john@yahoo.com>
Sending admin email to john smith<john@yahoo.com>

这次我们看到了 admin 类型是如何实现 notifier 接口的,以及如何由 sendNotification函数以及直接使用外部类型的变量 ad 来执行 admin 类型实现的方法。这表明,如果外部类型实现了 notify 方法,内部类型的实现就不会被提升。不过内部类型的值一直存在,因此还可以通过直接访问内部类型的值,来调用没有被提升的内部类型实现的方法。

5.6 公开或未公开的标识符

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。

例子已经修改为使用工厂函数来创建一个未公开的 alertCounter 类型的值。

// counters 包提供告警计数器的功能
package counters

// alertCounter 是一个未公开的类型05
// 这个类型用于保存告警计数
type alertCounter int

// New 创建并返回一个未公开的
// alertCounter 类型的值
func New(value int) alertCounter {
    return alertCounter(value)
}

将工厂函数命名为 New 是 Go 语言的一个习惯。这个 New 函数做了些有意思的事情:它创建了一个未公开的类型的值,并将这个值返回给调用者。

/ main 是应用程序的入口
func main() {
    // 使用 counters 包公开的 New 函数来创建
    // 一个未公开的类型的变量
    counter := counters.New(10)

fmt.Printf("Counter: %d\n", counter)
 }

这个 New 函数返回的值被赋给一个名为 counter 的变量。这个程序可以编译并且运行,要让这个行为可行,需要两个理由。第一,公开或者未公开的标识符,不是一个值。第二,短变量声明操作符,有能力捕获引用的类型,并创建一个未公开的类型的变量。永远不能显式创建一个未公开的类型的变量,不过短变量声明操作符可以这么做。

由于内部类型 user 是未公开的,这段代码无法直接通过结构字面量的方式初始化该内部类型。不过,即便内部类型是未公开的,内部类型里声明的字段依旧是公开的。既然内部类型的标识符提升到了外部类型,这些公开的字段也可以通过外部类型的字段的值来访问。

5.7 小结
使用关键字 struct 或者通过指定已经存在的类型,可以声明用户定义的类型;方法提供了一种给用户定义的类型增加行为的方式;设计类型时需要确认类型的本质是原始的,还是非原始的;接口是声明了一组行为并支持多态的类型;嵌入类型提供了扩展类型的能力,而无需使用继承; 标识符要么是从包里公开的,要么是在包里未公开的。

第6章 并发

本章主要内容:使用 goroutine 运行程序;检测并修正竞争状态;利用通道共享数据。

Web 服务需要在各自独立的套接字(socket)上同时接收多个数据请求。每个套接字请求都是独立的,可以完全独立于其他套接字进行处理 。具有并行执行多个请求的能力可以显著提高这类系统的性能。Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine时,Go 会将其视为一个独立的工作单元。

Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。

6.1 并发与并行

什么是操作系统的线程(thread)和进程(process)。一个线程是一个执行空间,这个空间会被操作系统调度来运行函数中所写的代码。每个进程至少包含一个线程,每个进程的初始线程被称作主线程。因为执行这个线程的空间是应用程序的本身的空间,所以当主线程终止时,应用程序也会终止。

FIFO:先进先出调度算法LRU:最近最久未使用调度算法两者都是缓存调度算法,经常用作内存的页面置换算法。

线程调度算法:1、先来先服务(FCFS)  2、最短作业优先(SJF) 3、基于优先权的调度算法(FPPS) 4、时间片轮转(RR) 5、多级队列调度(Multilevel feedback queue)

抢占式、非抢占式

图 6-1 一个运行的应用程序的进程和线程的简要描绘

在图 6-2 中,可以看到操作系统线程、逻辑处理器和本地运行队列之间的关系。

图 6-2 Go 调度器如何管理 goroutine

并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。

6.2 goroutine

深入了解一下调度器的行为,以及调度器是如何创建 goroutine 并管理其寿命的。

//这个示例程序展示如何创建goroutine

//以及调度器的行为\

package main

import (

"fmt"

"runtime"

"sync"

)

//main是所有Go程序的入口

func main() {

//分配一个逻辑处理器给调度器使用

runtime.GOMAXPROCS(1)

//wg用来等待程序完成

//计数加2,表示要等待两个goroutine

var wg sync.WaitGroup

wg.add(2)

fmt.Println("Start Goroutines")

go func() {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

//显示字母表3次

for count := 0; count < 3; count++ {

for char := 'a'; char < 'a'+26; char++ {

fmt.Printf("%c, char")

}

}

}()

//声明一个匿名函数,并创建一个goroutine

go func() {

//在函数退出时调用Done来通知mian函数工作已经完成

defer wg.Done()

//显示字母表3次

for count := 0; count < 3; count++ {

for char := 'A'; char < 'A'+26; char++ {

fmt.Printf("%c", char)

}

}

}()

//等待goroutine结束

fmt.Println("Waiting to Finish")

wg.Wait()

fmt.Println("\nTerminating Program")

}

基于调度器的内部算法,一个正运行的 goroutine 在工作结束前,可以被停止并重新调度。调度器这样做的目的是防止某个 goroutine 长时间占用逻辑处理器。当 goroutine 占用时间过长时,调度器会停止当前正运行的 goroutine,并给其他可运行的 goroutine 运行的机会。

//这个示例程序展示goroutine调度器是如何在单个程序上

//切分时间片的

package main

import (

"fmt"

"runtime"

"sync"

)

//wg用来等待程序完成

var wg sync.WaitGroup

//main是所有Go程序的入口

func main() {

//分配一个逻辑处理器给调度器使用

runtime.GOMAXPROCX(1)

//计数加2,表示要等待两个goroutine

wg.Add(2)

//创建两个goroutine

fmt.Println("Create Goroutines")

go printPrime("A")

go printPrime("B")

//等待goroutine结束

fmt.Println("Waiting To Finish")

wg.Wait()

fmt.Println("Terminating Program")

}

//printPrime显示5000以内的素数值

func printPrime(prefix string) {

//函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

next:

for outer := 2; outer < 5000; outer++ {

for inner := 2; inner < outer; inner++ {

if outer%inner == 0 {

continue next

}

}

fmt.Printf("%s:%d", prefix, outer)

}

fmt.Println("Completed", prefix)

}

6.3 竞争状态

如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。对一个共享资源的读和写操作必须是原子化的,换句话说,同一时刻只能有一个 goroutine 对共享资源进行读和写操作。

// 这个示例程序展示如何在程序里造成竞争状态

// 实际上不希望出现这种情况

package main

import (

"fmt"

"runtime"

"sync"

)

var (

//counter是所有goroutine都要增加其值的变量

counter int

//wg用来等待程序结束

wg.sync.WaitGroup

)

//main是所有Go程序的入口

func main() {

//计数加2,表示要等待两个goroutine

wg.Add(2)

//创建两个goroutine

go printPrime("A")

go printPrime("B")

//等待goroutine结束

wg.Wait()

fmt.Println("Final Counter:", counter)

}

//incCounter增加包里counter变量的值

func incCounter(id int) {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

for count := 0; count < 2; count++ {

//捕获counter的值

value := counter

//当前goroutine从线程退出,并放回到队列

runtime.Gosched()

//增加本地value变量的值

value++

//将改值保存回counter

counter = value

}

}

go build -race // 用竞争检测器标志来编译程序

./example    // 运行程序

一种修正代码、消除竞争状态的办法是,使用 Go 语言提供的锁机制,来锁住共享资源,从而保证 goroutine 的同步状态。

6.4 锁住共享资源

6.4.1 原子函数

原子函数能够以很底层的加锁机制来同步访问整型变量和指针。我们可以用原子函数来修正代码清单创建的竞争状态。

// 这个示例程序展示如何使用 atomic 包来提供

// 对数值类型的安全访问

package main

import (

"fmt"

"runtime"

"sync"

"sync/atomic"

)

var (

//counter是所有goroutine都要增加其值的变量

counter int64

//wg用来等待程序结束

wg.sync.WaitGroup

)

//main是所有Go程序的入口

func main() {

//计数加2,表示要等待两个goroutine

wg.Add(2)

//创建两个goroutine

go incCounter(1)

go incCounter(2)

//等待goroutine结束

wg.Wait()

//现实最终的值

fmt.Println("Final Counter:", counter)

}

//incCounter增加包里counter变量的值

func incCounter(id int) {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

for count := 0; count < 2; count++ {

//安全地对counter加1

atomic.AddInt64(&counter, 1)

//当前goroutine从线程退出,并放回到队列

runtime.Gosched()

}

}

Final Counter: 4

现在,程序的第 43 行使用了 atmoic 包的 AddInt64 函数。这个函数会同步整型值的加法,方法是强制同一时刻只能有一个 goroutine 运行并完成这个加法操作。当 goroutine 试图去调用任何原子函数时,这些 goroutine 都会自动根据所引用的变量做同步处理。现在我们得到了正确的值 4。

另外两个有用的原子函数是 LoadInt64 和 StoreInt64 。这两个函数提供了一种安全地读和写一个整型值的方式。

// 这个示例程序展示如何使用 atomic 包里的

// Store 和 Load 类函数来提供对数值类型

package main

import (

"fmt"

"sync"

"sync/atomic"

"time"

)

var (

//shutdown是通知正在执行的goroutine停止工作的标志

shutdown int64

//wg用来等待程序结束

wg.sync.WaitGroup

)

//main是所有Go程序的入口

func main() {

//计数加2,表示要等待两个goroutine

wg.Add(2)

//创建两个goroutine

go doWork("A")

go doWork("B")

//给定goroutine执行的时间

time.Sleep(1 * time.Second)

//该停止工作了,安全地设置shutdown标志

fmt.Println("Shutdown Now")

atomic.StoreInt64(&shutdown, 1)

//等待goroutine结束

wg.Wait()

}

//doWork用来模拟执行工作的goroutine,

//检测之前的shutdown标志来决定是否提前终止

func doWork(name string) {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

for {

fmt.Printf("Doing %s Work\n", name)

time.Sleep(250* time.Millisecod)

//要停止工作了吗

if  atomic.AddInt64(&counter, 1) == 1 {

fmt.Printf("Shutting %sl0wen", name)

break

}

}

}

6.4.2 互斥锁

另一种同步访问共享资源的方式是使用互斥锁( mutex )。互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

// 这个示例程序展示如何使用互斥锁来

// 定义一段需要同步访问的代码临界区

// 资源的同步访问

package main

import (

"fmt"

"runtime"

"sync"

)

var (

//counter是所有goroutine都要增加其值的变量

counter int

//wg用来等待程序结束

wg.sync.WaitGroup

)

//main是所有Go程序的入口

func main() {

//计数加2,表示要等待两个goroutine

wg.Add(2)

//创建两个goroutine

go incCounter(1)

go incCounter(2)

//等待goroutine结束

wg.Wait()

fmt.Printf("Final Counter: %d\\n", counter)

}

//incCounter使用互斥锁来同步并保证安全访问

//增加包里counter变量的值

func incCounter(id int) {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

for count := 0; count < 2; counter++ {

//同一时刻只允许一个goroutine进入

//这个临界区

mutex.Lock()

{

//捕获counter的值

value := counter

//当前goroutine从线程退出,并放回到队列

runtime.Gosched()

//增加本地value变量的值

value++

//将该值保存回counter

counter = value

}

mutex.Unlock()

//释放锁,允许其他正在等待的goroutine

//进入临界区

}

}

6.5 通道

在 Go 语言里,你不仅可以使用原子函数和互斥锁来保证对共享资源的安全访

问以及消除竞争状态,还可以使用通道,通过发送和接收需要共享的资源,在 goroutine 之间做同步。

使用 make 创建通道

//无缓冲的整型通道

unbuffered := make(chan int)

//有缓冲的整型通道

buffered := make(chan string, 10)

//向通道发送值

// 通过通道发送一个字符串

buffered <- "Gopher"

// 从通道接收一个字符串

value := <-buffered

6.5.1 无缓冲的通道

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个 goroutine没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

// 这个示例程序展示如何用无缓冲的通道来模拟

// 2 个 goroutine 间的网球比赛

package main

import (

"fmt"

"math/rand"

"sync"

"time"

)

//wg用来等待程序结束

var wg sync.WaitGroup

func init() {

rand.Seed(time.Now().UnixNano())

}

//main是所有Go程序的入口

func main() {

//创建一个无缓冲的通道

court := make(chan int)

//计数加2,表示要等待两个goroutine

wg.Add(2)

//启动两个选手

go player("Nadal", court)

go player("Djokovic", court)

//发球(将球发到通道里)

court <- -1

//等待游戏结束

wg.Wait()

}

//player模拟一个选手在打网球

func player(name string, court chan int) {

//在函数退出时调用Done来通知main函数工作已经完成

defer wg.Done()

for {

//等待球被击打过来

//goroutine 从通道接收数据,用来表示等待接球。这个接收动作会锁住//goroutine,直到有数据发送到通道里。

ball, ok := <-court

if !ok {

//如果通道被关闭,我们就赢了

fmt.Printf("Player %s Won\n", name)

return

}

//选随机数,然后用这个数来判断我们是否丢球

n := rand.Intn(100)

if n%13 == 0 {

fmt.Printf("Player %s Missed\n", name)

//关闭通道,表示我们输了

close(court)

return

}

//显示击球数,并将击球数加1

fmt.Printf("Player %s Hit %d\n", name, ball)

ball++

//将球打向对手

court <- ball

}

}

// 这个示例程序展示如何用无缓冲的通道来模拟

// 4 个 goroutine 间的接力比赛

package main

import (

"fmt"

"sync"

"time"

)

//wg用来等待程序结束

var wg sync.WaitGroup

//main是所有Go程序的入口

func main() {

//创建一个无缓冲的通道

court := make(chan int)

//为最后一位跑步者将计数加1

wg.Add(1)

//第一位跑步者持有接力棒

go Runner(baton)

//开始比赛

baton <- -1

//等待比赛结束

wg.Wait()

}

//Runner模拟一个选手在打网球

func Runner(baton chan int) {

var newRunner int

//等待接力棒

runner := <-baton

//开始绕着跑道跑步

fmt.Printf("Runner %d Running With Baton\n", runner)

//创建下一位跑步者

if runner != 4 {

newRunner = runer + 100

fmt.Printf("Runner %d To The Line\n", newRunner)

go Runner(baton)

}

//围绕跑到跑

time.Sleep(100 * time.Millisecond)

//比赛结束了吗?

if runner == 4 {

fmt.Printf("Runner %d Finished, Race Over\n", runner)

wg.Done()

return

}

//将接力棒交给下一位跑步者

fmt.Printf("Runner %d Exchange With Runner %d\n",

runner,

newRunner)

baton <- newRunner

}

}

6.5.2 有缓冲的通道

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

// 这个示例程序展示如何使用

// 有缓冲的通道和固定数目的

// goroutine 来处理一堆工作

package main

import (

"fmt"

"math/rand"

"sync"

"time"

)

const (

numberGoroutines = 4 //要使用的goroutine的数量

taskLoad         = 10 //要处理的工作的数量

)

//wg用来等待程序结束

var wg sync.WaitGroup

//init初始化包,Go语言运行时会在其他代码执行之前

//优先执行这个函数

func init() {

//初始化随机数种子

rand.Seed(time.Now().Unix())

}

//main是所有Go程序的入口

func main() {

//创建一个有缓冲的通道来管理工作

task := make(chan string, taskLoad)

//启动goroutine来处理工作

wg.Add(numberGoroutines)

for gr := 1; gr <= numberGoroutines; gr++ {

go worker(tasks, gr)

}

//增加一组要完成的工作

for post := 1; post <= taskLoad; post++ {

tasks <- fmt.Sprintf("Task : %d", post)

}

//当所有工作都处理完时关闭通道

//以便所有goroutine退出

close(tasks)

//等待所有工作完成

wg.Wait()

}

//worker作为goroutine启动处理

//从有缓存的通道传入的工作

func worker(tasks chan string, worker int) {

//通知函数已经返回

defer wg.Done()

for {

//等待分配工作

task, ok := <-tasks

if !ok {

//这意味着通道已经空了,并且已被关闭

fmt.Printf("Worker: %d : Shutting Down\n", worker)

return

}

//显示我们开始工作了

fmt.Printf("Worker: %d : Started %s\n", worker, task)

//随机等一段时间来模拟工作

sleep := rand.Int63n(100)

time.Sleep(time.Duration(sleep) * time.Millisecond)

//显示我们完成了工作

fmt.Printf("Worker: %d : Completed %s\n", worker, task)

}

}

当通道关闭后,goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。能够从已经关闭的通道接收数据这一点非常重要,因为这允许通道关闭后依旧能取出其中缓冲的全部值,而不会有数据丢失。从一个已经关闭且没有数据的通道

里获取数据,总会立刻返回,并返回一个通道类型的零值。如果在获取通道时还加入了可选的标志,就能得到通道的状态信息。

6.6 小结

并发是指 goroutine 运行的时候是相互独立的;使用关键字 go 创建 goroutine 来运行函数;goroutine 在逻辑处理器上执行,而逻辑处理器具有独立的系统线程和运行队列;竞争状态是指两个或者多个 goroutine 试图访问同一个资源;原子函数和互斥锁提供了一种防止出现竞争状态的办法;通道提供了一种在两个 goroutine 之间共享数据的简单方法;无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证。

第7章 并发模式

本章主要内容:控制程序的生命周期;管理可复用的资源池;创建可以处理任务的 goroutine 池。

7.1 runner

runner 包用于展示如何使用通道来监视程序的执行时间,如果程序运行时间太长,也可以用 runner 包来终止程序。

// Gabriel Aszalos 协助完成了这个示例

// runner 包管理处理任务的运行和生命周期

package runner

import (

"errors"

"os"

"os/signal"

"time"

)

//Runner在给定的超时间内执行一组任务,

//并且在操作系统发送中断信号时结束这些任务

type Runner struct {

//interrupt通道报告从操作系统

//发送的信号

interrupt chan os.signal

//complete通道报告处理任务已经完成

complete chan error

//timeout报告处理任务已经超时

timeout <-chan time.time

//task持有一组以索引顺序依次执行的

//函数

task []func(int)

}

//ErrTimeout会在任务执行超时时返回

var ErrTimeout = errors.New("received timeout")

//ErrTimeout会在接收到操作系统的事件时返回

var ErrInterrupt = errors.New("received interrupt")

//New返回一个新的准备使用的Runner

func New(d time.Duration) *Runner {

return &Runner{

interrupt: make(chan os.Signal, 1),

complete: make(chan error),

timeout: time.After(d),

}

}

//Add将一个任务附加到Runner上。这个任务是一个

//接收一个int类型的ID作为参数的函数

func (r *Runner) Add(tasks ...func(int)) {

r.tasks = append(r.tasks, tasks...)

}

//Start执行所有任务,并监视通道事件

func (r *Runner) Start() error {

//我们希望接收所有中断信号

sinal.Notify(r.interrupt, os.Interrupt)

//用不同的goroutine执行不同的任务

go func() {

r.complete <- r.run()

}()

select {

//当任务处理完成时发出的信号

case err := <-r.complete:

return err

//当任务处理程序运行超时发出的信号

case <-r.timeout:

return ErrTimeout

}

}

//run执行每一个已注册的任务

func (r *Runner) run() error {

for id, task := range r.tasks {

//检测操作系统的中断信号

if r.gotInterrupt() {

return ErrInterrupt

}

//执行已注册的新任务

task(id)

}

return nil

}

//gotInterrupt验证是否接收到了中断信号

func (r *Runner) gotInterrupt() bool {

select {

//当中断事件被触发时发出的信号

case <-r.interrupt:

//停止接收后续的任何信号

sinal.Stop(r.interrupt)

return true

//继续正常运行

default:

return false

}

}

程序展示了依据调度运行的无人值守的面向任务的程序,及其所使用的并发模式。在设计上,可支持以下终止点:程序可以在分配的时间内完成工作,正常终止;程序没有及时完成工作,“自杀”;接收到操作系统发送的中断事件,程序立刻试图清理状态并停止工作。

// 这个示例程序演示如何使用通道来监视
// 程序运行的时间,以在程序运行时间过长
// 时如何终止程序
package main

import (
   "log"
   "time"

   "github.com/goinaction/code/chapter7/patterns/runner"
   "os"
)

//timeout规定了必须在多少秒内处理完成
const timeout = 3 * time.Second

//main是程序的入口
func main() {
   log.Println("Starting work.")

//为本次执行分配超时时间
   r := runner.New(timeout)

//加入要执行的任务
   r.Add(createTask(), createTask(), createTask())

//执行任务并处理结果
   if err := r.Start(); err != nil {
      switch err {
      case runner.ErrTimeout:
         log.Println("Terminating dur to timeout.")
         os.Exit(1)
      case runner.ErrInterrupt:
         log.Println("Terminating dur to interrupt.")
         os.Exit(2)
      }
   }
   
   log.Println("Process ended.")
}

//createTask返回一个根据id
//休眠指定秒数的示例任务
func createTask() func(int) {
   return func(id int) {
      log.Printf("Processor - Task #%d.", id)
      time.Sleep(time.Duration(id) * time.Second)
   }
}
7.2 pool

pool这个包用于展示如何使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间共享及独立使用的资源。在 Go 1.6 及之后的版本中,标准库里自带了资源池的实现(sync.Pool)。

7.3 work

work 包的目的是展示如何使用无缓冲的通道来创建一个 goroutine 池,这些 goroutine 执行并控制一组工作,让其并发执行。

// work 包管理一个 goroutine 池来完成工作
package work

import "sync"

//Worker必须满足接口类型,
//才能使用工作池
type Worker interface {
   Task()
}

//Pool提供一个goroutine池,这个池可以完成
//任何已提交的Worker任务
type Pool struct {
   work chan Worker
   wg   sync.WaitGroup
}

//New创建一个新工作池
func New(maxGoroutines int) *Pool {
   p := Pool{
      work: make(chan Worker),
   }

p.wg.Add(maxGoroutines)
   for i := 0; i < maxGoroutines; i++ {
      go func() {
         for w := range p.work {
            w.Task()
         }
         p.wg.Done()
      }()
   }

return &p
}

//Run提交到工作池
func (p *Pool) Run(w Worker) {
   p.work <- w
}

//Shutdown等待所有goroutine停止工作
func (p *Pool) Shuntdown() {
   close(p.work)
   p.wg.Wait()
}

7.4 小结

可以使用通道来控制程序的生命周期; 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞;有缓冲的通道可以用来管理一组可复用的资源;语言运行时会处理好通道的协作和同步;使用无缓冲的通道来创建完成工作的 goroutine 池;任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保证对方接收到了数据。

  1. 标准库

本章主要内容:输出数据以及记录日志;对 JSON 进行编码和解码;处理输入/输出,并以流的方式处理数据;让标准库里多个包协同工作。

8.1 文档与源代码

标准库里总共有超过100 个包,这些包被分到 38 个类别里。标准库里的顶级目录和包:

archive debug hash mime sort Time bufio encoding html net strconv unicode bytes errors image os strings unsafe compress expvar index path sync container flag io reflect syscall crypto fmt log regexp testing database go math runtime text

8.2.1 log 包

记录日志的目的是跟踪程序什么时候在什么位置做了什么。

声明 Ldate 常量

// 日期: 2009/01/23

Ldate = 1 << iota

关键字 iota 在常量声明区里有特殊的作用。这个关键字让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。关键字 iota 的另一个功能是, iota 的初始值为 0,之后 iota 的值在每次处理为常量后,都会自增 1。

初始完 log 包后,可以看一下 main() 函数,看它是是如何写消息的。

func main() {
   //Println写到标准日志记录器
   log.Println("message")

//Fatalln在调用Println()之后会接着调用os.Exit(1)
   log.Fatalln("fatal message")
   
   //Panicln在调用Println()之后会接着调用panic()
   log.Panicln("panic message")
}

8.2.2 定制的日志记录器

要想创建一个定制的日志记录器,需要创建一个 Logger 类型值。可以给每个日志记录器配置一个单独的目的地,并独立设置其前缀和标志。

// 这个示例程序展示如何创建定制的日志记录器
package main

import (
   "io"
   "io/ioutil"
   "log"
   "os"
   "sync"
)

var (
   Trace   *log.Logger // 记录所有日志
   Info    *log.Logger // 重要的信息
   Warning *log.Logger // 需要注意的信息
   Error   *log.Logger // 非常严重的问题
)

func init() {
   filr, err := os.OpenFile("errors.txt",
      os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
   if err != nil {
      log.Fatalln("Failed to open error log file:", err)
   }

Trace = log.New(ioutil.Discard,
      "TRACE:",
         log.Ldate|log.Ltime|log.Lshortfile)

Info = log.New(os.Stdout,
      "INFO:",
      log.Ldate|log.Ltime|log.Lshortfile)
   Warning = log.New(os.Stdout,
      "Warning:",
      log.Ldate|log.Ltime|log.Lshortfile)
   Error = log.New(os.Stdout,
      "Error:",
      log.Ldate|log.Ltime|log.Lshortfile)
}

func main() {
   Trace.Println("I have something standard to say")
   Info.Println("Special Information")
   Warning.Println("There is something you need to know about")
   Error.Println("Something has failed")
}

8.3 编码 / 解码

8.3.1 解码 JSON

使用 json 包的 NewDecoder 函数以及 Decode方法进行解码。如果要处理来自网络响应或者文件的 JSON,那么一定会用到这个函数及方法。

// 这个示例程序展示如何使用 json 包和 NewDecoder 函数
// 来解码 JSON 响应
package main

import (
   "net/http"
   "log"
   "encoding/json"
   "fmt"
)

type (
   //gResult映射从搜索拿到的结果文档
   gResult struct {
      GsearchResultClass string 'json:"GsearchResultClass"'
      unescapedURL      string 'json:"unescapedURL"'
      URL              string 'json:"url"'
      VisibleURL           string 'json:"VisibleUrl"'
      CacheURl           string 'json:"cacheUrl"'
      Title              string 'json:"title"'
      TitleNoFormatting  string 'json:"titleNoFormatting"'
      Content            string 'json:"content"'
   }

//gResponse包含顶级的文档
   gResponse struct {
      ResponseData struct {
         Results []gResult 'json:"results"'
      } 'json:"responseData"'
   }
)
func main() {
   uri := "http://ajax.googleapis.com/ajax/services/search/web?v=1.0&rsz=8&q=golang"

   //Google发起搜索
   resp, err := http.Get(uri)
   if err != nil {
      log.Println("ERROR:", err)
      return
   }
   defer resp.Body.Close()
   
   //JSON响应解码到结构类型
   var gr gResponse
   err = json.NewDecoder(resp.Body).Decode(&gr)
   if err != nil {
      log.Println("ERROR", err)
      return
   }
   
   fmt.Println(gr)
}

// 这个示例程序展示如何解码 JSON 字符串
package main

import (
   "log"
   "encoding/json"
   "fmt"
)

//Contact结构代表我们的JSON字符串
type Contact struct {
    Name    string 'json:"name"'
    Title   string 'json:"title"'
    Contact struct {
      Home string ‘json:"home"
      Cell string 'json:"cell"'
     } 'json:"contact"'
}

//JSON包含用于反序列化的演示字符串
var JSON = '{
   "name": "Gopher",
   "title":"programmer",
   "contact":{
      "home": "415.333.3333",
      "cell": "415.555.5555"
   }
}'

func main() {
   //JSON字符串反序列化到变量
   var c Contact
   err := json.Unmarshal([]byte(JSON), &c)
   if err != nil {
      log.Println("ERROR:", err)
      return
   }

fmt.Println(c)
}

有时,无法为 JSON 的格式声明一个结构类型,而是需要更加灵活的方式来处理 JSON 文档。在这种情况下,可以将 JSON 文档解码到一个 map 变量中。

// 这个示例程序展示如何解码 JSON 字符串
package main

import (
   "log"
   "encoding/json"
   "fmt"
)

//JSON包含用于反序列化的演示字符串
var JSON = '{
   "name": "Gopher",
   "title":"programmer",
   "contact":{
      "home": "415.333.3333",
      "cell": "415.555.5555"
   }
}'

func main() {
   //JSON字符串反序列化到map变量
   var c map[string]interface{}
   err := json.Unmarshal([]byte(JSON), &c)
   if err != nil {
      log.Println("ERROR:", err)
      return
   }

fmt.Println("Name:", c["name"])
   fmt.Println("Title:", c["title"])
   fmt.Println("Contact")
   fmt.Println("H:", c["contact"].(map[string]interface{})["home"])
   fmt.Println("C:", c["contact"].(map[string]interface{})["cell"])
}

8.3.2 编码 JSON

我们要学习的处理 JSON 的第二个方面是,使用 json 包的 MarshalIndent 函数进行编码。这个函数可以很方便地将 Go 语言的 map 类型的值或者结构类型的值转换为易读格式的 JSON 文档。 序列化 (marshal)是指将数据转换为 JSON 字符串的过程。

// 这个示例程序展示如何序列化 JSON 字符串
package main

import (
   "encoding/json"
   "log"
   "fmt"
)

func main() {
   //创建一个保存键值对的映射
   c := make(map[string]interface{})
   c["name"] = "Gopher"
   c["title"] = "programmer"
   c["contact"] = map[string]interface{}{
      "home": "415.333.3333"
      "cell": "415.555.5555"
   }
   
   //将这个映射序列化到JSON字符串
   data, err := json.MarshalIndent(c, "", "   ")
   if err != nil {
      log.Println("ERROR:", err)
      return
   }
   
   fmt.Println(string(data))
}

// MarshalIndent 很像 Marshal,只是用缩进对输出进行格式化

func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) {

在 MarshalIndent 函 数 里 再 一 次 看 到 使 用 了 空 接 口 类 型 interface{} 。 函 数MarshalIndent 会使用反射来确定如何将 map 类型转换为 JSON 字符串。

8.4 输入和输出

8.4.1 Writer 和 Reader 接口

// 这个示例程序展示来自不同标准库的不同函数是如何
// 使用 io.Writer 接口的
package main

import (
   "bytes"
   "fmt"
   "os"
)

//main是应用程序的入口
func main() {
   //创建一个Buffer值,并将一个字符串写入Buffer
   //使用实现io.WriterWrite方法
   var b bytes.Buffer
   b.Write([]byte("Hello "))
   
   //使用Fprintf来将一个字符串拼接到Buffer
   //bytes.Buffer的地址作为io.Writer类型值传入
   fmt.Fprintf(&b, "World!")
   
   //Buffer的内容输出到标准输出设备
   //os.File值的地址作为io.Writer类型值传入
   b.WriteTo(os.Stdout)
}

8.4.3 简单的 curl

curl这个工具可以对指定的 URL 发起 HTTP 请求,并保存返回的内容。

// 这个示例程序展示来自不同标准库的不同函数是如何
// 使用 io.Writer 接口的
package main

import (
   "net/http"
   "os"
   "log"
   "io"
)
//main是应用程序的入口
func main() {
   //这里的r是一个响应,r.Bodyio.Reader
   r, err :=  http.Get(os.Args[1])
   if err != nil {
      log.Fatalln(err)
   }

//创建文件来保存响应内容
   file, err := os.Create(os.Args[2])
   if err != nil {
      log.Fatalln(err)
   }
   defer file.Close()

//使用MultiWriter,这样就可以同时向文件和标准输出设备
   //进行写操作
   dest := io.MultiWriter(os.Stdout, file)

//从响应的结果读出响应的内容,并写道两个目的地
   io.Copy(dest, r.Body)
   if err := r.Body.Close(); err != nil {
      log.Println(err)
   }
}

8.5 小结

标准库有特殊的保证,并且被社区广泛应用;使用标准库的包会让你的代码更易于管理,别人也会更信任你的代码;100 余个包被合理组织,分布在 38 个类别里;标准库里的 log 包拥有记录日志所需的一切功能;标准库里的 xml 和 json 包让处理这两种数据格式变得很简单;io 包支持以流的方式高效处理数据;接口允许你的代码组合已有的功能;阅读标准库的代码是熟悉 Go 语言习惯的好方法。

第9章 测试和性能

本章主要内容:编写单元测试来验证代码的正确性;使用 httptest 来模拟基于 HTTP 的请求和响应;使用示例代码来给包写文档;通过基准测试来检查性能。

9.1 单元测试

单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在给定的场景下,有没有按照期望工作。另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。

在 Go 语言里有几种方法写单元测试。基础测试(basic test)只使用一组参数和结果来测试一段代码。表组测试(table test)也会测试一段代码,但是会使用多组参数和结果进行测试。也可以使用一些方法来模仿(mock)测试代码需要使用到的外部资源,如数据库或者网络服务器。

// 这个示例程序展示如何写基础单元测试
package listing01

import (
   "net/http"
   "testing"
)

const chechMark = "\u2713"
const ballotX = "\u2717"

//TestDownload确认http包的Get函数可以下载内容
func TestDownload(t *testing.T) {
   url := "http://www.goinggo.net/feeds/posts/default?alt=rss"
   statusCode := 200

t.Log("Given the need to test downloadig content.")
   {
      t.Logf("\tWhen checking \"%s\" for status code \"%d\"",
         url, statusCode)
      {
         resp, err := http.Get(url)
         if err != nil {
            t.Fatal("\t\tShould be able to make the Get call.",
               ballotX, err)
         }
         t.Log("\t\tShould be able to make the Get call.",
            checkMark)

defer resp.Body.Close()

if resp.StatusCode == statusCode {
            t.Logf("\t\tShould receive a \"%d\" status. %v",
               statusCode, checkMark)
         } else {
            t.Errorf("\t\tShould receive a \"%d\" status. %v %v",
               statusCode, ballotX, resp.StatusCode")
         }
      }
   }
}

展示了测试 http 包的 Get 函数的单元测试。测试的内容是确保可以从网络正常下载 goinggo.net 的 RSS 列表。通过调用 go test -v 来运行这个测试( -v 表示提供冗余输出)。

9.1.2 表组测试

如果测试可以接受一组不同的输入并产生不同的输出的代码,那么应该使用表组测试的方法进行测试。

9.1.3 模仿调用

标准库包含一个名为 httptest 的包,它让开发人员可以模仿基于HTTP 的网络调用。

9.3 基准测试

基准测试是一种测试代码性能的方法。

9.4 小结

测试功能被内置到 Go 语言中,Go 语言提供了必要的测试工具;go test 工具用来运行测试;测试文件总是以_test.go 作为文件名的结尾;表组测试是利用一个测试函数测试多组值的好办法;包中的示例代码,既能用于测试,也能用于文档;基准测试提供了探查代码性能的机制。

《Go语言实战》William Kennedy中文版学习笔记相关推荐

  1. 拉勾启源老师mysql讲义,【拉勾教育数据分析实战训练营】--Tableau学习笔记-重点回顾1...

    [拉勾教育数据分析实战训练营]--Tableau学习笔记-重点回顾1 [拉勾教育数据分析实战训练营]--Tableau学习笔记-重点回顾1 以下是我搜罗的一些官方优秀case分享: 1.https:/ ...

  2. 《Node.js开发实战详解》学习笔记

    <Node.js开发实战详解>学习笔记 --持续更新中 一.NodeJS设计模式 1 . 单例模式 顾名思义,单例就是保证一个类只有一个实例,实现的方法是,先判断实例是否存在,如果存在则直 ...

  3. 《密码编码学与网络安全》William Stalling著---学习笔记(二)【知识点速过】【数字签名+密钥管理分发+用户认证】

    提示:博文有点长,请保持耐心哦~ 前一篇文章: <密码编码学与网络安全>William Stalling著-学习笔记(一)[知识点速过][传统密码+经典对称加密算法+经典公钥密码算法+密码 ...

  4. 《密码编码学与网络安全》William Stalling著---学习笔记(三)【知识点速过】【网络安全与Internet安全概览】

    提示:博文有点长,请保持耐心哦~ 前两篇文章: <密码编码学与网络安全>William Stalling著-学习笔记(一)[知识点速过][传统密码+经典对称加密算法+经典公钥密码算法+密码 ...

  5. 《MySQL实战45讲》——学习笔记04-05 “深入浅出索引、最左前缀原则、索引下推优化“

    04 | 深入浅出索引(上) 1. 什么是索引? 索引的出现其实就是为了提高数据查询的效率,就像书的目录一样,书有500页,每页存的都是书的内容,目录可能只有5页,只存了页码:通过目录能快速找到某个主 ...

  6. 《MySQL实战45讲》——学习笔记12 “InnoDB刷脏页的控制策略“

    本篇介绍MYSQL InnoDB的WAL机制带来的小问题--利用WAL技术,数据库将随机写转换成了顺序写,大大提升了数据库的性能,但也带来了内存脏页的问题: 脏页会被后台线程自动flush,也会由于数 ...

  7. 《MySQL实战45讲》——学习笔记01-03 “MySQL基本架构、日志系统、事务隔离“

    最近有新闻说"丁奇"炒股失败欠债,赶紧去极客时间买了他的<MySQL 实战 45 讲>以防下架,顺带重新系统的复习下MYSQL相关知识,记录下学习笔记: 本篇介绍: M ...

  8. 《智能对话机器人开发实战20讲》--学习笔记--AIML基础功能拓展-与互联网的集成

    一.学习笔记 环境要求: aiml bs4 语料库: tuling.aiml search_web.aiml <that>WHICH SEARCH ENGINE WOULD YOU LIK ...

  9. 《机器学习实战》第二章学习笔记:K-近邻算法(代码详解)

    <机器学习实战>数据资料以及总代码可以去GitHub中下载: GitHub代码地址:https://github.com/yangshangqi/Machine-Learning-in-A ...

  10. Web协议详解与抓包实战之HTTP1.1 学习笔记【一】

    Web协议详解与抓包实战之HTTP1.1[一] 前言 <Web协议详解与抓包实战>课程学习,陶辉老师主讲 学习内容: HTTP–TLS/SSL–TCP/IP自上而下根据应用学习web协议H ...

最新文章

  1. 最大公约数与最小公约数!_只愿与一人十指紧扣_新浪博客
  2. C++_泛型编程与标准库(六)
  3. 《七笔勾》--陕北风光
  4. Maven 多模块项目,多个root解决方法
  5. 做计算机工作的要专用手机吗,怎么在手机上完成工作?原来没有电脑手机还可以这样用...
  6. GitHub上如何创建文件夹
  7. java 清单文件 生成,使用批处理文件生成文件列表清单
  8. 消息中间件原理及JMS简介之二
  9. 小苹果 html,定时轮播.html
  10. ps怎么一下选中多个图层_ps新手入门之蒙版工具
  11. html5qq空间代码作业,免费QQ空间背景代码大全(高手整理)
  12. Android Facebook登陆获取 Key Hashes值
  13. 语音信号短时时域分析
  14. wps中的格式化快捷键
  15. diyer 电脑_每个DIYer应该拥有的基本工具
  16. Hack The Box - Catch 利用let chat API查询信息,Cachet配置泄露漏洞获取ssh登录密码,apk代码注入漏洞利用获取root权限
  17. 亚马逊,富士康,腾讯等大厂的3亿美元打水漂,Android之父公司宣布关闭
  18. 核心交换机的链路聚合、冗余、堆叠、热备份如何理解与配置
  19. 《转》创新团队中常见的几种“怪人”
  20. 计算方法的稳定性 | 误差来源之舍入误差 | 数值计算基本原则

热门文章

  1. css半透明渐变过渡效果
  2. 泛微oa部署linux步骤,泛微oa部署微搜功能手册
  3. Oracle数据库的下载安装教程
  4. 高德地图:No implementation found for void com.autonavi.ae.gmap.GLMapEngine.nativeInitParam
  5. 离线安装SilverLight
  6. JS 幻灯片代码(含自动播放)
  7. 微软最近对外发布了必应翻译应用开发接口(API),Facebook成为第一批尝鲜者...
  8. P2422 良好的感觉
  9. ping命令使用集合
  10. 办公自动化——Python操作Excel案例