学习golang的迷茫

有问必答leoliu 回复了问题 • 6 人关注 • 4 个回复 • 419 次浏览 • 2016-10-12 13:44 • 来自相关话题

Go语言并发模型:使用 select

文章分享ming 发表了文章 • 0 个评论 • 199 次浏览 • 2016-10-12 12:30 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

... 查看全部


此文章已得到翻译者授权转载,点击查看原文



简介


作为一种现代语言,go语言实现了对并发的原生支持。上几期文章中,我们对goroutine 和 channel进行了详细的讲解。
但是要实现对 channel 的控制,从语言层面上来说,select 语句是必不可少的部分。本文中,我们就 select 语句的
行为和使用方法进行深入讨论。


阅读建议


本文中的内容是 Go语言并发模型的一篇,但是与上几期关系不是特别密切,可以独立阅读。本文的内容源自于
go language specifications
Rob Pike 在2012年进行的一场名为"concurrency"
的演讲。如果有时间的话,建议在 YouTube 上看一下他本人的演讲。


select 语句的行为


为了便于理解,我们首先给出一个代码片段:


// https://talks.golang.org/2012/concurrency.slide#32
select {
case v1 := <-c1:
fmt.Printf("received %v from c1\n", v1)
case v2 := <-c2:
fmt.Printf("received %v from c2\n", v1)
case c3 <- 23:
fmt.Printf("sent %v to c3\n", 23)
default:
fmt.Printf("no one was ready to communicate\n")
}

上面这段代码中,select 语句有四个 case 子语句,前两个是 receive 操作,第三个是 send 操作,最后一个是默认操作。
代码执行到 select 时,case 语句会按照源代码的顺序被评估,且只评估一次,评估的结果会出现下面这几种情况:



  1. 除 default 外,如果只有一个 case 语句评估通过,那么就执行这个case里的语句;

  2. 除 default 外,如果有多个 case 语句评估通过,那么通过伪随机的方式随机选一个;

  3. 如果 default 外的 case 语句都没有通过评估,那么执行 default 里的语句;

  4. 如果没有 default,那么 代码块会被阻塞,指导有一个 case 通过评估;否则一直阻塞


如果 case 语句中 的 receive 操作的对象是 nil channel,那么也会阻塞,下面我们看一个更全面、用法也更高级的例子:


// https://golang.org/ref/spec#Select_statements
var a []int
var c, c1, c2, c3, c4 chan int
var i1, i2 int
select {
case i1 = <-c1:
print("received ", i1, " from c1\n")
case c2 <- i2:
print("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
print("received ", i3, " from c3\n")
} else {
print("c3 is closed\n")
}
case a[f()] = <-c4:
// same as:
// case t := <-c4
// a[f()] = t
default:
print("no communication\n")
}

for { // 向 channel c 发送随机 bit 串
select {
case c <- 0: // note: no statement, no fallthrough, no folding of cases
case c <- 1:
}
}

select {} // 永久阻塞

注意:与 C/C++ 等传统编程语言不同,go语言的 case 语句不需要 break 关键字去跳出 select。


select 的使用


为请求设置超时时间


在 golang 1.7 之前, http 包并没有引入 context 支持,通过 http.Client 向一个坏掉的服务发送请求会导致响应缓慢。
类似的场景下,我们可以使用 select 控制服务响应时间,下面是一个简单的demo:


func main() {
c := boring("Joe")
timeout := time.After(5 * time.Second)
for {
select {
case s := <-c:
fmt.Println(s)
case <-timeout:
fmt.Println("You talk too much.")
return
}
}
}

done channel


上几期的文章中,我们均讨论过 done channel,它可以用于保证流水线上每个阶段goroutine 的退出。在 golang.org/x/net 包中,
done channel 被广泛应用。这里我们看一眼 golang.org/x/net/context/ctxhttp 中 Do 方法的实现:


// https://github.com/golang/net/ ... tp.go

// Do sends an HTTP request with the provided http.Client and returns
// an HTTP response.
//
// If the client is nil, http.DefaultClient is used.
//
// The provided ctx must be non-nil. If it is canceled or times out,
// ctx.Err() will be returned.
func Do(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req.WithContext(ctx))
// If we got an error, and the context has been canceled,
// the context's error is probably more useful.
if err != nil {
select {
case <-ctx.Done():
err = ctx.Err()
default:
}
}
return resp, err
}

quit channel


在很多场景下,quit channel 和 done channel 是一个概念。在并发程序中,通常 main routine 将
任务分给其它 go routine 去完成,而自身只是起到调度作用。这种情况下,main 函数无法知道 其它goroutine
任务是否完成,此时我们需要 quit channel;对于更细粒度的控制,比如完成多少,还是需要 done channel (参考WaitGroup)。
下面是 quit channel 的一个例子,首先是 main routine:


// 创建 quit channel
quit := make(chan string)
// 启动生产者 goroutine
c := boring("Joe", quit)
// 从生产者 channel 读取结果
for i := rand.Intn(10); i >= 0; i-- { fmt.Println(<-c) }
// 通过 quit channel 通知生产者停止生产
quit <- "Bye!"
fmt.Printf("Joe says: %q\n", <-quit)

我们再看 生产者 go routine 中与 quit channel 相关的部分:


select {
case c <- fmt.Sprintf("%s: %d", msg, i):
// do nothing
case <-quit:
cleanup()
quit <- "See you!"
return
}

Google Search (延伸阅读)


Google Search 是一个很经典的例子,由于代码较多,有兴趣的童鞋查看 Rob Pike 的 ppt
更高阶的并发方式可以阅读 Sameer Ajmani 的 ppt Advanced Go Concurrency Patterns


并发相关的主题就先到这里,下一期文章中,我们会讨论go语言测试工具链中的单元测试。


相关链接:



  1. Rob Pike演讲:concurrency

  2. [language specification: select statement](https://golang.org/ref/spec#Select_statements “select")


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:使用 context

文章分享ming 发表了文章 • 0 个评论 • 202 次浏览 • 2016-10-12 12:28 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。
请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。
用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、
验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的
goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。


在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与
请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。
你可以通过 go get golang.org/x/net/context 命令获取这个包。本文要讲的就是如果使用
这个包,同时也会提供一个完整的例子。


阅读建议


本文内容涉及到了 done channel,如果你不了解这个概念,那么请先阅读 "Go语言并发模型:像Unix Pipe那样使用channel"


由于访问 golang.org/x/net/context 需要梯子,你可以访问它在 github 上的 mirror
如果要下载本文中的代码,可以查看文章末尾的“相关链接”环节。


package context


context 包的核心是 struct Context,声明如下:


// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this `Context` is canceled
// or times out.
Done() <-chan struct{}

// Err indicates why this Context was canceled, after the Done channel
// is closed.
Err() error

// Deadline returns the time when this Context will be canceled, if any.
Deadline() (deadline time.Time, ok bool)

// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

注意: 这里我们对描述进行了简化,更详细的描述查看 godoc:context


Done 方法返回一个 channel,这个 channel 对于以 Context 方式运行的函数而言,是一个取消信号。
当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。 之后,Err 方法会返回一个错误,
告知为什么 Context 被取消。关于 Done channel 的更多细节查看上一篇文章 "Go语言并发模型:像Unix Pipe那样使用channel"


一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。背后的原因是一致的:接收取消信号的函数
和发送信号的函数通常不是一个。 一个典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
作为一个折中,WithCancel 函数 (后面会细说) 提供了一种取消新的 Context 的方法。


Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,
对它执行 取消 操作时,所有 goroutine 都会接收到取消信号。


Deadline 方法允许函数确定它们是否应该开始工作。如果剩下的时间太少,也许这些函数就不值得启动。
代码中,我们也可以使用 Deadline 对象为 I/O 操作设置截止时间。


Value 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。


继承 context


context 包提供了一些函数,协助用户从现有的 Context 对象创建新的 Context 对象。
这些 Context 对象形成一棵树:当一个 Context 对象被取消时,继承自它的所有 Context 都会被取消。


Background 是所有 Context 对象树的根,它不能被取消。它的声明如下:


// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level `Context` for incoming requests.
func Background() Context

WithCancelWithTimeout 函数 会返回继承的 Context 对象, 这些对象可以比它们的父 Context 更早地取消。


当请求处理函数返回时,与该请求关联的 Context 会被取消。 当使用多个副本发送请求时,可以使用 WithCancel
取消多余的请求。 WithTimeout 在设置对后端服务器请求截止时间时非常有用。 下面是这三个函数的声明:


// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed, cancel is called, or timeout elapses. The new
// Context's Deadline is the sooner of now+timeout and the parent's deadline, if
// any. If the timer is still running, the cancel function releases its
// resources.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下:


// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context

当然,想要知道 Context 包是如何工作的,最好的方法是看一个栗子。


一个栗子:Google Web Search


我们的例子是一个 HTTP 服务,它能够将类似于 /search?q=golang&timeout=1s 的请求 转发给
Google Web Search API,然后渲染返回的结果。
timeout 参数用来告诉 server 时间到时取消请求。


这个例子的代码存放在三个包里:



  1. server:它提供 main 函数和 处理 /search 的 http handler

  2. userip:它能够从 请求解析用户的IP,并将请求绑定到一个 Context 对象。

  3. google:它包含了 Search 函数,用来向 Google 发送请求。


深入 server 程序


server 程序处理类似于 /search?q=golang 的请求,
返回 Google API 的搜索结果。它将 handleSearch 函数注册到 /search 路由。处理函数创建一个 Context ctx,并对其进行初始化,
以保证 Context 取消时,处理函数返回。如果请求的 URL 参数中包含 timeout,那么当 timeout 到期时, Context 会被自动取消。
handleSearch 的代码如下:


func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx is the `Context` for this handler. Calling cancel closes the
// ctx.Done channel, which is the cancellation signal for requests
// started by this handler.
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a `Context` that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // Cancel ctx as soon as handleSearch returns.

处理函数 (handleSearch) 将query 参数从请求中解析出来,然后通过 userip 包将client IP解析出来。这里 Client IP 在
后端发送请求时要用到,所以 handleSearch 函数将它 attach 到 Context 对象 ctx 上。代码如下:


// Check the search query.
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}

// Store the user IP in ctx for use by code in other packages.
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)

处理函数带着 Context 对象 ctxquery 调用 google.Search,代码如下:


// Run the Google search and print the results.
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)

如果搜索成功,处理函数会渲染搜索结果,代码如下:


if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}

深入 userip 包


userip 包提供了两个功能:1. 从请求解析出
Client IP;2. 将 Client IP 关联到一个 Context 对象。一个 Context 对象提供一个 key-value 映射,key 和 value
的类型都是 interface{},但是 key 必须满足等价性(可以比较),value 必须是线程安全的。类似于 userip 的包
隐藏了映射的细节,提供的是对特定 Context 类型值得强类型访问。


为了避免 key 冲突,userip 定义了一个非输出类型 key,并使用该类型的值作为 Context 的key。代码如下:


// 为了避免与其他包中的 `Context` key 冲突
// 这里不输出 key 类型 (首字母小写)
type key int

// userIPKey 是 user IP 的 `Context` key
// 它的值是随意写的。如果这个包中定义了其他
// `Context` key,这些 key 必须不同
const userIPKey key = 0

函数 FromRequest 用来从一个 http.Request 对象中解析出 userIP:


func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}

函数 NewContext 返回一个新的 Context 对象,它携带者 userIP:


func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}

函数 FromContext 从一个 Context 对象中解析 userIP:


func FromContext(ctx context.Context) (net.IP, bool) {
// ctx.Value returns nil if ctx has no value for the key;
// the net.IP type assertion returns ok=false for nil.
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}

深入 google 包


函数 google.Search 想 Google Web Search API 发送一个 HTTP 请求,并解析返回的 JSON 数据。
该函数接收一个 Context 对象 ctx 作为第一参数,在请求还没有返回时,一旦 ctx.Done 关闭,该函数也会立即返回。


Google Web Search API 请求包含 query 关键字和 user IP 两个参数。具体实现如下:


func Search(ctx context.Context, query string) (Results, error) {
// Prepare the Google Search API request.
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)

// If ctx is carrying the user IP address, forward it to the server.
// Google APIs use the user IP to distinguish server-initiated requests
// from end-user requests.
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()

函数 Search 使用一个辅助函数 httpDo 发送 HTTP 请求,并在 ctx.Done 关闭时取消请求 (如果还在处理请求或返回)。
函数 Search 传递给 httpDo 一个闭包处理 HTTP 结果。下面是具体实现:


var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()

// Parse the JSON search result.
// https://developers.google.com/ ... fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo waits for the closure we provided to return, so it's safe to
// read results here.
return results, err

函数 httpDo 在一个新的 goroutine 中发送 HTTP 请求和处理结果。如果 ctx.Done 已经关闭,
而处理请求的 goroutine 还存在,那么取消请求。下面是具体实现:


func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}

在自己的代码中使用 Context


许多服务器框架都提供了管理请求作用域数据的包和类型。我们可以定义一个 Context 接口的实现,
将已有代码和期望 Context 参数的代码粘合起来。


举个栗子,Gorilla 框架的 github.com/gorilla/context
允许处理函数 (handlers) 将数据和请求结合起来,他通过 HTTP 请求 到 key-value对 的映射来实现。
gorilla.go 中,我们提供了一个 Context 的具体实现,
这个实现的 Value 方法返回的值已经与 gorilla 包中特定的 HTTP 请求关联起来。


还有一些包实现了类似于 Context 的取消机制。比如 Tomb 中有一个 Kill 方法,
该方法通过关闭 名为Dying 的 channel 发送取消信号。Tomb 也提供了等待 goroutine 退出的方法,类似于 sync.WaitGroup
tomb.go 中,我们提供了一个 Context 的实现,当它的父 Context 被取消
或 一个 Tomb 对象被 kill 时,该 Context 对象也会被取消。


结论


在 Google, 我们要求 Go 程序员把 Context 作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。
这样一种机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制,
保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。


如果你要在 Context 之上构建服务器框架,需要一个自己的 Context 实现,在框架与期望 Context 参数的代码之间建立一座桥梁。
当然,Client 库也需要接收一个 Context 对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context
分享代码、创建可扩展的服务都会非常方便。


原作者:Sameer Ajmani 翻译:Oscar


下期预告:Go语言并发模型:使用 select (原文链接)。


相关链接:



  1. 原文链接

  2. [代码位置](https://blog.golang.org/context/ “context")

  3. 代码位置(mirror)

  4. mirror of package net

  5. Google Web Search API


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:以并行处理MD5为例

文章分享ming 发表了文章 • 1 个评论 • 141 次浏览 • 2016-10-12 12:27 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

<... 查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Go语言的并发原语允许开发者以类似于 Unix Pipe 的方式构建数据流水线 (data pipelines),数据流水线能够高效地利用 I/O和多核 CPU 的优势。


本文要讲的就是一些使用流水线的一些例子,流水线的错误处理也是本文的重点。


阅读建议


本文是"Go语言并发模型:像Unix Pipe那样使用channel"
一文的下半部分,但重点在于实践。如果你对 channel 已经比较熟悉,则可以独立阅读。
如果你对 channel 和 go 两个关键字不太熟悉,建议先阅读上半部分。


本文所使用的例子是批量计算文件的MD5值,实现了 linux 下的 md5sum 命令。
我们首先会讲到 md5sum 的单线程版本,逐步深入到并发的初级和高级版本。


本文中绝大多数讲解都是基于代码进行的。在文章末尾"相关链接"中可以下载三个版本的 md5sum 的实现。


单线程版的 md5sum


MD5 是一种广泛用于文件校验的 hash 算法。Linux 下的 md5sum 命令会打印一组文件的 md5值。它的使用方式如下:


% md5sum *.go
c33237079343a4d567a2a29df0b8e46e bounded.go
a7e3771f2ed58d4b34a73566d93ce63a parallel.go
1dc687202696d650594aaac56d579179 serial.go

我们的示例程序类似于 md5sum,但是它接收文件夹作为参数,并打印出每个文件的 md5值,打印结果按照路径排序。
下面这个例子是 打印当前目录下所有文件的 md5 值:


% go run serial.go .
c33237079343a4d567a2a29df0b8e46e bounded.go
a7e3771f2ed58d4b34a73566d93ce63a parallel.go
1dc687202696d650594aaac56d579179 serial.go

程序的 main 函数调用辅助函数 MD5All,它会返回路径名称到md5值的一个映射。main 函数中对结果进行排序以后,打印出来:


func main() {
// 计算特定目录下所有文件的 md5值,
// 然后按照路径名顺序打印结果
m, err := MD5All(os.Args[1])
if err != nil {
fmt.Println(err)
return
}
var paths []string
for path := range m {
paths = append(paths, path)
}
sort.Strings(paths)
for _, path := range paths {
fmt.Printf("%x %s\n", m[path], path)
}
}

本文中,函数 MD5All 是讨论的焦点。在 serial.go的实现中,我们没有使用并发,而是逐个读取和计算 filepath.Walk 生成的目录和文件。代码如下:


// MD5All 读取 root 目录下的所有文件,返回一个map
// 该 map 存储了 文件路径到文件内容 md5值的映射
// 如果 Walk 执行失败,或者 ioutil.ReadFile 读取失败,
// MD5All 都会返回错误
func MD5All(root string) (map[string][md5.Size]byte, error) {
m := make(map[string][md5.Size]byte)
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
m[path] = md5.Sum(data)
return nil
})
if err != nil {
return nil, err
}
return m, nil
}

上面的代码中,filepath.Walk 接收两个参数,文件路径和函数指针。
只要是函数签名和返回值 满足 func(string, os.FileInfo, error) error,均可以作为第二参数传递给 filepath.Walk。


点击 serial.go 下载单线程版本的 md5sum。


并发版的 md5sum


点击 parallel.go 下载并发版 md5sum 的代码。


在这个版本的实现中,我们把 MD5All 切割成两个阶段的流水线。
第一阶段是 sumFiles,它遍历文件树,每个文件都在一个新的 goroutine 里计算md5值,然后将结果发送到一个result 类型的channel里。
result 类型的定义如下:


type result struct {
path string
sum [md5.Size]byte
err error
}

sumFiles 返回两个 channel,一个用于接收 md5计算的结果,一个用于接收 filepath.Walk 产生的错误。
Walk 函数为每一个文件创建一个 goroutine,然后检查 done channel。如果 done channel 被关闭,walk 函数立即停止执行。代码示例如下:


func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) {
// 对于每一个普通文件,启动一个 gorotuine 计算文件 md5 值,
// 然后 将结果发送到 c。
// walk 的错误结果发送到 errc。
c := make(chan result)
errc := make(chan error, 1)
go func() {
var wg sync.WaitGroup
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
wg.Add(1)
go func() {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
}
wg.Done()
}()
// done channel 关闭时,终止 walk 函数
select {
case <-done:
return errors.New("walk canceled")
default:
return nil
}
})
// Walk 函数已经返回,所以 所有对 wg.Add 的调用都会结束
// 启动一个 goroutine, 它会在所有发送都结束时,关闭 c。
go func() {
wg.Wait()
close(c)
}()
// 这里不需要 select 语句,应为 errc 是缓冲管道
errc <- err
}()
return c, errc
}

MD5All 从 c 接收 md5值。 MD5All 遇到错误时会提前返回,通过 defer 语句关闭 done channel:


func MD5All(root string) (map[string][md5.Size]byte, error) {
// MD5All 在函数返回时关闭 done channel
// 在从 c 和 errc 接收数据前,也可能关闭
done := make(chan struct{})
defer close(done)

c, errc := sumFiles(done, root)

m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
if err := <-errc; err != nil {
return nil, err
}
return m, nil
}

限制并发量


并发版 MD5All (parallel.go) 的实现中,
我们为每个文件创建了一个 goroutine。如果一个目录中包含很多大文件,可能出现OOM。


我们对并发读取的文件数目稍作限制,进而限制内存的分配。点击 bounded.go
查看限制并发版本的 md5sum。 为了实现限制的目的,我们创建固定数量的 goroutine 用于读取文件。
这里的流水线包含三个阶段:遍历文件和目录、读取并计算md5值、搜集和整合计算结果。


第一阶段时 walkFiles,它生成一个目录下每个普通文件的路径。代码如下:


func walkFiles(done <-chan struct{}, root string) (<-chan string, <-chan error) {
paths := make(chan string)
errc := make(chan error, 1)
go func() {
// Walk 函数返回时,关闭 channel paths
defer close(paths)
// 这里不需要select,因为 errc 是缓冲 channel
errc <- filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return nil
}
select {
case paths <- path:
case <-done:
return errors.New("walk canceled")
}
return nil
})
}()
return paths, errc
}

第二阶段创建固定个数的goroutine digester,每个 digester 从 paths channel 读取文件名,并将结果发送给 c。代码如下:


func digester(done <-chan struct{}, paths <-chan string, c chan<- result) {
for path := range paths {
data, err := ioutil.ReadFile(path)
select {
case c <- result{path, md5.Sum(data), err}:
case <-done:
return
}
}
}

不像前面的例子,这里 digester 没有关闭输出 channel c,因为 多个 digester 在共享这个channel。
关闭操作放到 MD5All 中实现,当所有 digester 运行结束时,MD5All 关闭这个channel。代码如下:


    // 启动固定数量的 goroutine 处理文件
c := make(chan result)
var wg sync.WaitGroup
const numDigesters = 20
wg.Add(numDigesters)
for i := 0; i < numDigesters; i++ {
go func() {
digester(done, paths, c)
wg.Done()
}()
}
go func() {
wg.Wait()
close(c)
}()

我们可以让每个 digester 创建和返回自己的输出 channel。如果这样做,我们还需要额外的 goroutine 去合并结果。


第三阶段从 channel c 接收结果,并从 channel errc 读取错误信息并执行检查。
检查操作不能在 c 读取结束之前完成,因为 walkFiles 函数可能会被阻塞而无法向下游阶段发送数据。 代码如下:


// ... 省略部分代码 ...
m := make(map[string][md5.Size]byte)
for r := range c {
if r.err != nil {
return nil, r.err
}
m[r.path] = r.sum
}
// Check whether the Walk failed.
if err := <-errc; err != nil {
return nil, err
}
return m, nil
}

关于Go语言并发模型,使用 Go内置的 channel 类型和 go 关键字实现高并发和并发控制的主题就先到这里。
在最近发布的 go 1.7中,在核心库中广泛加入了对 context 的支持,以便更好地控制并发和超时。但在这之前
golang.org/x/net/context 包就一直存在,下一期我们将对 context 包及其应用场景进行讨论。


相关链接:



  1. 原文链接

  2. serial.go

  3. parallel.go

  4. bounded.go

  5. golang.org/x/net/context


扫码关注微信公众号“深入Go语言”


在这里

Go语言并发模型:像Unix Pipe那样使用channel

文章分享ming 发表了文章 • 0 个评论 • 146 次浏览 • 2016-10-12 12:25 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Go语言的并发原语允许开发者以类似于 Unix Pipe 的方式构建数据流水线 (data pipelines),数据流水线能够高效地利用 I/O和多核 CPU 的优势。


本文要讲的就是一些使用流水线的一些例子,流水线的错误处理也是本文的重点。


阅读建议


数据流水线充分利用了多核特性,代码层面是基于 channel 类型 和 go 关键字。


channel 和 go 贯穿本文的始终。如果你对这两个概念不太了解,建议先阅读之前发布的两篇文章:Go 语言内存模型(上/下)


如果你对操作系统中"生产者"和"消费者"模型比较了解的话,也将有助于对本文中流水线的理解。


本文中绝大多数讲解都是基于代码进行的。换句话说,如果你看不太懂某些代码片段,建议补全以后,在机器或play.golang.org 上运行一下。对于某些不明白的细节,可以手动添加一些语句以助于理解。


由于 Go语言并发模型 的英文原文 Go Concurrency Patterns: Pipelines and cancellation 篇幅比较长,本文只包含 理论推导和简单的例子。
下一篇文章我们会对 "并行MD5" 这个现实生活的例子进行详细地讲解。


什么是 "流水线" (pipeline)?


对于"流水线"这个概念,Go语言中并没有正式的定义,它只是很多种并发方式的一种。这里我给出一个非官方的定义:一条流水线是 是由多个阶段组成的,相邻的两个阶段由 channel 进行连接;
每个阶段是由一组在同一个函数中启动的 goroutine 组成。在每个阶段,这些 goroutine 会执行下面三个操作:



  1. 通过 inbound channels 从上游接收数据

  2. 对接收到的数据执行一些操作,通常会生成新的数据

  3. 将新生成的数据通过 outbound channels 发送给下游


除了第一个和最后一个阶段,每个阶段都可以有任意个 inbound 和 outbound channel。
显然,第一个阶段只有 outbound channel,而最后一个阶段只有 inbound channel。
我们通常称第一个阶段为"生产者""源头",称最后一个阶段为"消费者""接收者"


首先,我们通过一个简单的例子来演示这个概念和其中的技巧。后面我们会更出一个真实世界的例子。


流水线入门:求平方数


假设我们有一个流水线,它由三个阶段组成。


第一阶段是 gen 函数,它能够将一组整数转换为channel,channel 可以将数字发送出去。
gen 函数首先启动一个 goroutine,该goroutine 发送数字到 channel,当数字发送完时关闭channel。
代码如下:


func gen(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}

第二阶段是 sq 函数,它从 channel 接收一个整数,然后返回 一个channel,返回的channel可以发送 接收到整数的平方。
当它的 inbound channel 关闭,并且把所有数字均发送到下游时,会关闭 outbound channel。代码如下:


func sq(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}

main 函数 用于设置流水线并运行最后一个阶段。最后一个阶段会从第二阶段接收数字,并逐个打印出来,直到来自于上游的 inbound channel关闭。代码如下:


func main() {
// 设置流水线
c := gen(2, 3)
out := sq(c)

// 消费输出结果
fmt.Println(<-out) // 4
fmt.Println(<-out) // 9
}

由于 sq 函数的 inbound channel 和 outbound channel 类型一样,所以组合任意个 sq 函数。比如像下面这样使用:


func main() {
// 设置流水线并消费输出结果
for n := range sq(sq(gen(2, 3))) {
fmt.Println(n) // 16 then 81
}
}

如果我们稍微修改一下 gen 函数,便可以模拟 haskell的惰性求值。有兴趣的读者可以自己折腾一下。


流水线进阶:扇入和扇出


扇出:同一个 channel 可以被多个函数读取数据,直到channel关闭。
这种机制允许将工作负载分发到一组worker,以便更好地并行使用 CPU 和 I/O。


扇入:多个 channel 的数据可以被同一个函数读取和处理,然后合并到一个 channel,直到所有 channel都关闭。


下面这张图对 扇入 有一个直观的描述:


扇入


我们修改一下上个例子中的流水线,这里我们运行两个 sq 实例,它们从同一个 channel 读取数据。
这里我们引入一个新函数 merge 对结果进行"扇入"操作:


func main() {
in := gen(2, 3)

// 启动两个 sq 实例,即两个goroutines处理 channel "in" 的数据
c1 := sq(in)
c2 := sq(in)

// merge 函数将 channel c1 和 c2 合并到一起,这段代码会消费 merge 的结果
for n := range merge(c1, c2) {
fmt.Println(n) // 打印 4 9, 或 9 4
}
}

merge 函数 将多个 channel 转换为一个 channel,它为每一个 inbound channel 启动一个 goroutine,用于将数据
拷贝到 outbound channel。
merge 函数的实现见下面代码 (注意 wg 变量):


func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为每一个输入channel cs 创建一个 goroutine output
// output 将数据从 c 拷贝到 out,直到 c 关闭,然后 调用 wg.Done
output := func(c <-chan int) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go output(c)
}

// 启动一个 goroutine,用于所有 output goroutine结束时,关闭 out
// 该goroutine 必须在 wg.Add 之后启动
go func() {
wg.Wait()
close(out)
}()
return out
}

在上面的代码中,每个 inbound channel 对应一个 output 函数。所有 output goroutine 被创建以后,merge 启动一个额外的 goroutine,
这个goroutine会等待所有 inbound channel 上的发送操作结束以后,关闭 outbound channel。


对已经关闭的channel 执行发送操作(ch<-)会导致异常,所以我们必须保证所有的发送操作都在关闭channel之前结束。
sync.WaitGroup 提供了一种组织同步的方式。
它保证 merge 中所有 inbound channel (cs ...<-chan int) 均被正常关闭, output goroutine 正常结束后,关闭 out channel。


停下来思考一下


在使用流水线函数时,有一个固定的模式:



  1. 在一个阶段,当所有发送操作 (ch<-) 结束以后,关闭 outbound channel

  2. 在一个阶段,goroutine 会持续从 inbount channel 接收数据,直到所有 inbound channel 全部关闭


在这种模式下,每一个接收阶段都可以写成 range 循环的方式,
从而保证所有数据都被成功发送到下游后,goroutine能够立即退出。


在现实中,阶段并不总是接收所有的 inbound 数据。有时候是设计如此:接收者可能只需要数据的一个子集就可以继续执行。
更常见的情况是:由于前一个阶段返回一个错误,导致该阶段提前退出。
这两种情况下,接收者都不应该继续等待后面的值被传送过来。


我们期望的结果是:当后一个阶段不需要数据时,上游阶段能够停止生产。


在我们的例子中,如果一个阶段不能消费所有的 inbound 数据,试图发送这些数据的 goroutine 会永久阻塞。看下面这段代码片段:


    // 只消费 out 的第一个数据
out := merge(c1, c2)
fmt.Println(<-out) // 4 or 9
return
// 由于我们不再接收 out 的第二个数据
// 其中一个 goroutine output 将会在发送时被阻塞
}

显然这里存在资源泄漏。一方面goroutine 消耗内存和运行时资源,另一方面goroutine 栈中的堆引用会阻止 gc 执行回收操作。
既然goroutine 不能被回收,那么他们必须自己退出。


我们重新整理一下流水线中的不同阶段,保证在下游阶段接收数据失败时,上游阶段也能够正常退出。
一个方式是使用带有缓冲的管道作为 outbound channel。缓存可以存储固定个数的数据。
如果缓存没有用完,那么发送操作会立即返回。看下面这段代码示例:


c := make(chan int, 2) // 缓冲大小为 2
c <- 1 // 立即返回
c <- 2 // 立即返回
c <- 3 // 该操作会被阻塞,直到有一个 goroutine 执行 <-c,并接收到数字 1

如果在创建 channel 时就知道要发送的值的个数,使用buffer就能够简化代码。
仍然使用求平方数的例子,我们对 gen 函数进行重写。我们将这组整型数拷贝到一个
缓冲 channel中,从而避免创建一个新的 goroutine:


func gen(nums ...int) <-chan int {
out := make(chan int, len(nums))
for _, n := range nums {
out <- n
}
close(out)
return out
}

回到 流水线中被阻塞的 goroutine,我们考虑让 merge 函数返回一个缓冲管道:


func merge(cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int, 1) // 在本例中存储未读的数据足够了
// ... 其他部分代码不变 ...

尽管这种方法解决了这个程序中阻塞 goroutine的问题,但是从长远来看,它并不是好办法。
缓存大小选择为1 是建立在两个前提之上:



  1. 我们已经知道 merge 函数有两个 inbound channel

  2. 我们已经知道下游阶段会消耗多少个值


这段代码很脆弱。如果我们在传入一个值给 gen 函数,或者下游阶段读取的值变少,goroutine
会再次被阻塞。


为了从根本上解决这个问题,我们需要提供一种机制,让下游阶段能够告知上游发送者停止接收的消息。
下面我们看下这种机制。


显式取消 (Explicit cancellation)


当 main 函数决定退出,并停止接收 out 发送的任何数据时,它必须告诉上游阶段的 goroutine 让它们放弃
正在发送的数据。 main 函数通过发送数据到一个名为 done 的channel实现这样的机制。 由于有两个潜在的
发送者被阻塞,它发送两个值。如下代码所示:


func main() {
in := gen(2, 3)

// 启动两个运行 sq 的goroutine
// 两个goroutine的数据均来自于 in
c1 := sq(in)
c2 := sq(in)

// 消耗 output 生产的第一个值
done := make(chan struct{}, 2)
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// 告诉其他发送者,我们将要离开
// 不再接收它们的数据
done <- struct{}{}
done <- struct{}{}
}

发送数据的 goroutine 使用一个 select 表达式代替原来的操作,select 表达式只有在接收到 out 或 done
发送的数据后,才会继续进行下去。 done 的值类型为 struct{} ,因为它发送什么值不重要,重要的是它发送没发送:
接收事件发生意味着 channel out 的发送操作被丢弃。 goroutine output 基于 inbound channel c 继续执行
循环,所以上游阶段不会被阻塞。(后面我们会讨论如何让循环提前退出)。 使用 done channel 方式实现的merge 函数如下:


func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为 cs 的的每一个 输入channel
// 创建一个goroutine。output函数将
// 数据从 c 拷贝到 out,直到c关闭,
// 或者接收到 done 信号;
// 然后调用 wg.Done()
output := func(c <-chan int) {
for n := range c {
select {
case out <- n:
case <-done:
}
}
wg.Done()
}
// ... the rest is unchanged ...

这种方法有一个问题:每一个下游的接收者需要知道潜在被阻上游发送者的个数,然后向这些发送者发送信号让它们提前退出。
时刻追踪这些数目是一项繁琐且易出错的工作。


我们需要一种方式能够让未知数目、且个数不受限制的goroutine 停止向下游发送数据。在Go语言中,我们可以通过关闭一个
channel 实现,因为在一个已关闭 channel 上执行接收操作(<-ch)总是能够立即返回,返回值是对应类型的零值。关于这点的细节,点击这里查看。


换句话说,我们只要关闭 done channel,就能够让解开对所有发送者的阻塞。对一个管道的关闭操作事实上是对所有接收者的广播信号。


我们把 done channel 作为一个参数传递给每一个 流水线上的函数,通过 defer 表达式声明对 done channel的关闭操作。
因此,所有从 main 函数作为源头被调用的函数均能够收到 done 的信号,每个阶段都能够正常退出。 使用 done 对main函数重构以后,代码如下:


func main() {
// 设置一个 全局共享的 done channel,
// 当流水线退出时,关闭 done channel
// 所有 goroutine接收到 done 的信号后,
// 都会正常退出。
done := make(chan struct{})
defer close(done)

in := gen(done, 2, 3)

// 将 sq 的工作分发给两个goroutine
// 这两个 goroutine 均从 in 读取数据
c1 := sq(done, in)
c2 := sq(done, in)

// 消费 outtput 生产的第一个值
out := merge(done, c1, c2)
fmt.Println(<-out) // 4 or 9

// defer 调用时,done channel 会被关闭。
}

现在,流水线中的每个阶段都能够在 done channel 被关闭时返回。merge 函数中的 output 代码也能够顺利返回,因为它
知道 done channel关闭时,上游发送者 sq 会停止发送数据。 在 defer 表达式执行结束时,所有调用链上的 output 都能保证 wg.Done() 被调用:


func merge(done <-chan struct{}, cs ...<-chan int) <-chan int {
var wg sync.WaitGroup
out := make(chan int)

// 为 cs 的每一个 channel 创建一个 goroutine
// 这个 goroutine 运行 output,它将数据从 c
// 拷贝到 out,直到 c 关闭,或者 接收到 done
// 的关闭信号。人啊后调用 wg.Done()
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
select {
case out <- n:
case <-done:
return
}
}
}
// ... the rest is unchanged ...

同样的原理, done channel 被关闭时,sq 也能够立即返回。在defer表达式执行结束时,所有调用链上的 sq 都能保证
out channel 被关闭。代码如下:


func sq(done <-chan struct{}, in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for n := range in {
select {
case out <- n * n:
case <-done:
return
}
}
}()
return out
}

这里,我们给出几条构建流水线的指导:



  1. 当所有发送操作结束时,每个阶段都关闭自己的 outbound channels

  2. 每个阶段都会一直从 inbound channels 接收数据,直到这些 channels 被关闭,或发送者解除阻塞状态。


流水线通过两种方式解除发送者的阻塞:



  1. 提供足够大的缓冲保存发送者发送的数据

  2. 接收者放弃 channel 时,显式地通知发送者。


结论


本文介绍了Go 语言中构建数据流水线的一些技巧。流水线的错误处理比较复杂,流水线的每个阶段都可能阻塞向下游发送数据,
下游阶段也可能不再关注上游发送的数据。上面我们介绍了通过关闭一个channel,向流水线中的所有 goroutine 发送一个 "done" 信号;也定义了
构建流水线的正确方法。


下一篇文章,我们将通过一个 并行 md5 的例子来说明本文所讲的一些理念和技巧。


原作者 Sameer Ajmani,翻译 Oscar


下期预告:Go语言并发模型:以并行md5计算为例。原文链接


相关链接:



  1. 原文链接:https://blog.golang.org/pipelines

  2. Go并发模型:http://talks.golang.org/2012/concurrency.slide#1

  3. Go高级并发模型:http://blog.golang.org/advanced-go-concurrency-patterns


扫码关注微信公众号“深入Go语言”


在这里

Go语言反射三定律

文章分享ming 发表了文章 • 0 个评论 • 151 次浏览 • 2016-10-12 12:20 • 来自相关话题

此文章已得到翻译者授权转载,点击查看原文

查看全部

此文章已得到翻译者授权转载,点击查看原文



简介


Reflection(反射)在计算机中表示 程序能够检查自身结构的能力,尤其是类型。它是元编程的一种形式,也是最容易让人迷惑的一部分。


本文中,我们将解释Go语言中反射的运作机制。每个编程语言的反射模型不大相同,很多语言索性就不支持反射(C、C++)。由于本文是介绍Go语言的,所以当我们谈到“反射”时,默认为是Go语言中的反射。


阅读建议


本文中,我们将解释Go语言中反射的运作机制。每个编程语言的反射模型不大相同,很多语言索性就不支持反射(C、C++)。


由于本文是介绍Go语言的,所以当我们谈到“反射”时,默认为是Go语言中的反射。


虽然Go语言没有继承的概念,但为了便于理解,如果一个struct A 实现了 interface B的所有方法时,我们称之为“继承”。


类型和接口


反射建立在类型系统之上,因此我们从类型基础知识说起。


Go是静态类型语言。每个变量都有且只有一个静态类型,在编译时就已经确定。比如 int、float32、*MyType、[]byte。 如果我们做出如下声明:


type MyInt int

var i int
var j MyInt

上面的代码中,变量 i 的类型是 int,j 的类型是 MyInt。 所以,尽管变量 i 和 j 具有共同的底层类型 int,但它们的静态类型并不一样。不经过类型转换直接相互赋值时,编译器会报错。


关于类型,一个重要的分类是 接口类型(interface),每个接口类型都代表固定的方法集合。一个接口变量就可以存储(或“指向”,接口变量类似于指针)任何类型的具体值,只要这个值实现了该接口类型的所有方法。一组广为人知的例子是 io.Reader 和 io.Writer, Reader 和 Writer 类型来源于 io包,声明如下:


// Reader is the interface that wraps the basic Read method.
type Reader interface {
Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
type Writer interface {
Write(p []byte) (n int, err error)
}

任何实现了 Read(Write)方法的类型,我们都称之为继承了 io.Reader(io.Writer)接口。换句话说, 一个类型为 io.Reader 的变量 可以指向(接口变量类似于指针)任何类型的变量,只要这个类型实现了Read 方法:


var r io.Reader
r = os.Stdin
r = bufio.NewReader(r)
r = new(bytes.Buffer)
// and so on

要时刻牢记:不管变量 r 指向的具体值是什么,它的类型永远是 io.Reader。再重复一次:Go语言是静态类型语言,变量 r 的静态类型是 io.Reader。


一个非常非常重要的接口类型是空接口,即:


interface{}

它代表一个空集,没有任何方法。由于任何具体的值都有 零或更多个方法,因此类型为interface{} 的变量能够存储任何值。


有人说,Go的接口是动态类型的。这个说法是错的!接口变量也是静态类型的,它永远只有一个相同的静态类型。如果在运行时它存储的值发生了变化,这个值也必须满足接口类型的方法集合。


由于反射和接口两者的关系很密切,我们必须澄清这一点。


接口变量的表示


Russ Cox 在2009年写了一篇文章介绍 Go中接口变量的表示形式,具体参考文章末尾的链接“Go语言接口的表示”。这里我们不需要重复所有的细节,只做一个简单的总结。


Interface变量存储一对值:赋给该变量的具体的值、值类型的描述符。更准确一点来说,值就是实现该接口的底层数据,类型是底层数据类型的描述。举个例子:


var r io.Reader
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
return nil, err
}
r = tty

在这个例子中,变量 r 在结构上包含一个 (value, type) 对:(tty, os.File)。注意:类型 os.File 不仅仅实现了 Read 方法。虽然接口变量只提供 Read 函数的调用权,但是底层的值包含了关于这个值的所有类型信息。所以我们能够做这样的类型转换:


var w io.Writer
w = r.(io.Writer)

上面代码的第二行是一个类型断言,它断定变量 r 内部的实际值也继承了 io.Writer接口,所以才能被赋值给 w。赋值之后,w 就指向了 (tty, *os.File) 对,和变量 r 指向的是同一个 (value, type) 对。不管底层具体值的方法集有多大,由于接口的静态类型限制,接口变量只能调用特定的一些方法。


我们继续往下看:


var empty interface{}
empty = w

这里的空接口变量 empty 也包含 (tty, *os.File) 对。这一点很容易理解:空接口变量可以存储任何具体值以及该值的所有描述信息。


细心的朋友可能会发现,这里没有使用类型断言,因为变量 w 满足 空接口的所有方法(传说中的“无招胜有招”)。在前一个例子中,我们把一个具体值 从 io.Reader 转换为 io.Writer 时,需要显式的类型断言,是因为 io.Writer 的方法集合 不是 io.Reader 的子集。


另外需要注意的一点是,(value, type) 对中的 type 必须是 具体的类型(struct或基本类型),不能是 接口类型。 接口类型不能存储接口变量。


关于接口,我们就介绍到这里,下面我们看看Go语言的反射三定律。


反射第一定律:反射可以将“接口类型变量”转换为“反射类型对象”。


注:这里反射类型指 reflect.Typereflect.Value


从用法上来讲,反射提供了一种机制,允许程序在运行时检查接口变量内部存储的 (value, type) 对。在最开始,我们先了解下 reflect 包的两种类型:Type 和 Value。这两种类型使访问接口内的数据成为可能。它们对应两个简单的方法,分别是 reflect.TypeOf 和 reflect.ValueOf,分别用来读取接口变量的 reflect.Type 和 reflect.Value 部分。当然,从 reflect.Value 也很容易获取到 reflect.Type。目前我们先将它们分开。


首先,我们下看 reflect.TypeOf:


package main

import (
"fmt"
"reflect"
)

func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}

这段代码会打印出:


type: float64

你可能会疑惑:为什么没看到接口?这段代码看起来只是把一个 float64类型的变量 x 传递给 reflect.TypeOf,并没有传递接口。事实上,接口就在那里。查阅一下TypeOf 的文档,你会发现 reflect.TypeOf 的函数签名里包含一个空接口:


// TypeOf returns the reflection Type of the value in the interface{}.
func TypeOf(i interface{}) Type

我们调用 reflect.TypeOf(x) 时,x 被存储在一个空接口变量中被传递过去; 然后reflect.TypeOf 对空接口变量进行拆解,恢复其类型信息。


函数 reflect.ValueOf 也会对底层的值进行恢复(这里我们忽略细节,只关注可执行的代码):


var x float64 = 3.4
fmt.Println("value:", reflect.ValueOf(x))

上面这段代码打印出:


value: <float64 Value>

类型 reflect.Type 和 reflect.Value 都有很多方法,我们可以检查和使用它们。这里我们举几个例子。类型 reflect.Value 有一个方法 Type(),它会返回一个 reflect.Type 类型的对象。Type和 Value都有一个名为 Kind 的方法,它会返回一个常量,表示底层数据的类型,常见值有:Uint、Float64、Slice等。Value类型也有一些类似于Int、Float的方法,用来提取底层的数据。Int方法用来提取 int64, Float方法用来提取 float64,参考下面的代码:


var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

上面这段代码会打印出:


type: float64
kind is float64: true
value: 3.4

还有一些用来修改数据的方法,比如SetInt、SetFloat,在讨论它们之前,我们要先理解“可修改性”(settability),这一特性会在“反射第三定律”中进行详细说明。


反射库提供了很多值得列出来单独讨论的属性。首先是介绍下Value 的 getter 和 setter 方法。为了保证API 的精简,这两个方法操作的是某一组类型范围最大的那个。比如,处理任何含符号整型数,都使用 int64。也就是说 Value 类型的Int 方法返回值为 int64类型,SetInt 方法接收的参数类型也是 int64 类型。实际使用时,可能需要转化为实际的类型:


var x uint8 = 'x'
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type()) // uint8.
fmt.Println("kind is uint8: ", v.Kind() == reflect.Uint8) // true.
x = uint8(v.Uint()) // v.Uint returns a uint64.

第二个属性是反射类型变量(reflection object)的 Kind 方法 会返回底层数据的类型,而不是静态类型。如果一个反射类型对象包含一个用户定义的整型数,看代码:


type MyInt int
var x MyInt = 7
v := reflect.ValueOf(x)

上面的代码中,虽然变量 v 的静态类型是MyInt,不是 int,Kind 方法仍然返回 reflect.Int。换句话说, Kind 方法不会像 Type 方法一样区分 MyInt 和 int。


反射第二定律:反射可以将“反射类型对象”转换为“接口类型变量”。


和物理学中的反射类似,Go语言中的反射也能创造自己反面类型的对象。


根据一个 reflect.Value 类型的变量,我们可以使用 Interface 方法恢复其接口类型的值。事实上,这个方法会把 type 和 value 信息打包并填充到一个接口变量中,然后返回。其函数声明如下:


// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}

然后,我们可以通过断言,恢复底层的具体值:


y := v.Interface().(float64) // y will have type float64.
fmt.Println(y)

上面这段代码会打印出一个 float64 类型的值,也就是 反射类型变量 v 所代表的值。


事实上,我们可以更好地利用这一特性。标准库中的 fmt.Println 和 fmt.Printf 等函数都接收空接口变量作为参数,fmt 包内部会对接口变量进行拆包(前面的例子中,我们也做过类似的操作)。因此,fmt 包的打印函数在打印 reflect.Value 类型变量的数据时,只需要把 Interface 方法的结果传给 格式化打印程序:


fmt.Println(v.Interface())

你可能会问:问什么不直接打印 v ,比如 fmt.Println(v)? 答案是 v 的类型是 reflect.Value,我们需要的是它存储的具体值。由于底层的值是一个 float64,我们可以格式化打印:


fmt.Printf("value is %7.1e\n", v.Interface())

上面代码的打印结果是:


3.4e+00

同样,这次也不需要对 v.Interface() 的结果进行类型断言。空接口值内部包含了具体值的类型信息,Printf 函数会恢复类型信息。


简单来说,Interface 方法和 ValueOf 函数作用恰好相反,唯一一点是,返回值的静态类型是 interface{}。


我们重新表述一下:Go的反射机制可以将“接口类型的变量”转换为“反射类型的对象”,然后再将“反射类型对象”转换过去。


反射第三定律:如果要修改“反射类型对象”,其值必须是“可写的”(settable)。


这条定律很微妙,也很容易让人迷惑。但是如果你从第一条定律开始看,应该比较容易理解。


下面这段代码不能正常工作,但是非常值得研究:


var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1) // Error: will panic.

如果你运行这段代码,它会抛出抛出一个奇怪的异常:


panic: reflect.Value.SetFloat using unaddressable value

这里问题不在于值 7.1 不能被寻址,而是因为变量 v 是“不可写的”。“可写性”是反射类型变量的一个属性,但不是所有的反射类型变量都拥有这个属性。


我们可以通过 CanSet 方法检查一个 reflect.Value 类型变量的“可写性”。对于上面的例子,可以这样写:


var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("settability of v:", v.CanSet())

上面这段代码打印结果是:


settability of v: false

对于一个不具有“可写性”的 Value类型变量,调用 Set 方法会报出错误。首先,我们要弄清楚什么“可写性”。


“可写性”有些类似于寻址能力,但是更严格。它是反射类型变量的一种属性,赋予该变量修改底层存储数据的能力。“可写性”最终是由一个事实决定的:反射对象是否存储了原始值。举个代码例子:


var x float64 = 3.4
v := reflect.ValueOf(x)

这里我们传递给 reflect.ValueOf 函数的是变量 x 的一个拷贝,而非 x 本身。想象一下,如果下面这行代码能够成功执行:


v.SetFloat(7.1)

答案是:如果这行代码能够成功执行,它不会更新 x ,虽然看起来变量 v 是根据 x 创建的。相反,它会更新 x 存在于 反射对象 v 内部的一个拷贝,而变量 x 本身完全不受影响。这会造成迷惑,并且没有任何意义,所以是不合法的。“可写性”就是为了避免这个问题而设计的。


这看起来很诡异,事实上并非如此,而且类似的情况很常见。考虑下面这行代码:


f(x)

上面的代码中,我们把变量 x 的一个拷贝传递给函数,因此不期望它会改变 x 的值。如果期望函数 f 能够修改变量 x,我们必须传递 x 的地址(即指向 x 的指针)给函数 f,如下:


f(&x)

你应该很熟悉这行代码,反射的工作机制是一样的。如果你想通过反射修改变量 x,就咬吧想要修改的变量的指针传递给 反射库。


首先,像通常一样初始化变量 x,然后创建一个指向它的 反射对象,名字为 p:


var x float64 = 3.4
p := reflect.ValueOf(&x) // Note: take the address of x.
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:", p.CanSet())

这段代码的输出是:


type of p: *float64
settability of p: false

反射对象 p 是不可写的,但是我们也不像修改 p,事实上我们要修改的是 *p。为了得到 p 指向的数据,可以调用 Value 类型的 Elem 方法。Elem 方法能够对指针进行“解引用”,然后将结果存储到反射 Value类型对象 v中:


v := p.Elem()
fmt.Println("settability of v:", v.CanSet())

在上面这段代码中,变量 v 是一个可写的反射对象,代码输出也验证了这一点:


settability of v: true

由于变量 v 代表 x, 因此我们可以使用 v.SetFloat 修改 x 的值:


v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)

上面代码的输出如下:


7.1
7.1

反射不太容易理解,reflect.Type 和 reflect.Value 会混淆正在执行的程序,但是它做的事情正是编程语言做的事情。你只需要记住:只要反射对象要修改它们表示的对象,就必须获取它们表示的对象的地址。


结构体(struct)


在前面的例子中,变量 v 本身并不是指针,它只是从指针衍生而来。把反射应用到结构体时,常用的方式是 使用反射修改一个结构体的某些字段。只要拥有结构体的地址,我们就可以修改它的字段。


下面通过一个简单的例子对结构体类型变量 t 进行分析。


首先,我们创建了反射类型对象,它包含一个结构体的指针,因为后续会修改。


然后,我们设置 typeOfT 为它的类型,并遍历所有的字段。


注意:我们从 struct 类型提取出每个字段的名字,但是每个字段本身也是常规的 reflect.Value 对象。


type T struct {
A int
B string
}
t := T{23, "skidoo"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}

上面这段代码的输出如下:


0: A int = 23
1: B string = skidoo

这里还有一点需要指出:变量 T 的字段都是首字母大写的(暴露到外部),因为struct中只有暴露到外部的字段才是“可写的”。


由于变量 s 包含一个“可写的”反射对象,我们可以修改结构体的字段:


f.Interface())s.Field(0).SetInt(77)
s.Field(1).SetString("Sunset Strip")
fmt.Println("t is now", t)

上面代码的输出如下:


t is now {77 Sunset Strip}

如果变量 s 是通过 t ,而不是 &t 创建的,调用 SetInt 和 SetString 将会失败,因为 t 的字段不是“可写的”。


结论


最后再次重复一遍反射三定律:



  1. 反射可以将“接口类型变量”转换为“反射类型对象”。

  2. 反射可以将“反射类型对象”转换为“接口类型变量”。

  3. 如果要修改“反射类型对象”,其值必须是“可写的”(settable)。
    一旦你理解了这些定律,使用反射将会是一件非常简单的事情。它是一件强大的工具,使用时务必谨慎使用,更不要滥用。


关于反射,我们还有很多内容没有讨论,包括基于管道的发送和接收、内存分配、使用slice和map、调用方法和函数,由于本文已经非常长了,这些话题在后续的文章中介绍。


原作者 Rob Pike,翻译Oscar


相关链接:


原文链接:https://blog.golang.org/laws-of-reflection


reflect 包:https://golang.org/pkg/reflect/


扫码关注微信公众号“深入Go语言”


在这里

经常有人打不开beego.me,这是什么问题呢?

有问必答xieyanke 回复了问题 • 3 人关注 • 3 个回复 • 217 次浏览 • 2016-10-12 12:19 • 来自相关话题

深入理解Go语言的slice

文章分享ming 发表了文章 • 1 个评论 • 143 次浏览 • 2016-10-12 12:09 • 来自相关话题

先看这段代码,结果是[0 2 3],很多人都能答对。

func modify(s []int) {
    s[0] = 0
}
func ... 			查看全部
					

先看这段代码,结果是[0 2 3],很多人都能答对。


func modify(s []int) {
s[0] = 0
}
func main() {
s := []int{1, 2, 3}
modify(s)
fmt.Println(s)
}

然后稍微改动一下,再猜一下结果


func pop(s []int) {
s = s[:len(s)-1]
}
func main() {
s := []int{1, 2, 3}
pop(s)
fmt.Println(s)
}

如果认为输出[1 2]的话那么你错了,结果是[1 2 3],你可能会觉得很奇怪,slice是引用语义这个在第一个例子中已经证明了,为什么第二个例子中又不是这样呢。


我们对中间过程加一些输出,再来看看


func pop(s []int) {
fmt.Printf("[pop] s addr:%p\n", &s)
s = s[:len(s)-1]
fmt.Println("[pop] s value:", s)
}
func main() {
s := []int{1, 2, 3}
fmt.Printf("[main] s addr:%p\n", &s)
pop(s)
fmt.Println("[main] s value:", s)
}

运行上面代码输出如下


[main] s addr:0xc082004640
[pop] s addr:0xc0820046c0
[pop] s value: [1 2]
[main] s value: [1 2 3]

看到上面的结果,可以知道pop()中的s并不是引用,而是一个副本,虽然在pop()内部修改成功,但并没有影响到main()中的s。但第一个例子却修改成功了,这又是为什么。


下面来看下slice的实现,就能很清楚的了解原因了。

slice是由长度固定的数组实现的。当使用内建函数append()向slice添加元素时,如果超过底层的数组长度则会重新分配空间(与C++的vector类似)。

可以把slice认为是下面这样的一个结构体(先不考虑slice的容量)。Lenght表示slice的长度,`ZerothElement表示底层数组的头指针


type sliceHeader struct {
Length int
ZerothElement *byte
}

参照这个结构体的定义和下面的说明,就能很清楚地了解开始的两个例子了


clipboard.png


那当我们需要将slice做为函数参数传入,并且函数会修改slice时,怎么办呢。这里说三种方法。
1.将slice指针做为参数,而不是slice


func modify(s *[]int) {
// do something
}

2.把函数内被修改后的slice做为返回值,将函数返回值赋值给原始slice


func modify(s []int) []int {
// do something
return s
}
func main() {
s := []int{1, 2, 3}
s = modify(s)
}

3.将函数做为slice指针的方法


type slice []int

func (s *slice) modify() {
// do something
}

深入理解go的slice和到底什么时候该用slice

文章分享sheepbao 发表了文章 • 1 个评论 • 106 次浏览 • 2016-10-12 12:09 • 来自相关话题

前言

用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特性可以很容易的切割slice,它们是怎么... 查看全部

前言


用过go语言的亲们都知道,slice(中文翻译为切片)在编程中经常用到,它代表变长的序列,序列中每个元素都有相同的类型,类似一个动态数组,利用append可以实现动态增长,利用slice的特性可以很容易的切割slice,它们是怎么实现这些特性的呢?现在我们来探究一下这些特性的本质是什么。


先了解一下slice的特性



  • 定义一个slice
    s := []int{1,2,3,4,5}
    fmt.Println(s) // [1 2 3 4 5]

    一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。


  • slice的扩容
    s := []int{1,2,3,4,5}
    s = append(s, 6)
    fmt.Println(s) // [1 2 3 4 5 6]

    内置append函数在现有数组的长度 < 1024 时 cap 增长是翻倍的,再往上的增长率则是 1.25,至于为何后面会说。


  • slice的切割
    s := []int{1,2,3,4,5,6}
    s1 := s[0:2]
    fmt.Println(s1) // [1 2]
    s2 := s[4:]
    fmt.Println(s2) // [5 6]
    s3 := s[:4]
    fmt.Println(s3) // [1 2 3 4]


  • slice作为函数参数


    package main

    import "fmt"

    func main() {

    slice_1 := []int{1, 2, 3, 4, 5}
    fmt.Printf("main-->data:\t%#v\n", slice_1)
    fmt.Printf("main-->len:\t%#v\n", len(slice_1))
    fmt.Printf("main-->cap:\t%#v\n", cap(slice_1))
    test1(slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

    test2(&slice_1)
    fmt.Printf("main-->data:\t%#v\n", slice_1)

    }

    func test1(slice_2 []int) {
    slice_2[1] = 6666 // 函数外的slice确实有被修改
    slice_2 = append(slice_2, 8888) // 函数外的不变
    fmt.Printf("test1-->data:\t%#v\n", slice_2)
    fmt.Printf("test1-->len:\t%#v\n", len(slice_2))
    fmt.Printf("test1-->cap:\t%#v\n", cap(slice_2))
    }

    func test2(slice_2 *[]int) { // 这样才能修改函数外的slice
    *slice_2 = append(*slice_2, 6666)
    }

    结果:


    main-->data:    []int{1, 2, 3, 4, 5}
    main-->len: 5
    main-->cap: 5
    test1-->data: []int{1, 6666, 3, 4, 5, 8888}
    test1-->len: 6
    test1-->cap: 12
    main-->data: []int{1, 6666, 3, 4, 5}
    main-->data: []int{1, 6666, 3, 4, 5, 6666}

    这里要注意注释的地方,为何slice作为值传递参数,函数外的slice也被更改了?为何在函数内append不能改变函数外的slice?要回da这些问题就得了解slice内部结构,详细请看下面.




slice的内部结构


其实slice在Go的运行时库中就是一个C语言动态数组的实现,在$GOROOT/src/pkg/runtime/runtime.h中可以看到它的定义:


struct    Slice
{ // must not move anything
byte* array; // actual data
uintgo len; // number of elements
uintgo cap; // allocated number of elements
};

这个结构有3个字段,第一个字段表示array的指针,就是真实数据的指针(这个一定要注意),所以才经常说slice是数组的引用,第二个是表示slice的长度,第三个是表示slice的容量,注意:len和cap都不是指针


现在就可以解释前面的例子slice作为函数参数提出的问题:
函数外的slice叫slice_1,函数的参数叫slice_2,当函数传递slice_1的时候,其实传入的确实是slice_1参数的复制,所以slice_2复制了slise_1,但要注意的是slice_2里存储的数组的指针,所以当在函数内更改数组内容时,函数外的slice_1的内容也改变了。在函数内用append时,append会自动以倍增的方式扩展slice_2的容量,但是扩展也仅仅是函数内slice_2的长度和容量,slice_1的长度和容量是没变的,所以在函数外打印时看起来就是没变。


append的运作机制


在对slice进行append等操作时,可能会造成slice的自动扩容。其扩容时的大小增长规则是:



  • 如果新的slice大小是当前大小2倍以上,则大小增长为新大小

  • 否则循环以下操作:如果当前slice大小小于1024,按每次2倍增长,否则每次按当前大小1/4增长。直到增长的大小超过或等于新大小。

  • append的实现只是简单的在内存中将旧slice复制给新slice


至于为何会这样,你要看一下golang的源码就知道了:
https://github.com/golang/go/blob/master/src/runtime/slice.go


    newcap := old.cap
if newcap+newcap < cap {
newcap = cap
} else {
for {
if old.len < 1024 {
newcap += newcap
} else {
newcap += newcap / 4
}
if newcap >= cap {
break
}
}
}

为何不用动态链表实现slice?



  • 首先拷贝一断连续的内存是很快的,假如不想发生拷贝,也就是用动态链表,那你就没有连续内存。此时随机访问开销会是:链表 O(N), 2倍增长块链 O(LogN),二级表一个常数很大的O(1)。问题不仅是算法上开销,还有内存位置分散而对缓存高度不友好,这些问题i在连续内存方案里都是不存在的。除非你的应用是狂append然后只顺序读一次,否则优化写而牺牲读都完全不 make sense. 而就算你的应用是严格顺序读,缓存命中率也通常会让你的综合效率比拷贝换连续内存低。

  • 对小 slice 来说,连续 append 的开销更多的不是在 memmove, 而是在分配一块新空间的 memory allocator 和之后的 gc 压力(这方面对链表更是不利)。所以,当你能大致知道所需的最大空间(在大部分时候都是的)时,在make的时候预留相应的 cap 就好。如果所需的最大空间很大而每次使用的空间量分布不确定,那你就要在浪费内存和耗 CPU 在 allocator + gc 上做权衡。

  • Go 在 append 和 copy 方面的开销是可预知+可控的,应用上简单的调优有很好的效果。这个世界上没有免费的动态增长内存,各种实现方案都有设计权衡。


什么时候该用slice?


在go语言中slice是很灵活的,大部分情况都能表现的很好,但也有特殊情况。
当程序要求slice的容量超大并且需要频繁的更改slice的内容时,就不应该用slice,改用list更合适。

for j:=0;j<4 ;j++ { } 与for _,v :=range persons {} 区别

有问必答sheepbao 回复了问题 • 4 人关注 • 3 个回复 • 271 次浏览 • 2016-10-12 11:44 • 来自相关话题

Beego能否在静态文件的header上动态增加no cache?

有问必答astaxie 回复了问题 • 2 人关注 • 1 个回复 • 210 次浏览 • 2016-10-12 10:20 • 来自相关话题

beego框架 个人博客系统

有问必答朋也 回复了问题 • 6 人关注 • 3 个回复 • 430 次浏览 • 2016-10-12 09:56 • 来自相关话题

如果用go重构samba为分布式的软件,会有前景吗?

有问必答leoliu 回复了问题 • 2 人关注 • 1 个回复 • 185 次浏览 • 2016-10-12 09:52 • 来自相关话题

一起来总结一下 golang 的一些最佳实践

技术讨论astaxie 回复了问题 • 3 人关注 • 2 个回复 • 334 次浏览 • 2016-10-12 09:51 • 来自相关话题

请问go的windows gui库walk中提及的syso文件是否哪位兄弟有过比较深入的研究?

技术讨论astaxie 回复了问题 • 2 人关注 • 1 个回复 • 241 次浏览 • 2016-10-12 09:45 • 来自相关话题