Go语言实战-快速开始一个Go程序

前言

通过例子学习

  1. 如何声明类型、变量、函数、方法
  2. 启动并同步操作goroutine
  3. 使用接口写通用的代码
  4. 处理程序逻辑和错误

功能简介

该例子从不同的数据源拉取数据,将数据与检索项做对比,然后将匹配内容显示在终端窗口。该程序会读取文本文件,进行网络调用,解码XML和JSON成结构化类型数据,并用上并发机制来保障速度。

程序架构

架构图

项目结构

- sample
- data
data.json -- 包含一组数据源
- matchers
rss.go -- 搜索 rss 源的匹配器
- search
default.go -- 搜索数据用的默认匹配器
feed.go -- 用于读取 json 数据文件
match.go -- 用于支持不同匹配器的接口
search.go -- 执行搜索的主控制逻辑
main.go -- 程序的入口

重点讲解

程序入口

程序的入口有两个特征:

  1. 包名为main
  2. 名为main的函数
package main
func main() {}

包的名字类似于命名空间,可间接访问包内声明的标识符。同一个文件夹里的代码文件,必须使用同一个包名。这个特性可把不同包中定义的同名标识符区分开。

注意到下述代码中,包前加下划线。这是为了让Go语言对包做初始化操作(init函数),但是并不使用包里的标识符。Go编译器不允许导入包而不使用,下划线可让编译器接受这类导入。

import (
"log"

_ "github.com/denyu95/go-in-action-code/chapter2/sample/matchers"
"github.com/denyu95/go-in-action-code/chapter2/sample/search"
)

init 函数

init函数都会在main函数执行前调用。

func init() {
log.SetOutput(os.Stdout)
}

编译器如何找包

编译器查找包的时候,总是会到 GOROOT 和 GOPATH 环境变量引用的位置去查找。

GOROOT="/Users/me/go"
GOPATH="/Users/me/spaces/go/projects"

声明类型

声明一个结构类型

type Feed struct {
Name string `json:"site"`
URI string `json:"link"`
Type string `json:"type"`
}

声明最后 ` 引号里的部分被称作标记(tag)。这个标记描述了JSON解码的元数据,每个标记对应JSON文档中指定名字的字段。

{
"site" : "cnn",
"link" : "http://rss.cnn.com/rss/cnn_world.rss",
"type" : "rss"
}

声明变量

  • 变量使用关键字 var声明
var matchers = make(map[string]Matcher)
  • 简化变量声明运算符(:=)
feeds, err := RetrieveFeeds()

(:=)只是一种简化记法,让代码可读性更高。(:=)与 var 无区别。据经验,若声明初始值为零值的变量,应使用 var ;若声明初始值非零值的变量或者使用函数返回值创建变量,应该使用(:=)。

标识符首字母是小写的,代表不对外公开。反之首字母大写代表对外公开。不对外公开的标识符可通过间接的方式访问到。例如,通过函数返回一个未公开的值。

若map变量声明改为如下,会怎么样呢?

var matchers = map[string]Matcher

在使用matchers时会收到报错信息。这是因为map变量的默认零值是nil,所以要先通过make来构造map并将构造后的值赋值给变量。

声明函数

关键字func声明函数,结构是 func + 函数名 + 参数(可无) + 返回值(可无) + 函数体。

func Run(searchTerm string) {}

声明方法

结构是 func + 接收者 + 函数名 + 参数 + 返回值 + 函数体。

实现接口方法时候需要注意case3,如果通过接口类型的值调用方法,使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时
候被调用 。

type defaultMatcher struct{}
// case 1
// 方法接收者为类型值的指针
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 第一种声明类型值,第二种声明类型值的指针。都可以成功调用Search
var dm defaultMatchdm := new(defaultMatch)
dm.Search(feed, "test")

// case 2
// 方法接收者为类型值
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 第一种声明类型值,第二种声明类型值的指针。都可以成功调用Search
var dm defaultMatchdm := new(defaultMatch)
dm.Search(feed, "test")

---------------------------------------------------------------------------------------------

// case 3
// 方法接收者为类型值的指针,通过接口类型调用方法
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
var dm defaultMatch
// 第一种接口类型的值为值,第二种接口类型的值为指针。只有第二种成功调用Search
// 第一种报错 cannot use dm (type defaultMatcher) as type Matcher in assignment
var matcher Matcher = dmvar matcher Matcher = &dm
dm.Search(feed, "test")

// case 4
// 方法接收者为类型值,通过接口类型调用方法
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
var dm defaultMatch
// 第一种接口类型的值为值,第二种接口类型的值为指针。都可以成功调用Search
var matcher Matcher = dmvar matcher Matcher = &dm
dm.Search(feed, "test")

启动并同步操作goroutine

非常推荐使用 sync包中的WaitGroup 来跟踪 goroutine 的工作是否完成。WaitGroup 是一个计数信号量,我们可以利用它来统计所有的goroutine 是不是都完成了工作。

// 创建一个无缓冲的通道,接收匹配后的结果
results := make(chan *Result)
var waitGroup sync.WaitGroup
// len(feeds)的数量等于启动goroutine的数量
waitGroup.Add(len(feeds))

goroutine完成工作后,会通过waitGroup.Done()递减waitGroup的计数值。

// 为每个数据源启动一个 goroutine 来查找结果
for _, feed := range feeds {
...
// 启动一个 goroutine 来执行搜索
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}

启动一个监控所有goroutine都完成工作的goroutine,利用WaitGroup的 Wait 方法。这个方法会导致 goroutine阻塞,直到 WaitGroup 内部的计数到达0。之后,goroutine 调用了内置的 close 函数,关闭了通道,最终导致程序终止。

go func () {
waitGroup.Wait()
close(results)
}()
// 通道会一直被阻塞,直到有结果写入
// 一旦通道被关闭,for 循环就会终止
for result := range results {
fmt.Printf("%s:\n%s\n\n", result.Field, result.Content)
}

使用接口写通用的代码

声明接口类型

type Matcher interface {
Search(feed *Feed, searchTerm string) ([]*Result, error)
}

命名接口的时候,需要遵守 Go 语言的命名惯例。如果接口类型只包含一个方法,类型的名字以 er 结尾。如果接口类型内部声明了多个方法,其名字需要与其行为关联。

实现接口

type defaultMatcher struct{}
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
return nil, nil
}

如下函数,可以接收实现了Macher接口的defaultMatcher类型作为传参。

func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
searchResults, err := matcher.Search(feed, searchTerm)
...
}

处理程序逻辑和错误

document, err := m.retrieve(feed)
if err != nil {
return nil, err
}

小结

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