Skip to content

Go 语言实战 第 2 章 快速开始一个 Go 程序

🏷️ Go 《Go 语言实战》

示例代码:

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

main 包

go
package main

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

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

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

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

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

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

search 包

go
package search

import (
    "log"
    "sync"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

JiaJia:

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

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

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

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

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

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

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

JiaJia:

类似于 Java 中的 CountDownLatch

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

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

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

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

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

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

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

feed.go

go
package search

import (
    "encoding/json"
    "os"
)

const dataFile = "data/data.json"

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

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

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

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

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

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

结构类型

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

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

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

JiaJia:

类似于 Java 中的 finally

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

match.go

go
package search

import (
    "log"
)

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

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

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

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

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

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

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

default.go

go
package search

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

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

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

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

go
func (m defaultMatcher) Search

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

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

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

go
func (m *defaultMatcher) Search

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

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

JiaJia:

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

rss.go

go
package matchers

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

    "../search"
)

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

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

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

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

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

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

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

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

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

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

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

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

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

    return results, nil
}

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

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

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

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

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

小结

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