精华内容
参与话题
问答
  • Go语言实战完整版PDF

    2017-12-26 20:44:33
    Go语言结合了底层系统语言的能力以及现代语言的高级特性,旨在降低构建简单、可靠、高效软件的门槛。本书向读者提供一个专注、全面且符合语言习惯的视角。Go语言实战同时关注语言的规范和实现,涉及的内容包括语法、...
  • GO语言教程,中文带目录! GO语言教程,中文带目录! GO语言教程,中文带目录! GO语言教程,中文带目录! GO语言教程,中文带目录!
  • Go语言实战》.pdf

    2018-01-06 07:04:23
    go语言
  • Go语言实战 .pdf

    2018-02-11 14:25:02
    Go语言实战 .pdf Go语言实战 .pdf Go语言实战 .pdf Go语言实战 .pdf
  • 第1章 Go语言的介绍 本章主要内容:用Go解决现代计算难题,使用 Go 语言工具。C 和 C++这类语言提供了很快的执行速度,而 Ruby 和 Python 这类语言则擅长快速开发。Go 语言在这两者间架起了桥梁,不仅提供了高性能的...

    第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语言入门-《Go语言实战》学习笔记

    千次阅读 2018-07-30 17:11:47
    Go语言实战》 《Go程序设计语言》 《Go Web编程》作者还翻译了Go标准库文档 安装 Go安装包下载 环境变量配置 在/etc/profile文件中添加 export PATH=$PATH:/usr/local/go/bin 执行命令立即生效 ...

    学习资料

    安装

    环境变量配置

    • 在/etc/profile文件中添加
    export PATH=$PATH:/usr/local/go/bin
    • 执行命令立即生效
    source /etc/profile
    • 输入命令查看配置是否成功
    go

    安装DELVE

    1. 安装Homebrew,执行命令

    /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

    2. 安装成功后,执行

    brew install go-delve/delve/delve

    Visual Studio Code插件安装

    设置代理

    首选项-设置

    http.proxy

    需要安装的插件,会自动在右下角弹出提示,具体插件如下

    Installing 9 tools at /Users/mac/go/bin
      gocode
      gopkgs
      go-outline
      go-symbols
      guru
      gorename
      godef
      goreturns
      golint

    开发工具

    终端设置代理

    $ export http_proxy=http://proxyAddress:port
    
    $ export ALL_PROXY=socks5://127.0.0.1:1080 

    原文:让终端走代理的几种方法

    Go语言简介

    第3章打包和工具链

    3.1包

    • 同一个目录下的所有.go文件必须声明同一个包名

    3.1.2 main包

    • 在Go语言里,命名为main的包具有特殊的含义。Go语言的编译程序会试图把这种名字的包编译为二进制可执行文件。main()函数是程序的入口,程序编译时,会使用声明main包的代码所在的目录的目录名作为二进制可执行文件的文件名。
    • 使用“远程导入”时,请确保您的网络畅通无阻。
    import (
       // 远程导入
        "github.com/goinaction/code/chapter2/sample/search"
    )

    3.2.2 命名导入

    • 导入的多个包具有相同的名字,可以使用命名导入:在import语句给出的包路径的左侧定义一个名字,将导入的包命名为新名字。
    package main
    
    import (
       "fmt"
       myfmt "mylib/fmt"
    )
    • 当你导入了一个不在代码里使用的包时,Go编译器会编译失败,并输出一个错误。目的是避免代码变的臃肿。
    • 有时,用户可能需要导入一个包,但是不需要引用这个包的标识符。在这种情况,可以使用空白标识符_来重命名这个导入。

    空标识符

    下划线字符(_)在Go语言有很多用法。用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不兴趣的值。

    3.3 函数init

    • 每个包可以包含任意多个init函数,会在main函数之前执行。
    func init() {
        // 执行操作
    }

    3.4 使用Go的工具

    • 使用命令执行编译和清理工作
    go build
    go clean

    3.5.1 go vet

    • 检测代码的常见错误

    3.7.2 对gb的介绍

    • 依赖管理
    • gb工程会区分开发人员写的代码和开发人员需要依赖的代码。
    • gb工程与Go官方工具链(包括go get)并不兼容。因为gb不需要设置GOPATH,而Go工具链无法理解gb工程的目录结构。

    第4章 数组、切片和映射

    数组

    • 由于数组占用的内存是连续分配的,CPU能把正在使用的数据缓存更久的时间。

    声明和初始化

    • 在Go语言中声明变量时,总和使用对应类型的零值来对变量进行初始化。
    // 声明一个包含5个元素的整型数组
    var array [5]int
    
    // 声明一个包含5个元素的整型数组
    // 用具体值初始化每个元素
    array := [5]int{10, 20, 30, 40, 50}
    • 如果使用…代替数组的长度,Go语言会根据初始化时数组元素的数量来确定该数组的长度。
    // 声明一个整型数组
    // 用具体值初始化每个元素
    // 容量由初始化值的数量决定
    array := [...] int{10, 20, 30, 40, 50}
    
    // 声明一个有5个元素的数组
    // 用具体值初始化索引为1和2的元素
    // 其余元素保持零值
    array := [5]int{1: 10, 2: 20}

    切片

    • 切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。

    创建和初始化

    1. 使用make函数创建切片,需要传入一个参数,指定切片的长度。
    • 如果只指定长度,那么切片的容量和长度相等。也可以分别指定长度和容量。
    // 创建一个字符串切片
    // 其长度和容量都是5个元素
    slice := make([] string, 5)
    • 分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素。
    // 创建一个整型切片
    // 期长度为3个元素,容量为5个元素
    slice := make([] int, 3, 5)
    2. 使用切片字面量创建切片,这种方法和创建数组类似,只是不需要指定[]运算符里的值。初始的长度和容量会基于初始化时提供的元素的个数确定。
    // 创建字符串切片
    // 期长度和容量都是5个元素
    slice := [] string {"Red", "Blue", "Green", "Yellow", "Pink"}
    • 可以设置初始长度和容量:在初始化时给出所需的长度和容量作为索引。
    // 创建字符串切片
    // 使用空字符串初始化第100个元素
    slice := [] string{99: ""}
    • 如果在[]运算符里指定了一个值,那么创建的就是数组而不是切片。

    映射

    • Map数据结构

    第5章 Go语言的类型系统

    定义类型的方法

    使用struct关键字

    // user在程序里定义一个用户类型
    type user struct{
        name        string
        email       string
        ext         int
        privileged  bool
    }
    
    // 声明user类型的变量,并初始化所有字段
    lisa := admin{
        name:       "Lisa", 
        email:      "lisa@email.com",
        ext:        123,
        privileged: true,
    }
    
    // 也可以这样
    // 声明user类型的变量
    lisa := user{"Lisa", "lisa@email.com", 123, true}

    零值

    • 数值型:0
    • 字符串:空字符串
    • 布尔类型:false

    第6章 并发

    并发与并行的概念

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

    goroutine

    • 一个线程可以创建多个goroutine

    锁住共享资源

    1. 原子函数
    2. 互斥锁
    3. 通道

    第7章 并发模式

    • runner
    • pool
    • work
    展开全文
  • Go 语言实战

    2018-04-12 10:41:40
    尽管这门语言借鉴了很多其他语言的思想,但是凭借自身统一和自然的表达,Go 程序在本质上完全不同于用其他语言编写的程序。Go 平衡了底层系统语言的能力,以及在现代语言中所见到的高级特性。你可以依靠 Go 语言来...

    内容介绍

    Go 是一门开源的编程语言,目的在于降低构建简单、可靠、高效软件的门槛。尽管这门语言借鉴了很多其他语言的思想,但是凭借自身统一和自然的表达,Go 程序在本质上完全不同于用其他语言编写的程序。Go 平衡了底层系统语言的能力,以及在现代语言中所见到的高级特性。你可以依靠 Go 语言来构建一个非常快捷、高性能且有足够控制力的编程环境。使用 Go 语言,可以写得更少,做得更多。

    本书是写给已经有一定其他语言编程经验,并且想学习 Go 语言的中级开发者的。我们写这本书的目的是,为读者提供一个专注、全面且符合语言习惯的视角。我们同时关注语言的规范和实现,涉及的内容包括语法、类型系统,并发、通道、测试以及其他一些主题。我们相信,对于刚开始学 Go 语言的人,以及想要深入了解这门语言内部实现的人来说,本书都是极佳的选择。

    译者简介

    李兆海,多年专注于后端分布式网络服务开发,曾使用过多个流行后端技术和相关架构实践,是 Go 语言和 Docker 的早期使用者和推广者,《第一本 Docker 书》的译者。作为项目技术负责人,成功开发了百万用户级直播系统。

    课程内容

    译者序

    Go 语言是由谷歌公司在2007年开始开发的一门语言,目的是能在多核心时代高效编写网络应用程序。Go 语言的创始人 Robert Griesemer、Rob Pike 和 Ken Thompson 都是在计算机发展过程中作出过重要贡献的人。自从2009年11月正式公开发布后,Go 语言迅速席卷了整个互联网后端开发领域,其社区里不断涌现出类似 vitess、Docker、etcd、Consul 等重量级的开源项目。

    在 Go 语言发布后,我就被其简洁、强大的特性所吸引,并于2010年开始在技术聚会上宣传 Go 语言,当时所讲的题目是《Go 语言:互联网时代的 C》。现在看来,Go 语言确实很好地解决了互联网时代开发的痛点,而且入门门槛不高,是一种上手容易、威力强大的工具。试想一下,不需要学习复杂的异步逻辑,使用习惯的顺序方法,就能实现高性能的网络服务,并充分利用系统的多个核心,这是多么美好的一件事情。

    本书是国外 Go 社区多年经验积累的成果。本书默认读者已经具有一定的编程基础,希望更好地使用 Go 语言。全书以示例为基础,详细介绍了 Go 语言中的一些比较深入的话题。对于有经验的程序员来说,很容易通过学习书中的例子来解决自己实际工作中遇到的问题。辅以文字介绍,读者会对相关问题有更系统的了解和认识。翻译过程中我尽量保持了原书的叙述方法,并加强了叙述逻辑,希望读者会觉得清晰易读。

    在翻译本书的过程中,感谢人民邮电出版社编辑杨海玲老师的指导和进度安排,让本书能按时与读者见面。感谢谢孟军对译稿的审校,你的润色使译文读起来流畅了很多。尤其要感谢我老婆对我的支持,感谢你能理解我出于热爱才会“匍匐”在计算机前码字。

    最后,感谢读者购买此书。希望读者在探索 Go 语言的道路上,能够享受到和我一样的乐趣。

    在计算机科学领域,提到不同寻常的人,总会有一些名字会闪现在你的脑海中。Rob Pike、Robert Griesmier 和 Ken Thompson 就是其中几个。他们3个人负责构建过 UNIX、Plan 9、B、Java的JVM HotSpot、V8、Strongtalk、Sawzall、Ed、Acme 和 UTF8,此外还有很多其他的创造。在2007年,这3个人凑在一起,尝试一个伟大的想法:综合他们多年的经验,借鉴已有的语言,来创建一门与众不同的、全新的系统语言。他们随后以开源的形式发布了自己的实验成果,并将这种语言命名为“Go”。如果按照现在的路线发展下去,这门语言将是这3个人最有影响的一项创造。

    当人们聚在一起,纯粹是为了让世界变得更好的时候,往往也是他们处于最佳状态的时候。在2013年,为了围绕 Go 语言构建一个更好的社区,Brian 和 Erik 联合成立了 Gopher Academy,没过多久,Bill 和其他一些有类似想法的人也加入进来。他们首先注意到,社区需要有一个地方可以在线聚集和分享素材,所以他们在 slack 创立了 Go 讨论版和 Gopher Academy 博客。随着时间的推移,社区越来越大,他们创建了世界上第一个全球 Go 语言大会—GopherCon。随着与社区更深入地交流,他们意识到还需要为广大想学习这门新语言的人提供一些资源,所以他们开始着手写一本书,就是现在你手里拿的这本书。

    为 Go 社区贡献了大量的时间和精力的3位作者,出于对 Go 语言社区的热爱写就了这本书。我曾在 Bill、Brian 和 Erik 身边,见证了他们在不同的环境和角色(作为 Gopher Academy 博客的编辑,作为大会组织者,甚至是在他们的日常工作中,作为父亲和丈夫)下,都会认真负责地撰写和修订本书。对他们来说,这不仅仅是一本书,也是对他们心爱的语言的献礼。他们并不满足于写就一本“好”书。他们编写、审校,再写、再修改,再三推敲每页文字、每个例子、每一章,直到认为本书的内容配得上他们珍视的这门语言。

    离开一门使用舒服、掌握熟练的语言,去学习一门不仅对自己来说,对整个世界来说都是全新的语言,是需要勇气的。这是一条人迹罕至,沿途充满 bug,只有少数先行者熟悉的路。这里充满了意外的错误,文档不明确或者缺失,而且缺少可以拿来即用的代码库。这是拓荒者、先锋才会选择的道路。如果你正在读这本书,那么你可能正在踏上这段旅途。

    本书自始至终是为你—本书的读者精心制作的一本探索、学习和使用 Go 语言的简洁而全面的指导手册。在全世界,你也不会找到比 Bill、Brian 和 Erik 更好的导师了。我非常高兴你能开始探索 Go 语言的优点,期望能在线上和线下大会上遇到你。

    Steve Francia
    Go 语言开发者,Hugo、Cobra、Viper 和 SPF13-VIM 的创建人


    一个高性能强类型的Smalltalk实现。——译者注

    前言

    那是2013年10月,我刚刚花几个月的时间写完 GoingGo.net 博客,就接到了 Brian Ketelsen 和 Erik St. Martin 的电话。他们正在写这本书,问我是否有兴趣参与进来。我立刻抓住机会,参与到写作中。当时,作为一个 Go 语言的新手,这是我进一步了解这门语言的好机会。毕竟,与 Brian 和 Erik 一起工作、一起分享获得的知识,比我从构建博客中学到的要多得多。

    完成前4章后,我们在 Manning 早期访问项目(MEAP)中发布了这本书。很快,我们收到了来自语言团队成员的邮件。这位成员对很多细节提供了评审意见,还附加了大量有用的知识、意见、鼓励和支持。根据这些评审意见,我们决定从头开始重写第2章,并对第4章进行了全面修订。据我们所知,对整章进行重写的情况并不少见。通过这段重写的经历,我们学会要依靠社区的帮助来完成写作,因为我们希望能立刻得到社区的支持。

    自那以后,这本书就成了社区努力的成果。我们投入了大量的时间研究每一章,开发样例代码,并和社区一起评审、讨论并编辑书中的材料和代码。我们尽了最大的努力来保证本书在技术上没有错误,让代码符合通用习惯,并且使用社区认为应该有的方式来教 Go 语言。同时,我们也融入了自己的思考、自己的实践和自己的指导方式。

    我们希望本书能帮你学习 Go 语言,不仅是当下,就是多年以后,你也能从本书中找到有用的东西。Brian、Erik 和我总会在线上帮助那些希望得到我们帮助的人。如果你购买了本书,谢谢你,来和我们打个招呼吧。

    William Kennedy

    第01章:关于 Go 语言的介绍

    本章主要内容

    • 用 Go 语言解决现代计算难题
    • 使用 Go 语言工具

    计算机一直在演化,但是编程语言并没有以同样的速度演化。现在的手机,内置的 CPU 核数可能都多于我们使用的第一台电脑。高性能服务器拥有64核、128核,甚至更多核。但是我们依旧在使用为单核设计的技术在编程。

    编程的技术同样在演化。大部分程序不再由单个开发者来完成,而是由处于不同时区、不同时间段工作的一组人来完成。大项目被分解为小项目,指派给不同的程序员,程序员开发完成后,再以可以在各个应用程序中交叉使用的库或者包的形式,提交给整个团队。

    如今的程序员和公司比以往更加信任开源软件的力量。Go 语言是一种让代码分享更容易的编程语言。Go 语言自带一些工具,让使用别人写的包更容易,并且 Go 语言也让分享自己写的包更容易。

    在本章中读者会看到 Go 语言区别于其他编程语言的地方。Go 语言对传统的面向对象开发进行了重新思考,并且提供了更高效的复用代码的手段。Go 语言还让用户能更高效地利用昂贵服务器上的所有核心,而且它编译大型项目的速度也很快。

    在阅读本章时,读者会对影响 Go 语言形态的很多决定有一些认识,从它的并发模型到快如闪电的编译器。我们在前言中提到过,这里再强调一次:这本书是写给已经有一定其他编程语言经验、想学习 Go 语言的中级开发者的。本书会提供一个专注、全面且符合习惯的视角。我们同时专注语言的规范和实现,涉及的内容包括语法、Go 语言的类型系统、并发、通道、测试以及其他一些非常广泛的主题。我们相信,对刚开始要学习 Go 语言和想要深入了解语言内部实现的人来说,本书都是最佳选择。

    本书示例中的源代码可以在这里下载。

    我们希望读者能认识到,Go 语言附带的工具可以让开发人员的生活变得更简单。最后,读者会意识到为什么那么多开发人员用 Go 语言来构建自己的新项目。

    1.1 用Go解决现代编程难题

    Go 语言开发团队花了很长时间来解决当今软件开发人员面对的问题。开发人员在为项目选择语言时,不得不在快速开发和性能之间做出选择。C 和 C++这类语言提供了很快的执行速度,而 Ruby 和 Python 这类语言则擅长快速开发。Go 语言在这两者间架起了桥梁,不仅提供了高性能的语言,同时也让开发更快速。

    在探索 Go 语言的过程中,读者会看到精心设计的特性以及简洁的语法。作为一门语言,Go 不仅定义了能做什么,还定义了不能做什么。Go 语言的语法简洁到只有几个关键字,便于记忆。Go 语言的编译器速度非常快,有时甚至会让人感觉不到在编译。所以,Go 开发者能显著减少等待项目构建的时间。因为 Go 语言内置并发机制,所以不用被迫使用特定的线程库,就能让软件扩展,使用更多的资源。Go 语言的类型系统简单且高效,不需要为面向对象开发付出额外的心智,让开发者能专注于代码复用。Go 语言还自带垃圾回收器,不需要用户自己管理内存。让我们快速浏览一下这些关键特性。

    1.1.1 开发速度

    编译一个大型的 C 或者 C++项目所花费的时间甚至比去喝杯咖啡的时间还长。图1-1是 XKCD 中的一幅漫画,描述了在办公室里开小差的经典借口。

    enter image description here

    Go 语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。编译 Go 程序时,编译器只会关注那些直接被引用的库,而不是像 Java、C 和 C++那样,要遍历依赖链中所有依赖的库。因此,很多 Go 程序可以在1秒内编译完。在现代硬件上,编译整个 Go 语言的源码树只需要20秒。

    因为没有从编译代码到执行代码的中间过程,用动态语言编写应用程序可以快速看到输出。代价是,动态语言不提供静态语言提供的类型安全特性,不得不经常用大量的测试套件来避免在运行的时候出现类型错误这类 bug。

    想象一下,使用类似 JavaScript 这种动态语言开发一个大型应用程序,有一个函数期望接收一个叫作ID的字段。这个参数应该是整数,是字符串,还是一个 UUID?要想知道答案,只能去看源代码。可以尝试使用一个数字或者字符串来执行这个函数,看看会发生什么。在 Go 语言里,完全不用为这件事情操心,因为编译器就能帮用户捕获这种类型错误。

    1.1.2 并发

    作为程序员,要开发出能充分利用硬件资源的应用程序是一件很难的事情。现代计算机都拥有多个核,但是大部分编程语言都没有有效的工具让程序可以轻易利用这些资源。这些语言需要写大量的线程同步代码来利用多个核,很容易导致错误。

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

    1.goroutine

    goroutine 是可以与其他 goroutine 并行执行的函数,同时也会与主程序(程序的入口)并行执行。在其他编程语言中,你需要用线程来完成同样的事情,而在 Go 语言中会使用同一个线程来执行多个 goroutine。例如,用户在写一个 Web 服务器,希望同时处理不同的 Web 请求,如果使用 C 或者 Java,不得不写大量的额外代码来使用线程。在 Go 语言中,net/http 库直接使用了内置的 goroutine。每个接收到的请求都自动在其自己的 goroutine 里处理。goroutine 使用的内存比线程更少,Go 语言运行时会自动在配置的一组逻辑处理器上调度执行 goroutine。每个逻辑处理器绑定到一个操作系统线程上(见图1-2)。这让用户的应用程序执行效率更高,而开发工作量显著减少。

    enter image description here

    如果想在执行一段代码的同时,并行去做另外一些事情,goroutine 是很好的选择。下面是一个简单的例子:

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

    关键字go是唯一需要去编写的代码,调度log函数作为独立的 goroutine 去运行,以便与其他 goroutine 并行执行。这意味着应用程序的其余部分会与记录日志并行执行,通常这种并行能让最终用户觉得性能更好。就像之前说的,goroutine 占用的资源更少,所以常常能启动成千上万个 goroutine。我们会在第6章更加深入地探讨 goroutine 和并发。

    2.通道

    通道是一种数据结构,可以让 goroutine 之间进行安全的数据通信。通道可以帮用户避免其他语言里常见的共享内存访问的问题。

    并发的最难的部分就是要确保其他并发运行的进程、线程或 goroutine 不会意外修改用户的数据。当不同的线程在没有同步保护的情况下修改同一个数据时,总会发生灾难。在其他语言中,如果使用全局变量或者共享内存,必须使用复杂的锁规则来防止对同一个变量的不同步修改。

    为了解决这个问题,通道提供了一种新模式,从而保证并发修改时的数据安全。通道这一模式保证同一时刻只会有一个 goroutine 修改数据。通道用于在几个运行的 goroutine 之间发送数据。在图1-3中可以看到数据是如何流动的示例。想象一个应用程序,有多个进程需要顺序读取或者修改某个数据,使用 goroutine 和通道,可以为这个过程建立安全的模型。

    enter image description here

    图1-3中有3个 goroutine,还有2个不带缓存的通道。第一个 goroutine 通过通道把数据传给已经在等待的第二个 goroutine。在两个 goroutine 间传输数据是同步的,一旦传输完成,两个 goroutine 都会知道数据已经完成传输。当第二个 goroutine 利用这个数据完成其任务后,将这个数据传给第三个正在等待的 goroutine。这次传输依旧是同步的,两个 goroutine 都会确认数据传输完成。这种在 goroutine 之间安全传输数据的方法不需要任何锁或者同步机制。

    需要强调的是,通道并不提供跨 goroutine 的数据访问保护机制。如果通过通道传输数据的一份副本,那么每个 goroutine 都持有一份副本,各自对自己的副本做修改是安全的。当传输的是指向数据的指针时,如果读和写是由不同的 goroutine 完成的,每个 goroutine 依旧需要额外的同步动作。

    1.1.3 Go 语言的类型系统

    Go 语言提供了灵活的、无继承的类型系统,无需降低运行性能就能最大程度上复用代码。这个类型系统依然支持面向对象开发,但避免了传统面向对象的问题。如果你曾经在复杂的 Java 和 C++程序上花数周时间考虑如何抽象类和接口,你就能意识到 Go语言的类型系统有多么简单。Go 开发者使用组合(composition)设计模式,只需简单地将一个类型嵌入到另一个类型,就能复用所有的功能。其他语言也能使用组合,但是不得不和继承绑在一起使用,结果使整个用法非常复杂,很难使用。在 Go 语言中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型。

    另外,Go 语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。在 Go 语言中,不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口。Go 标准库里的很多接口都非常简单,只开放几个函数。从实践上讲,尤其对那些使用类似 Java 的面向对象语言的人来说,需要一些时间才能习惯这个特性。

    1.类型简单

    Go 语言不仅有类似intstring这样的内置类型,还支持用户定义的类型。在 Go 语言中,用户定义的类型通常包含一组带类型的字段,用于存储数据。Go 语言的用户定义的类型看起来和 C 语言的结构很像,用起来也很相似。不过 Go 语言的类型可以声明操作该类型数据的方法。传统语言使用继承来扩展结构——Client 继承自 User,User 继承自 Entity,Go 语言与此不同,Go 开发者构建更小的类型——Customer 和 Admin,然后把这些小类型组合成更大的类型。图1-4展示了继承和组合之间的不同。

    enter image description here

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

    接口用于描述类型的行为。如果一个类型的实例实现了一个接口,意味着这个实例可以执行一组特定的行为。你甚至不需要去声明这个实例实现某个接口,只需要实现这组行为就好。其他的语言把这个特性叫作鸭子类型——如果它叫起来像鸭子,那它就可能是只鸭子。Go 语言的接口也是这么做的。在 Go 语言中,如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明。

    在类似 Java 这种严格的面向对象语言中,所有的设计都围绕接口展开。在编码前,用户经常不得不思考一个庞大的继承链。下面是一个 Java 接口的例子:

    interface User {    public void login();    public void logout();}

    在 Java 中要实现这个接口,要求用户的类必须满足User接口里的所有约束,并且显式声明这个类实现了这个接口。而 Go 语言的接口一般只会描述一个单一的动作。在 Go 语言中,最常使用的接口之一是io.Reader。这个接口提供了一个简单的方法,用来声明一个类型有数据可以读取。标准库内的其他函数都能理解这个接口。这个接口的定义如下:

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

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

    这和传统的面向对象编程语言的接口系统有本质的区别。Go 语言的接口更小,只倾向于定义一个单一的动作。实际使用中,这更有利于使用组合来复用代码。用户几乎可以给所有包含数据的类型实现io.Reader接口,然后把这个类型的实例传给任意一个知道如何读取io.Reader的 Go 函数。

    Go 语言的整个网络库都使用了io.Reader接口,这样可以将程序的功能和不同网络的实现分离。这样的接口用起来有趣、优雅且自由。文件、缓冲区、套接字以及其他的数据源都实现了io.Reader接口。使用同一个接口,可以高效地操作数据,而不用考虑到底数据来自哪里。

    1.1.4 内存管理

    不当的内存管理会导致程序崩溃或者内存泄漏,甚至让整个操作系统崩溃。Go 语言拥有现代化的垃圾回收机制,能帮你解决这个难题。在其他系统语言(如 C 或者 C++)中,使用内存前要先分配这段内存,而且使用完毕后要将其释放掉。哪怕只做错了一件事,都可能导致程序崩溃或者内存泄漏。可惜,追踪内存是否还被使用本身就是十分艰难的事情,而要想支持多线程和高并发,更是让这件事难上加难。虽然 Go 语言的垃圾回收会有一些额外的开销,但是编程时,能显著降低开发难度。Go 语言把无趣的内存管理交给专业的编译器去做,而让程序员专注于更有趣的事情。

    1.2 你好,Go

    感受一门语言最简单的方法就是实践。让我们看看用 Go 语言如何编写经典的Hello World!应用程序:

    enter image description here

    运行这个示例程序后会在屏幕上输出我们熟悉的一句话。但是怎么运行呢?无须在机器上安装 Go 语言,在浏览器中就可以使用几乎所有 Go 语言的功能。

    介绍 Go Playground

    Go Playground 允许在浏览器里编辑并运行Go语言代码。在浏览器中打开http://play.golang.org。浏览器里展示的代码是可编辑的(见图1-5)。点击 Run,看看会发生什么。

    enter image description here

    可以把输出的问候文字改成别的语言。试着改动fmt.Println()里面的文字,然后再次点击 Run。

    分享Go代码
      Go 开发者使用 Playground 分享他们的想法,测试理论,或者调试代码。你也可以这么做。每次使用 Playground 创建一个新程序之后,可以点击 Share 得到一个用于分享的网址。
    任何人都能打开这个链接。试试http://play.golang.org/p/EWIXicJdmz

    要给想要学习写东西或者寻求帮助的同事或者朋友演示某个想法时,Go Playground 是非常好的方式。在 Go 语言的 IRC 频道、Slack 群组、邮件列表和 Go 开发者发送的无数邮件里,用户都能看到创建、修改和分享 Go Playground 上的程序。

    1.3 小结

    • Go 语言是现代的、快速的,带有一个强大的标准库。
    • Go 语言内置对并发的支持。
    • Go 语言使用接口作为代码复用的基础模块。
    第02章:快速开始一个 Go 程序(上)
    第02章:快速开始一个 Go 程序(下)
    第03章:打包和工具链 (上)
    第03章:打包和工具链 (下)
    第04章:数组、切片和映射 (上)
    第04章:数组、切片和映射 (中)
    第04章:数组、切片和映射 (下)
    第05章:Go 语言的类型系统(上)
    第05章:Go 语言的类型系统(中)
    第05章:Go 语言的类型系统(下)
    第06章:并发(上)
    第06章:并发(中)
    第06章:并发(下)
    第07章:并发模式(上)
    第07章:并发模式(中)
    第07章:并发模式(下)
    第08章:标准库 (上)
    第08章:标准库 (中)
    第08章:标准库 (下)
    第09章:测试和性能(上)
    第09章:测试和性能(中)
    第09章:测试和性能(下)

    阅读全文: http://gitbook.cn/gitchat/column/5a1e5ce8e33e7b6b9c1b5540

    展开全文
  • Go语言实战-- 通道

    千次阅读 2017-07-03 22:20:15
    上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写,但是呢,它们还是有点复杂,而且影响性能,对此,Go又为我们提供了一种工具,这就是通道。 所以在多个goroutine并发中,我们不仅可以通过原子函数和...

    上一篇我们讲的原子函数和互斥锁,都可以保证共享数据的读写,但是呢,它们还是有点复杂,而且影响性能,对此,Go又为我们提供了一种工具,这就是通道。

    所以在多个goroutine并发中,我们不仅可以通过原子函数和互斥锁保证对共享资源的安全访问,消除竞争的状态,还可以通过使用通道,在多个goroutine发送和接受共享的数据,达到数据同步的目的。

    通道,他有点像在两个routine之间架设的管道,一个goroutine可以往这个管道里塞数据,另外一个可以从这个管道里取数据,有点类似于我们说的队列。

    声明一个通道很简单,我们使用 chan 关键字即可,除此之外,还要指定通道中发送和接收数据的类型,这样我们才能知道,要发送什么类型的数据给通道,也知道从这个通道里可以接收到什么类型的数据。

    ch:=make(chan int)
    

    通道类型和Map这些类型一样,可以使用内置的 make 函数声明初始化,这里我们初始化了一个 chan int 类型的通道,所以我们只能往这个通道里发送 int 类型的数据,当然接收也只能是 int 类型的数据。

    我们知道,通道是用于在goroutine之间通信的,它具有发送和接收两个操作,而且这两个操作的运算符都是 <- 。

    ch <- 2 //发送数值2给这个通道
    x:=<-ch //从通道里读取值,并把读取的值赋值给x变量
    <-ch //从通道里读取值,然后忽略
    

    看例子,慢慢理解发送和接收的用法。发送操作 <- 在通道的后面,看箭头方向,表示把数值2发送到通道 ch 里;接收操作 <- 在通道的前面,而且是一个一元操作符,看箭头方向,表示从通道 ch 里读取数据。读取的数据可以赋值给一个变量,也可以忽略。

    通道我们还可以使用内置的 close 函数关闭。

    close(ch)
    

    如果一个通道被关闭了,我们就不能往这个通道里发送数据了,如果发送的话,会引起 painc异常。但是,我们还可以接收通道里的数据,如果通道里没有数据的话,接收的数据是 nil 。

    刚刚我们使用 make 函数初始化的时候,只有一个参数,其实 make 还可以有第二个参数,用于指定通道的大小。默认没有第二个参数的时候,通道的大小为0,这种通道也被成为 无缓冲通道

    ch:=make(chan int)
    ch:=make(chan int,0)
    ch:=make(chan int,2)
    

    看例子,其中第一个和第二个初始化是等价的。第三个初始化创建了一个大小为2的通道,这种称为 有缓冲通道 。

    无缓冲的通道

    无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送goroutine和接收goroutine同时准备好,才可以完成发送和接收操作。

    从上面无缓冲的通道定义来看,发送goroutine和接收gouroutine必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。这种无缓冲的通道我们也称之为 同步通道 。

    funcmain() {
    	ch := make(chan int)
    
    	go func() {
    		var sum int = 0
    		for i := 0; i < 10; i++ {
    			sum += i
    		}
    		ch <- sum
    	}()
    	
    	fmt.Println(<-ch)
    
    }
    

    在前面的例子中,我们为了演示goroutine,防止程序提前终止,都是使用 sync.WaitGroup 进行等待,现在的这个例子就不用了,我们使用同步通道来等待。

    在计算sum和的goroutine没有执行完,把值赋给 ch 通道之前, fmt.Println(<-ch) 会一直等待,所以 main 主goroutine就不会终止,只有当计算和的goroutine完成后,并且发送到 ch 通道的操作准备好后,同时 <-ch 就会接收计算好的值,然后打印出来。

    管道

    我们在使用Bash的时候,有个管道操作 | ,它的意思是把上一个操作的输出,当成下一个操作的输入,连起来,做一连串的处理操作。

    ➜  ~ ls |grep 'D'  
    Desktop
    Documents
    Downloads
    

    比如上面这个例子的意思是,先使用 ls 命令,把当前目录下的目录和文件列出来,作为下一个 grep 命令的输入,然后通过 grep 命令,匹配我们需要显示的目录和文件,这里匹配以 D开头的文件名或者目录名。

    其实我们使用通道也可以做到管道的效果,我们只需要把一个通道的输出,当成下一个通道的输入即可。

    funcmain() {
    	one := make(chan int)
    	two := make(chan int)
    
    	go func() {
    		one<-100
    	}()
    
    	go func() {
    		v:=<-one
    		two<-v
    	}()
    
    	fmt.Println(<-two)
    
    }
    

    这里例子中我们定义两个通道 one 和 two ,然后按照顺序,先把100发送给通道 one ,然后用另外一个goroutine从 one 接收值,再发送给通道 two ,最终在主goroutine里等着接收打印 two 通道里的值,这就类似于一个管道的操作,把通道 one 的输出,当成通道 two 的输入,类似于接力赛一样。

    有缓冲的通道

    有缓冲通道,其实是一个队列,这个队列的最大容量就是我们使用 make 函数创建通道时,通过第二个参数指定的。

    ch := make(chan int, 3)
    

    这里创建容量为3的,有缓冲的通道。对于有缓冲的通道,向其发送操作就是向队列的尾部插入元素,接收操作则是从队列的头部删除元素,并返回这个刚刚删除的元素。

    当队列满的时候,发送操作会阻塞;当队列空的时候,接受操作会阻塞。有缓冲的通道,不要求发送和接收操作时同步的,相反可以解耦发送和接收操作。

    想知道通道的容量以及里面有几个元素数据怎么办?其实和 map 一样,使用 cap 和 len 函数就可以了。

    cap(ch)
    len(ch)
    

    cap 函数返回通道的最大容量, len 函数返回现在通道里有几个元素。

    funcmirroredQuery()string {
        responses := make(chan string, 3)
        go func() { responses <- request("asia.gopl.io") }()
        go func() { responses <- request("europe.gopl.io") }()
        go func() { responses <- request("americas.gopl.io") }()
        return <-responses // return the quickest response
    }
    funcrequest(hostnamestring)(responsestring) { /* ... */ }
    

    这是Go语言圣经里比较有意义的一个例子,例子是想获取服务端的一个数据,不过这个数据在三个镜像站点上都存在,这三个镜像分散在不同的地理位置,而我们的目的又是想最快的获取到数据。

    所以这里,我们定义了一个容量为3的通道 responses ,然后同时发起3个并发goroutine向这三个镜像获取数据,获取到的数据发送到通道 responses 中,最后我们使用 return <-responses 返回获取到的第一个数据,也就是最快返回的那个镜像的数据。

    单向通道

    有时候,我们有一些特殊场景,比如限制一个通道只可以接收,但是不能发送;有时候限制一个通道只能发送,但是不能接收,这种通道我们称为单向通道。

    定义单向通道也很简单,只需要在定义的时候,带上 <- 即可。

    var send chan<- int //只能发送
    var receive <-chan int //只能接收
    

    注意 <- 操作符的为止,在后面是只能发送,对应发送操作;在前面是只能接收,对应接收操作。

    单向通道应用于函数或者方法的参数比较多,比如

    funccounter(outchan<-int) {
    }
    

    例子这样的,只能进行发送操作,防止误操作,使用了接收操作,如果使用了接收操作,在编译的时候就会报错的。

    使用通道可以很简单的在goroutine之间共享数据,下一篇会具体介绍一些例子,以便更好的理解并发。

    展开全文
  • Go语言实战

    2019-04-24 08:47:29
    Go语言结合了底层系统语言的能力以及现代语言的高级特性,旨在降低构建简单、可靠、高效软件的门槛。本书向读者提供一个专注、全面且符合语言习惯的视角。Go语言实战同时关注语言的规范和实现,涉及的内容包括语法、...
  • 快速开始一个Go程序 ...Go语言使用了更加智能的编译器,并简化了解决依赖的算法,最终提供了更快的编译速度。编译Go程序时,编译器只会关注那些直接被引用的库,而不是像java、C和C++那样,要办理依赖链中所有...
  • GO语言实战

    2018-01-07 11:43:34
    Go语言结合了底层系统语言的能力以及现代语言的高级特性,旨在降低构建简单、可靠、高效软件的门槛。本书向读者提供一个专注、全面且符合语言习惯的视角。Go语言实战同时关注语言的规范和实现,涉及的内容包括语法、...
  • go语言实战培训视频教程:本课程将给不熟悉go语言或者想进一步研究go的同学提供一个完整的教学视频,全部以代码教学,拒绝ppt。Go语言的环境设置、工具安装、部署、变量、运算符等入门知识,还涉及Go函数、Go的数据...
  • Go语言实战高清完整版,手把手教你用Go语言搭建服务器
  • Go语言实战笔记

    万次阅读 多人点赞 2018-08-03 10:45:26
    Go语言实战笔记(一)| Go包管理 Go语言实战笔记(二)| Go开发工具 Go语言实战笔记(三)| Go Doc 文档 Go语言实战笔记(四)| Go 数组 Go语言实战笔记(五)| Go 切片 Go语言实战笔记(六)| Go Map Go语言...
  • Go语言实战 源码

    2018-12-11 22:34:09
    Go语言实战源码。包含本书的所有章节。Go语言实战源码。包含本书的所有章节。
  • GO语言实战》一书t的各章节的源代码,方便读者课后练习。在很多代码清单中,代码被注释是为了说明关键概念,并且有时在正文中会用数字编号来给出对应代码的其他信息。
  • Go语言实战pdf

    2018-02-05 20:48:00
    下载地址:网盘下载内容简介······Go语言结合了底层系统语言的能力以及现代语言的高级特性,旨在降低构建简单、可靠、高效软件的门槛。本书向读者提供一个专注、全面且符合语言习惯的视角。Go语言实战同时关注...
  • Go语言实战》.pdf

    2020-10-10 08:53:43
    关注“Java后端技术全栈”回复“面试”获取全套面试资料Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。很多程序员表示,用 Go 编程...
  • go语言实战

    2018-03-16 18:38:35
    go语言实战书籍,是go语言中级程序员不错的选择,不太适合初学者
  • GO语言实战项目视频教程分享

空空如也

1 2 3 4 5 ... 20
收藏数 15,688
精华内容 6,275
关键字:

go语言实战