前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实效go编程--3

实效go编程--3

作者头像
李海彬
发布2018-03-26 12:49:42
7800
发布2018-03-26 12:49:42
举报
文章被收录于专栏:Golang语言社区Golang语言社区

最后,每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。 (其实每个文件都可以拥有多个 init 函数。)而它的结束就意味着初始化结束: 只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用, 而那些 init 只有在所有已导入的包都被初始化后才会被求值。

除了那些不能被表示成声明的初始化外,init 函数还常被用在程序真正开始执行前,检验或校正程序的状态。

func init() { if user == "" { log.Fatal("$USER not set") } if home == "" { home = "/home/" + user } if gopath == "" { gopath = home + "/go" } // gopath 可通过命令行中的 --gopath 标记覆盖掉。 flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH") } 方法

指针 vs. 值

正如 ByteSize 那样,我们可以为任何已命名的类型(除了指针或接口)定义方法; 接收者可不必为结构体。

在之前讨论切片时,我们编写了一个 Append 函数。 我们也可将其定义为切片的方法。为此,我们首先要声明一个已命名的类型来绑定该方法, 然后使该方法的接收者成为该类型的值。

type ByteSlice []byte

func (slice ByteSlice) Append(data []byte) []byte { // 主体和前面相同。 } 我们仍然需要该方法返回更新后的切片。为了消除这种不便,我们可通过重新定义该方法, 将一个指向 ByteSlice 的指针作为该方法的接收者, 这样该方法就能重写调用者提供的切片了。

func (p *ByteSlice) Append(data []byte) { slice := *p // 主体和前面相同,但没有 return。 *p = slice } 其实我们做得更好。若我们将函数修改为与标准 Write 类似的方法,就像这样,

func (p *ByteSlice) Write(data []byte) (n int, err error) { slice := *p // 依旧和前面相同。 *p = slice return len(data), nil } 那么类型 *ByteSlice 就满足了标准的 io.Writer 接口,这将非常实用。 例如,我们可以通过打印将内容写入。

var b ByteSlice fmt.Fprintf(&b, "This hour has %d days\n", 7) 我们将 ByteSlice 的地址传入,因为只有 *ByteSlice 才满足 io.Writer。以指针或值为接收者的区别在于:值方法可通过指针和值调用, 而指针方法只能通过指针来调用。

之所以会有这条规则是因为指针方法可以修改接收者;通过值调用它们会导致方法接收到该值的副本, 因此任何修改都将被丢弃,因此该语言不允许这种错误。不过有个方便的例外:若该值是可寻址的, 那么该语言就会自动插入取址操作符来对付一般的通过值调用的指针方法。在我们的例子中,变量 b 是可寻址的,因此我们只需通过 b.Write 来调用它的 Write 方法,编译器会将它重写为 (&b).Write。

顺便一提,在字节切片上使用 Write 的想法已被 bytes.Buffer 所实现。

接口与其它类型

接口

Go中的接口为指定对象的行为提供了一种方法:如果某样东西可以完成这个, 那么它就可以用在这里。我们已经见过许多简单的示例了;通过实现 String 方法,我们可以自定义打印函数,而通过 Write 方法,Fprintf 则能对任何对象产生输出。在Go代码中, 仅包含一两种方法的接口很常见,且其名称通常来自于实现它的方法, 如 io.Writer 就是实现了 Write 的一类对象。

每种类型都能实现多个接口。例如一个实现了 sort.Interface 接口的集合就可通过 sort 包中的例程进行排序。该接口包括 Len()、Less(i, j int) bool 以及 Swap(i, j int),另外,该集合仍然可以有一个自定义的格式化器。 以下特意构建的例子 Sequence 就同时满足这两种情况。

type Sequence []int

// Methods required by sort.Interface. // sort.Interface 所需的方法。 func (s Sequence) Len() int { return len(s) } func (s Sequence) Less(i, j int) bool { return s[i] < s[j] } func (s Sequence) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// Method for printing - sorts the elements before printing. // 用于打印的方法 - 在打印前对元素进行排序。 func (s Sequence) String() string { sort.Sort(s) str := "[" for i, elem := range s { if i > 0 { str += " " } str += fmt.Sprint(elem) } return str + "]" } 类型转换

Sequence 的 String 方法重新实现了 Sprint 为切片实现的功能。若我们在调用 Sprint 之前将 Sequence 转换为纯粹的 []int,就能共享已实现的功能。

func (s Sequence) String() string { sort.Sort(s) return fmt.Sprint([]int(s)) } 该方法是通过类型转换技术,在 String 方法中安全调用 Sprintf 的另个一例子。若我们忽略类型名的话,这两种类型(Sequence和 []int)其实是相同的,因此在二者之间进行转换是合法的。 转换过程并不会创建新值,它只是值暂让现有的时看起来有个新类型而已。 (还有些合法转换则会创建新值,如从整数转换为浮点数等。)

在Go程序中,为访问不同的方法集而进行类型转换的情况非常常见。 例如,我们可使用现有的 sort.IntSlice 类型来简化整个示例:

type Sequence []int

// // 用于打印的方法 - 在打印前对元素进行排序。 func (s Sequence) String() string { sort.IntSlice(s).Sort() return fmt.Sprint([]int(s)) } 现在,不必让 Sequence 实现多个接口(排序和打印), 我们可通过将数据条目转换为多种类型(Sequence、sort.IntSlice 和 []int)来使用相应的功能,每次转换都完成一部分工作。 这在实践中虽然有些不同寻常,但往往却很有效。

接口转换与类型断言

类型选择是类型转换的一种形式:它接受一个接口,在选择 (switch)中根据其判断选择对应的情况(case), 并在某种意义上将其转换为该种类型。以下代码为 fmt.Printf 通过类型选择将值转换为字符串的简化版。若它已经为字符串,我们需要该接口中实际的字符串值; 若它有 String 方法,我们则需要调用该方法所得的结果。

type Stringer interface { String() string }

var value interface{} // 调用者提供的值。 switch str := value.(type) { case string: return str case Stringer: return str.String() } 第一种情况获取具体的值,第二种将该接口转换为另一个接口。这种方式对于混合类型来说非常完美。

若我们只关心一种类型呢?若我们知道该值拥有一个 string 而想要提取它呢? 只需一种情况的类型选择就行,但它需要类型断言。类型断言接受一个接口值, 并从中提取指定的明确类型的值。其语法借鉴自类型选择开头的子句,但它需要一个明确的类型, 而非 type 关键字:

value.(typeName) 而其结果则是拥有静态类型 typeName 的新值。该类型必须为该接口所拥有的具体类型, 或者该值可转换成的第二种接口类型。要提取我们知道在该值中的字符串,可以这样:

str := value.(string) 但若它所转换的值中不包含字符串,该程序就会以运行时错误崩溃。为避免这种情况, 需使用“逗号, ok”惯用测试它能安全地判断该值是否为字符串:

str, ok := value.(string) if ok { fmt.Printf("字符串值为 %q\n", str) } else { fmt.Printf("该值非字符串\n") } 若类型断言失败,str 将继续存在且为字符串类型,但它将拥有零值,即空字符串。

作为对能量的说明,这里有个 if-else 语句,它等价于本节开头的类型选择。

if str, ok := value.(string); ok { return str } else if str, ok := value.(Stringer); ok { return str.String() } 通用性

若某种现有的类型仅实现了一个接口,且除此之外并无可导出的方法,则该类型本身就无需导出。 仅导出该接口能让我们更专注于其行为而非实现,其它属性不同的实现则能镜像该原始类型的行为。 这也能够避免为每个通用接口的实例重复编写文档。

在这种情况下,构造函数应当返回一个接口值而非实现的类型。例如在 hash 库中,crc32.NewIEEE 和 adler32.New 都返回接口类型 hash.Hash32。要在Go程序中用Adler-32算法替代CRC-32, 只需修改构造函数调用即可,其余代码则不受算法改变的影响。

同样的方式能将 crypto 包中多种联系在一起的流密码算法与块密码算法分开。 crypto/cipher 包中的 Block 接口指定了块密码算法的行为, 它为单独的数据块提供加密。接着,和 bufio 包类似,任何实现了该接口的密码包都能被用于构造以 Stream 为接口表示的流密码,而无需知道块加密的细节。

crypto/cipher 接口看其来就像这样:

type Block interface { BlockSize() int Encrypt(src, dst []byte) Decrypt(src, dst []byte) }

type Stream interface { XORKeyStream(dst, src []byte) } 这是计数器模式CTR流的定义,它将块加密改为流加密,注意块加密的细节已被抽象化了。

// NewCTR 返回一个 Stream,其加密/解密使用计数器模式中给定的 Block 进行。 // iv 的长度必须与 Block 的块大小相同。 func NewCTR(block Block, iv []byte) Stream NewCTR 的应用并不仅限于特定的加密算法和数据源,它适用于任何对 Block 接口和 Stream 的实现。因为它们返回接口值, 所以用其它加密模式来代替CTR只需做局部的更改。构造函数的调用过程必须被修改, 但由于其周围的代码只能将它看做 Stream,因此它们不会注意到其中的区别。

接口和方法

由于几乎任何类型都能添加方法,因此几乎任何类型都能满足一个接口。一个很直观的例子就是 http 包中定义的 Handler 接口。任何实现了 Handler 的对象都能够处理HTTP请求。

type Handler interface { ServeHTTP(ResponseWriter, *Request) } ResponseWriter 接口提供了对方法的访问,这些方法需要响应客户端的请求。 由于这些方法包含了标准的 Write 方法,因此 http.ResponseWriter 可用于任何 io.Writer 适用的场景。Request 结构体包含已解析的客户端请求。

为简单起见,我们假设所有的HTTP请求都是GET方法,而忽略POST方法, 这种简化不会影响处理程序的建立方式。这里有个短小却完整的处理程序实现, 它用于记录某个页面被访问的次数。

// 简单的计数器服务。 type Counter struct { n int }

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctr.n++ fmt.Fprintf(w, "counter = %d\n", ctr.n) } (紧跟我们的主题,注意 Fprintf 如何能输出到 http.ResponseWriter。) 作为参考,这里演示了如何将这样一个服务器添加到URL树的一个节点上。

import "net/http" ... ctr := new(Counter) http.Handle("/counter", ctr) 但为什么 Counter 要是结构体呢?一个整数就够了。 An integer is all that's needed. (接收者必须为指针,增量操作对于调用者才可见。)

// 简单的计数器服务。 type Counter int

func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) { *ctr++ fmt.Fprintf(w, "counter = %d\n", *ctr) } 当页面被访问时,怎样通知你的程序去更新一些内部状态呢?为Web页面绑定个信道吧。

// 每次浏览该信道都会发送一个提醒。 // (可能需要带缓冲的信道。) type Chan chan *http.Request

func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) { ch <- req fmt.Fprint(w, "notification sent") } 最后,假设我们需要输出调用服务器二进制程序时使用的实参 /args。 很简单,写个打印实参的函数就行了。

func ArgServer() { fmt.Println(os.Args) } 我们如何将它转换为HTTP服务器呢?我们可以将 ArgServer 实现为某种可忽略值的方法,不过还有种更简单的方法。 既然我们可以为除指针和接口以外的任何类型定义方法,同样也能为一个函数写一个方法。 http 包包含以下代码:

// HandlerFunc 类型是一个适配器,它允许将普通函数用做HTTP处理程序。 // 若 f 是个具有适当签名的函数,HandlerFunc(f) 就是个调用 f 的处理程序对象。 type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(c, req). func (f HandlerFunc) ServeHTTP(w ResponseWriter, req *Request) { f(w, req) } HandlerFunc 是个具有 ServeHTTP 方法的类型, 因此该类型的值就能处理HTTP请求。我们来看看该方法的实现:接收者是一个函数 f,而该方法调用 f。这看起来很奇怪,但不必大惊小怪, 区别在于接收者变成了一个信道,而方法通过该信道发送消息。

为了将 ArgServer 实现成HTTP服务器,首先我们得让它拥有合适的签名。

// 实参服务器。 func ArgServer(w http.ResponseWriter, req *http.Request) { fmt.Fprintln(w, os.Args) } ArgServer 和 HandlerFunc 现在拥有了相同的签名, 因此我们可将其转换为这种类型以访问它的方法,就像我们将 Sequence 转换为 IntSlice 以访问 IntSlice.Sort 那样。 建立代码非常简单:

http.Handle("/args", http.HandlerFunc(ArgServer)) 当有人访问 /args 页面时,安装到该页面的处理程序就有了值 ArgServer 和类型 HandlerFunc。 HTTP服务器会以 ArgServer 为接收者,调用该类型的 ServeHTTP 方法,它会反过来调用 ArgServer(通过 f(c, req)),接着实参就会被显示出来。

在本节中,我们通过一个结构体,一个整数,一个信道和一个函数,建立了一个HTTP服务器, 这一切都是因为接口只是方法的集和,而几乎任何类型都能定义方法。

空白标识符

我们在 for-range 循环和映射中提过几次空白标识符。 空白标识符可被赋予或声明为任何类型的任何值,而其值会被无害地丢弃。它有点像Unix中的 /dev/null 文件:它表示只写的值,在需要变量但不需要实际值的地方用作占位符。 我们在前面已经见过它的用法了。

多重赋值中的空白标识符

for range 循环中对空表标识符的用法是一种具体情况,更一般的情况即为多重赋值。

若某次赋值需要匹配多个左值,但其中某个变量不会被程序使用, 那么用空白标识符来代替该变量可避免创建无用的变量,并能清楚地表明该值将被丢弃。 例如,当调用某个函数时,它会返回一个值和一个错误,但只有错误很重要, 那么可使用空白标识符来丢弃无关的值。

if _, err := os.Stat(path); os.IsNotExist(err) { fmt.Printf("%s does not exist\n", path) } 你偶尔会看见为忽略错误而丢弃错误值的代码,这是种糟糕的实践。请务必检查错误返回, 它们会提供错误的理由。

// 烂代码!若路径不存在,它就会崩溃。 fi, _ := os.Stat(path) if fi.IsDir() { fmt.Printf("%s is a directory\n", path) } 未使用的导入和变量

若导入某个包或声明某个变量而不使用它就会产生错误。未使用的包会让程序膨胀并拖慢编译速度, 而已初始化但未使用的变量不仅会浪费计算能力,还有可能暗藏着更大的Bug。 然而在程序开发过程中,经常会产生未使用的导入和变量。虽然以后会用到它们, 但为了完成编译又不得不删除它们才行,这很让人烦恼。空白标识符就能提供一个工作空间。

这个写了一半的程序有两个未使用的导入(fmt 和 io)以及一个未使用的变量(fd),因此它不能编译, 但若到目前为止代码还是正确的,我们还是很乐意看到它们的。

package main

import ( "fmt" "io" "log" "os" )

func main() { fd, err := os.Open("test.go") if err != nil { log.Fatal(err) } // TODO: use fd. } 要让编译器停止关于未使用导入的抱怨,需要空白标识符来引用已导入包中的符号。 同样,将未使用的变量 fd 赋予空白标识符也能关闭未使用变量错误。 该程序的以下版本可以编译。

package main

import ( "fmt" "io" "log" "os" )

var _ = fmt.Printf // For debugging; delete when done. // 用于调试,结束时删除。 var _ io.Reader // For debugging; delete when done. // 用于调试,结束时删除。

func main() { fd, err := os.Open("test.go") if err != nil { log.Fatal(err) } // TODO: use fd. _ = fd } 按照惯例,我们应在导入并加以注释后,再使全局声明导入错误静默,这样可以让它们更易找到, 并作为以后清理它的提醒。

为副作用而导入

像前例中 fmt 或 io 这种未使用的导入总应在最后被使用或移除: 空白赋值会将代码标识为工作正在进行中。但有时导入某个包只是为了其副作用, 而没有任何明确的使用。例如,在 net/http/pprof 包的 init 函数中记录了HTTP处理程序的调试信息。它有个可导出的API, 但大部分客户端只需要该处理程序的记录和通过Web叶访问数据。只为了其副作用来哦导入该包, 只需将包重命名为空白标识符:

import _ "net/http/pprof" 这种导入格式能明确表示该包是为其副作用而导入的,因为没有其它使用该包的可能: 在此文件中,它没有名字。(若它有名字而我们没有使用,编译器就会拒绝该程序。)

接口检查

就像我们在前面接口中讨论的那样, 一个类型无需显式地声明它实现了某个接口。取而代之,该类型只要实现了某个接口的方法, 其实就实现了该接口。在实践中,大部分接口转换都是静态的,因此会在编译时检测。 例如,将一个 *os.File 传入一个预期的 io.Reader 函数将不会被编译, 除非 *os.File 实现了 io.Reader 接口。

尽管有些接口检查会在运行时进行。encoding/json 包中就有个实例它定义了一个 Marshaler 接口。当JSON编码器接收到一个实现了该接口的值,那么该编码器就会调用该值的编组方法, 将其转换为JSON,而非进行标准的类型转换。 编码器在运行时通过类型断言检查其属性,就像这样:

m, ok := val.(json.Marshaler) 若只需要判断某个类型是否是实现了某个接口,而不需要实际使用接口本身 (可能是错误检查部分),就使用空白标识符来忽略类型断言的值:

if _, ok := val.(json.Marshaler); ok { fmt.Printf("value %v of type %T implements json.Marshaler\n", val, val) } 当需要确保某个包中实现的类型一定满足该接口时,就会遇到这种情况。 若某个类型(例如 json.RawMessage) 需要一种定制的JSON表现时,它应当实现 json.Marshaler, 不过现在没有静态转换可以让编译器去自动验证它。若该类型通过忽略转换失败来满足该接口, 那么JSON编码器仍可工作,但它却不会使用定制的实现。为确保其实现正确, 可在该包中用空白标识符声明一个全局变量:

var _ json.Marshaler = (*RawMessage)(nil) 在此声明中,我们调用了一个 *RawMessage 转换并将其赋予了 Marshaler,以此来要求 *RawMessage 实现 Marshaler,这时其属性就会在编译时被检测。 若 json.Marshaler 接口被更改,此包将无法通过编译, 而我们则会注意到它需要更新。

在这种结构中出现空白标识符,即表示该声明的存在只是为了类型检查。 不过请不要为满足接口就将它用于任何类型。作为约定, 仅当代码中不存在静态类型转换时才能这种声明,毕竟这是种罕见的情况。

内嵌

Go并不提供典型的,类型驱动的子类化概念,但通过将类型<内嵌到结构体或接口中, 它就能“借鉴”部分实现。

接口内嵌非常简单。我们之前提到过 io.Reader 和 io.Writer 接口,这里是它们的定义。

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

type Writer interface { Write(p []byte) (n int, err error) } io 包也导出了一些其它接口,以此来阐明对象所需实现的方法。 例如 io.ReadWriter 就是个包含 Read 和 Write 的接口。我们可以通过显示地列出这两个方法来指明 io.ReadWriter, 但通过将这两个接口内嵌到新的接口中显然更容易且更具启发性,就像这样:

// ReadWriter 接口结合了 Reader 和 Writer 接口。 type ReadWriter interface { Reader Writer } 正如它看起来那样:ReadWriter 能够做任何 Reader 和 Writer 可以做到的事情,它是内嵌接口的联合体 (它们必须是不相交的方法集)。只有接口能被嵌入到接口中。

同样的基本想法可以应用在结构体中,但其意义更加深远。bufio 包中有 bufio.Reader 和 bufio.Writer 这两个结构体类型, 它们每一个都实现了与 io 包中相同意义的接口。此外,bufio 还通过结合 reader/writer 并将其内嵌到结构体中,实现了带缓冲的 reader/writer:它列出了结构体中的类型,但并未给予它们字段名。

// ReadWriter 存储了指向 Reader 和 Writer 的指针。 // 它实现了 io.ReadWriter。 type ReadWriter struct { *Reader // *bufio.Reader *Writer // *bufio.Writer } 内嵌的元素为指向结构体的指针,当然它们在使用前必须被初始化为指向有效结构体的指针。 ReadWriter 结构体和通过如下方式定义:

type ReadWriter struct { reader *Reader writer *Writer } 但为了提升该字段的方法并满足 io 接口,我们同样需要提供转发的方法, 就像这样:

func (rw *ReadWriter) Read(p []byte) (n int, err error) { return rw.reader.Read(p) } 而通过直接内嵌结构体,我们就能避免如此繁琐。 内嵌类型的方法可以直接引用,这意味着 bufio.ReadWriter 不仅包括 bufio.Reader 和 bufio.Writer 的方法,它还同时满足下列三个接口: io.Reader、io.Writer 以及 io.ReadWriter。

还有种区分内嵌与子类的重要手段。当内嵌一个类型时,该类型的方法会成为外部类型的方法, 但当它们被调用时,该方法的接收者是内部类型,而非外部的。在我们的例子中,当 bufio.ReadWriter 的 Read 方法被调用时, 它与之前写的转发方法具有同样的效果;接收者是 ReadWriter 的 reader 字段,而非 ReadWriter 本身。

内嵌同样可以提供便利。这个例子展示了一个内嵌字段和一个常规的命名字段。

type Job struct { Command string *log.Logger } Job 类型现在有了 Log、Logf 和 *log.Logger 的其它方法。我们当然可以为 Logger 提供一个字段名,但完全不必这么做。现在,一旦初始化后,我们就能记录 Job 了:

job.Log("starting now...") Logger 是 Job 结构体的常规字段, 因此我们可在 Job 的构造函数中,通过一般的方式来初始化它,就像这样:

func NewJob(command string, logger *log.Logger) *Job { return &Job{command, logger} } 或通过复合字面:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)} 若我们需要直接引用内嵌字段,可以忽略包限定名,直接将该字段的类型名作为字段名, 就像我们在 ReaderWriter 结构体的 Read 方法中做的那样。 若我们需要访问 Job 类型的变量 job 的 *log.Logger, 可以直接写作 job.Logger。若我们想精炼 Logger 的方法时, 这会非常有用。

func (job *Job) Logf(format string, args ...interface{}) { job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...)) } 内嵌类型会引入命名冲突的问题,但解决规则却很简单。首先,字段或方法 X 会隐藏该类型中更深层嵌套的其它项 X。若 log.Logger 包含一个名为 Command 的字段或方法,Job 的 Command 字段会覆盖它。

其次,若相同的嵌套层级上出现同名冲突,通常会产生一个错误。若 Job 结构体中包含名为 Logger 的字段或方法,再将 log.Logger 内嵌到其中的话就会产生错误。然而,若重名永远不会在该类型定义之外的程序中使用,那就不会出错。 这种限定能够在外部嵌套类型发生修改时提供某种保护。 因此,就算添加的字段与另一个子类型中的字段相冲突,只要这两个相同的字段永远不会被使用就没问题。

并发

通过通信共享内存

并发编程是个很大的论题。但限于篇幅,这里仅讨论一些Go特有的东西。

在并发编程中,为实现对共享变量的正确访问需要精确的控制,这在多数环境下都很困难。 Go语言另辟蹊径,它将共享的值通过信道传递,实际上,多个独立执行的线程从不会主动共享。 在任意给定的时间点,只有一个Go程能够访问该值。数据竞争从设计上就被杜绝了。 为了提倡这种思考方式,我们将它简化为一句口号:

不要通过共享内存来通信,而应通过通信来共享内存。 这种方法意义深远。例如,引用计数通过为整数变量添加互斥锁来很好地实现。 但作为一种高级方法,通过信道来控制访问能够让你写出更简洁,正确的程序。

我们可以从典型的单线程运行在单CPU之上的情形来审视这种模型。它无需提供同步原语。 现在考虑另一种情况,它也无需同步。现在让它们俩进行通信。若将通信过程看做同步着, 那就完全不需要其它同步了。例如,Unix管道就与这种模型完美契合。 尽管Go的并发处理方式来源于Hoare的通信顺序处理(CSP), 它依然可以看做是类型安全的Unix管道的实现。

Go程

我们称之为Go程是因为现有的术语—线程、协程、进程等等—无法准确传达它的含义。 Go程具有简单的模型:它是与其它Go程并发运行在同一地址空间的函数。它是轻量级的, 所有小号几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。

Go程在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O, 那么其它的线程就会运行。Go程的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的Go程中调用它。当调用完成后, 该Go程也会安静地退出。(效果有点像Unix Shell中的 & 符号,它能让命令在后台运行。)

go list.Sort() // 并发运行 list.Sort,无需等它结束。 函数字面在Go程调用中非常有用。

func Announce(message string, delay time.Duration) { go func() { time.Sleep(delay) fmt.Println(message) }() // 注意括号 - 必须调用该函数。 } 在Go中,函数字面都是闭包:其实现在保证了函数内引用变量的生命周期与函数的活动时间相同。

这些函数没什么实用性,因为它们没有实现完成时的信号处理。因此,我们需要信道。

信道

信道与映射一样,也需要通过 make 来分配内存。其结果值充当了对底层数据结构的引用。 若提供了一个可选的整数形参,它就会为该信道设置缓冲区大小。默认值是零,表示不带缓冲的或同步的信道。

ci := make(chan int) // 整数类型的无缓冲信道 cj := make(chan int, 0) // 整数类型的无缓冲信道 cs := make(chan *os.File, 100) // 指向文件指针的带缓冲信道 无缓冲信道在通信时会同步交换数据,它能确保(两个Go程的)计算处于确定状态。

信道有很多惯用法,我们从这里开始了解。在上一节中,我们在后台启动了排序操作。 信道使得启动的Go程等待排序完成。

c := make(chan int) // 分配一个信道 // 在Go程中启动排序。当它完成后,在信道上发送信号。 go func() { list.Sort() c <- 1 // 发送信号,什么值无所谓。 }() doSomethingForAWhile() <-c // 等待排序结束,丢弃发来的值。 接收者在收到数据前会一直阻塞。若信道是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞;若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞; 若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。

带缓冲的信道可被用作信号量,例如限制吞吐量。在此例中,进入的请求会被传递给 handle,它从信道中接收值,处理请求后将值发回该信道中,以便让该 “信号量”准备迎接下一次请求。信道缓冲区的容量决定了同时调用 process 的数量上限,因此我们在初始化时首先要填充至它的容量上限。

var sem = make(chan int, MaxOutstanding)

func handle(r *Request) { sem <- 1 // 等待活动队列清空。 process(r) // 可能需要很长时间。 <-sem // 完成;使下一个请求可以运行。 }

func Serve(queue chan *Request) { for { req := <-queue go handle(req) // 无需等待 handle 结束。 } } 由于数据同步发生在信道的接收端(也就是说发送发生在>接受之前,参见 Go内存模型),因此信号必须在信道的接收端获取,而非发送端。

然而,它却有个设计问题:尽管只有 MaxOutstanding 个Go程能同时运行,但 Serve 还是为每个进入的请求都创建了新的Go程。其结果就是,若请求来得很快, 该程序就会无限地消耗资源。为了弥补这种不足,我们可以通过修改 Serve 来限制创建Go程,这是个明显的解决方案,但要当心我们修复后出现的Bug。

func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func() { process(req) // 这儿有Bug,解释见下。 <-sem }() } } Bug出现在Go的 for 循环中,该循环变量在每次迭代时会被重用,因此 req 变量会在所有的Go程间共享,这不是我们想要的。我们需要确保 req 对于每个Go程来说都是唯一的。有一种方法能够做到,就是将 req 的值作为实参传入到该Go程的闭包中:

func Serve(queue chan *Request) { for req := range queue { sem <- 1 go func(req *Request) { process(req) <-sem }(req) } } 比较前后两个版本,观察该闭包声明和运行中的差别。 另一种解决方案就是以相同的名字创建新的变量,如例中所示:

func Serve(queue chan *Request) { for req := range queue { req := req // 为该Go程创建 req 的新实例。 sem <- 1 go func() { process(req) <-sem }() } } 它的写法看起来有点奇怪

req := req 但在Go中这样做是合法且惯用的。你用相同的名字获得了该变量的一个新的版本, 以此来局部地刻意屏蔽循环变量,使它对每个Go程保持唯一。

回到编写服务器的一般问题上来。另一种管理资源的好方法就是启动固定数量的 handle Go程,一起从请求信道中读取数据。Go程的数量限制了同时调用 process 的数量。Serve 同样会接收一个通知退出的信道, 在启动所有Go程后,它将阻塞并暂停从信道中接收消息。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-01-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档