NYC's Blog - Go http://niyanchun.com/tag/go/ zh-CN Thu, 25 May 2017 11:42:00 +0800 Thu, 25 May 2017 11:42:00 +0800 并发不是并行 http://niyanchun.com/Concurrency-is-not-Parallelism.html http://niyanchun.com/Concurrency-is-not-Parallelism.html Thu, 25 May 2017 11:42:00 +0800 NYC 现在我们都说设计可并行的程序、高并发的程序?而且很多时候潜意识里面觉得对并行(Parallelism)和并发(Concurrency)的的区别很清楚,但是如果要明确的说出二者的区别,又感觉没法给出一个非常清晰的描述。最近看到Go语言发明者之一Rob Pike的一篇演讲《Concurrency is not Parallelism》,觉得挺不错的。因为特别喜欢的里面的配图示例,所以我决定写本篇博客,将其转过来,也方便一些网络不畅的人。下面给出原文涉及的一些链接:

演讲幻灯片请移步:https://talks.golang.org/2012/waza.slide
演讲视频请移步:https://www.youtube.com/watch?v=cN_DpYBzKso&t=550s

那什么是并发?什么又是并行呢?并行的概念比较简单,并行总是和执行(executions)相关,很多东西同时执行就是并行;而并发则是通过一些方式组织你的程序,让它可以分成多个模块去独立的执行。并行必然是需要多核的,一个处理器是无法并行的;但并发和处理器并没有什么必然联系,在一个处理器上面,我们的程序也可以是并发的。好吧,有点绕。我们直接上Rob Pike的例子:这个例子是gopher们需要将一堆过时的语言指导书用小推车推到垃圾焚烧炉去烧毁(这个例子也是非常搞笑...)。

刚开始,我们只有一个gopher,完成任务必然需要比较长的时间:

gophersimple1.jpg

此时,如果再增加一个gopher,那也没用,因为它没有车...

gophersimple3.jpg

好吧,那我们再找辆车:

gophersimple2.jpg

这样虽然比之前快了,但还是有瓶颈的。因为书只有一堆,火炉也只有一个,所以我们还必须通过消息来协调两个gopher的行动。好吧,那我们再把书分成两堆,再增加一个火炉:

gophersimple4.jpg

这样就OK了,我们就可以比之前快差不多一倍了。

这个模型就是并发的,因为两个gopher可以独立的完成一件事了;但是却不一定是并行的,比如可能同一时刻只有一个gopher是干活的。但是我们这样去组织的话,让程序并行执行也会非常的容易。当然,除了这种,我们还可以设计出来很多并发模型,继续接着看漫画...

这次我们找了3个gopher,一个专门负责装车,一个专门负责运输,一个专门负责卸货焚烧,当然三个gopher之间需要使用一些诸如消息通信之类的手段进行协调。

gophercomplex0.jpg

然而我们却发现运输的这个gopher很累,有瓶颈。好,我们再招聘一个gopher,专门负责还空车:

gophercomplex1.jpg

两个gopher去搞运输,这样如果协调的好的话,理论情况下工作效率将是一个gopher的4倍。

这个新的并发模型就会比之前的模型更高级一点了,此时我们再将这种并发模型并行化:

gophercomplex2.jpg

漫画还没完,我们接着看另外一种并发模型:

运输的gopher抱怨说运输路程太远,那我们就增加一个中转站(我觉得实际中这个gopher很可能会被解雇):

gophercomplex3.jpg

然后再将这种并发模型并行化:

gophercomplex4.jpg

我们也可以这样设计并发模型:

gophercomplex5.jpg

然后再并行化:

请输入图片描述

OK,至此漫画就完了。可以看到有很多种并发模型,每种模型也可以很容易的并行化起来。回到程序中,书就代表着数据;gopher就是CPU;而车可能就是一些序列化、反序列化、网络等设施;火炉就是代理、浏览器或者其他的消费者。而上面的并发模型就是一个可扩展的Web Service。

至此,我们对于并发和并行的关系应该比较清楚了。最后再引用一些描述:

Concurrency is about dealing with lots of things at once.
Parallelism is about doing lots of things at once.
Not the same, but related.
Concurrency is about structure, parallelism is about execution.
Concurrency provides a way to structure a solution to solve a problem that may (but not necessarily) be parallelizable.
Parallelism is simply running things in parallel.
Concurrency is a way to structure your program.

如果喜欢看图的,这里再推荐一篇2016年Gopher大会上的一个演讲的文章:Visualizing Concurrency in Go,里面的配图也非常的炫酷哦~

]]>
0 http://niyanchun.com/Concurrency-is-not-Parallelism.html#comments http://niyanchun.com/feed/tag/go/
Go缓存库cache2go介绍 http://niyanchun.com/cache2go-introduction.html http://niyanchun.com/cache2go-introduction.html Fri, 14 Apr 2017 14:28:00 +0800 NYC cache2go是一个用Go实现的并发安全的缓存库,实现了如下特性:

  • 并发安全;
  • 可设置每条缓存的超时时间;
  • 内置缓存访问计数;
  • 自调节的缓存过期检查;
  • 可设置缓存增加/删除回调函数;
  • and so on...

这个库代码量很少,核心代码就三个文件,里面设计的技术点主要包括读写锁、goroutine、map操作等。作为Go语言学习样例也非常不错。

1. 源码解析

cache2go中主要涉及两个类型CacheItemCacheTable

  • 一个CacheItem代表一条缓存;
  • 一个CacheTable代表一个缓存表,由一条条缓存组成。

下面我们分别介绍。

1.1 单条缓存CacheItem

CacheItem的结构如下:

type CacheItem struct {
    sync.RWMutex

    // 缓存项的key.
    key interface{}
    // 缓存项的值.
    data interface{}
    // 缓存项的生命期
    lifeSpan time.Duration

    // 缓存项的创建时间戳
    createdOn time.Time
    // 缓存项上次被访问的时间戳
    accessedOn time.Time
    // 缓存项被访问的次数
    accessCount int64

    // 缓存项被删除时的回调函数(删之前执行)
    aboutToExpire func(key interface{})
}

从结构体可以看到:

  • datainterface{}类型的,也就是说缓存的值可以是任意类型;但key因为是map的key,所以必须是可比较的类型;
  • 每条缓存都可以设置生命期,即lifeSpan字段;
  • 每条缓存的访问技术就是有accessCount记录的;
  • 可以为每条缓存设置删除时的回调函数,该回调函数将在该条缓存被删除之前执行,其函数签名为func(key interface{})

CacheItem也定义了一些常用的方法来获取和设置其成员变量,都非常简单,这里就不介绍了。

1.2 缓存表CacheTable

CacheTable结构如下:

type CacheTable struct {
    sync.RWMutex

    // 缓存表名
    name string
    // 缓存项
    items map[interface{}]*CacheItem

    // 触发缓存清理的定时器
    cleanupTimer *time.Timer
    // 缓存清理周期
    cleanupInterval time.Duration

    // 该缓存表的日志
    logger *log.Logger

    // 获取一个不存在的缓存项时的回调函数
    loadData func(key interface{}, args ...interface{}) *CacheItem
    // 向缓存表增加缓存项时的回调函数
    addedItem func(item *CacheItem)
    // 从缓存表删除一个缓存项时的回调函数
    aboutToDeleteItem func(item *CacheItem)
}

从结构体可以看出缓存表就是由一个值类型为缓存项的map以及一些附加属性组成:

  • name用来唯一标识一个缓存表,在创建缓存表时指定。
  • items是一个存储缓存项的map。
  • cleanupTimercleanupInterval来控制多久更新一次缓存。
  • 相比于缓存项,缓存表多了一些回调函数。缓存表指定的回调函数作用于缓存表内的所有缓存项,而缓存项指定的回调函数仅对单条缓存有效。

缓存表提供了缓存常见的操作方法:

  • 增:AddNotFoundAdd
  • 删:Delete
  • 查:Value
  • 是否存在:Exists
  • 缓存总数:Count
  • 缓存刷新:Flush
  • 缓存遍历:Foreach
  • 回调函数设置:SetAboutToDeleteItemCallbackSetAddedItemCallbackSetDataLoader
  • 访问最多的前几个缓存项:MostAccessed
  • and so on...

为了提供访问最多的前几个缓存项,cache2go又定义了CacheItemPairCacheItemPairListCacheItemPair有缓存的keyAccessCount组成,而CacheItemPairList则是CacheItemPair组成的Slice,且实现了Sort接口。

前面我们提到过cache2go的一个特性是"自调节的缓存过期检查",我们看一下这个特性的实现代码:

// Expiration check loop, triggered by a self-adjusting timer.
func (table *CacheTable) expirationCheck() {
    table.Lock()
    if table.cleanupTimer != nil {
        table.cleanupTimer.Stop()
    }
    if table.cleanupInterval > 0 {
        table.log("Expiration check triggered after", table.cleanupInterval, "for table", table.name)
    } else {
        table.log("Expiration check installed for table", table.name)
    }

    // Cache value so we don't keep blocking the mutex.
    items := table.items
    table.Unlock()

    // To be more accurate with timers, we would need to update 'now' on every
    // loop iteration. Not sure it's really efficient though.
    now := time.Now()
    smallestDuration := 0 * time.Second
    for key, item := range items {
        // Cache values so we don't keep blocking the mutex.
        item.RLock()
        lifeSpan := item.lifeSpan
        accessedOn := item.accessedOn
        item.RUnlock()

        if lifeSpan == 0 {
            continue
        }
        if now.Sub(accessedOn) >= lifeSpan {
            // Item has excessed its lifespan.
            table.Delete(key)
        } else {
            // Find the item chronologically closest to its end-of-lifespan.
            if smallestDuration == 0 || lifeSpan-now.Sub(accessedOn) < smallestDuration {
                smallestDuration = lifeSpan - now.Sub(accessedOn)
            }
        }
    }

    // Setup the interval for the next cleanup run.
    table.Lock()
    table.cleanupInterval = smallestDuration
    if smallestDuration > 0 {
        table.cleanupTimer = time.AfterFunc(smallestDuration, func() {
            go table.expirationCheck()
        })
    }
    table.Unlock()
}

代码中会去遍历所有缓存项,找到最快要被淘汰掉的缓存项的的时间作为cleanupInterval,即下一次启动缓存刷新的时间,从而保证可以及时的更新缓存,可以看到其实质就是自调节下一次启动缓存更新的时间。另外我们也注意到,如果lifeSpan设置为0的话,就不会被淘汰,即永久有效。

1.3 缓存

前面介绍了缓存项和由缓存项组成的缓存表,最后我们来看cache2go中的缓存:

var (
    cache = make(map[string]*CacheTable)
    mutex sync.RWMutex
)

// Cache returns the existing cache table with given name or creates a new one
// if the table does not exist yet.
func Cache(table string) *CacheTable {
    mutex.RLock()
    t, ok := cache[table]
    mutex.RUnlock()

    if !ok {
        t = &CacheTable{
            name:  table,
            items: make(map[interface{}]*CacheItem),
        }

        mutex.Lock()
        cache[table] = t
        mutex.Unlock()
    }

    return t
}

使用cache2go时,我们第一步一般都是调用上面的Cache函数创建缓存,该函数会检查一个全局变量cache(该变量是一个map,其值类型为*CacheTable,key是缓存表的名字),如果该map中已经有名字为table的缓存表,就返回该缓存表;否则就创建。该函数返回缓存表的指针,即*CacheTable。也就是说在cache2go中,缓存表就代表一个缓存,而我们可以创建多个不同名字的缓存,存储在全局变量cache中。

2. 使用示例

这里的三个例子均来自cache2go,分别演示了缓存的基本使用、回调函数的使用、dataloader回调的使用。

2.1 缓存的基本使用

package main

import (
    "fmt"
    "time"

    "github.com/muesli/cache2go"
)

// Keys & values in cache2go can be off arbitrary types, e.g. a struct.
type myStruct struct {
    text     string
    moreData []byte
}

func main() {
    // Accessing a new cache table for the first time will create it.
    cache := cache2go.Cache("myCache")

    // We will put a new item in the cache. It will expire after
    // not being accessed via Value(key) for more than 5 seconds.
    val := myStruct{"This is a test!", []byte{}}
    cache.Add("someKey", 5*time.Second, &val)

    // Let's retrieve the item from the cache.
    res, err := cache.Value("someKey")
    if err == nil {
        fmt.Println("Found value in cache:", res.Data().(*myStruct).text)
    } else {
        fmt.Println("Error retrieving value from cache:", err)
    }

    // Wait for the item to expire in cache.
    time.Sleep(6 * time.Second)
    res, err = cache.Value("someKey")
    if err != nil {
        fmt.Println("Item is not cached (anymore).")
    }

    // Add another item that never expires.
    cache.Add("someKey", 0, &val)

    // cache2go supports a few handy callbacks and loading mechanisms.
    cache.SetAboutToDeleteItemCallback(func(e *cache2go.CacheItem) {
        fmt.Println("Deleting:", e.Key(), e.Data().(*myStruct).text, e.CreatedOn())
    })

    // Remove the item from the cache.
    cache.Delete("someKey")

    // And wipe the entire cache table.
    cache.Flush()
}

2.2 回调函数的使用

package main

import (
    "fmt"
    "time"

    "github.com/muesli/cache2go"
)

func main() {
    cache := cache2go.Cache("myCache")

    // This callback will be triggered every time a new item
    // gets added to the cache.
    cache.SetAddedItemCallback(func(entry *cache2go.CacheItem) {
        fmt.Println("Added:", entry.Key(), entry.Data(), entry.CreatedOn())
    })
    // This callback will be triggered every time an item
    // is about to be removed from the cache.
    cache.SetAboutToDeleteItemCallback(func(entry *cache2go.CacheItem) {
        fmt.Println("Deleting:", entry.Key(), entry.Data(), entry.CreatedOn())
    })

    // Caching a new item will execute the AddedItem callback.
    cache.Add("someKey", 0, "This is a test!")

    // Let's retrieve the item from the cache
    res, err := cache.Value("someKey")
    if err == nil {
        fmt.Println("Found value in cache:", res.Data())
    } else {
        fmt.Println("Error retrieving value from cache:", err)
    }

    // Deleting the item will execute the AboutToDeleteItem callback.
    cache.Delete("someKey")

    // Caching a new item that expires in 3 seconds
    res = cache.Add("anotherKey", 3*time.Second, "This is another test")

    // This callback will be triggered when the item is about to expire
    res.SetAboutToExpireCallback(func(key interface{}) {
        fmt.Println("About to expire:", key.(string))
    })

    time.Sleep(5 * time.Second)
}

这个示例和上面的实例比较相似,只不过多设置了一些回调函数。

2.3 dataloader回调的使用

之前介绍的回调函数都是在添加或删除缓存表项时候触发,而这个dataloader回调则是在调用Value时触发。即如果我们去查找某个key的缓存,如果找不到且我们设置了dataloader回调,就会执行该回调函数。这个功能还是挺实用的,举个例子比如我们缓存了数据库中的一些用户信息,如果我们可以设置dataloader回调,如果从缓存里面查找某个用户信息时没有找到,就从数据库中读取该用户信息并加到缓存里面,这个动作就可以加在dataloader回调里面。

package main

import (
    "fmt"
    "github.com/muesli/cache2go"
    "strconv"
)

func main() {
    cache := cache2go.Cache("myCache")

    // The data loader gets called automatically whenever something
    // tries to retrieve a non-existing key from the cache.
    cache.SetDataLoader(func(key interface{}, args ...interface{}) *cache2go.CacheItem {
        // Apply some clever loading logic here, e.g. read values for
        // this key from database, network or file.
        val := "This is a test with key " + key.(string)

        // This helper method creates the cached item for us. Yay!
        item := cache2go.NewCacheItem(key, 0, val)
        return item
    })

    // Let's retrieve a few auto-generated items from the cache.
    for i := 0; i < 10; i++ {
        res, err := cache.Value("someKey_" + strconv.Itoa(i))
        if err == nil {
            fmt.Println("Found value in cache:", res.Data())
        } else {
            fmt.Println("Error retrieving value from cache:", err)
        }
    }
}

至此,cache2go就介绍完了。推荐有兴趣的同学读一下其实现代码,个人感觉非常不错。

]]>
0 http://niyanchun.com/cache2go-introduction.html#comments http://niyanchun.com/feed/tag/go/
WebSocket协议及Go中的用法 http://niyanchun.com/websocket-and-golang.html http://niyanchun.com/websocket-and-golang.html Thu, 16 Mar 2017 22:11:00 +0800 NYC 本文主要介绍WebSocket以及Websocket在Golang中的实现和使用。

1. WebSocket是什么?

这里先列两篇文章:

1.1 为什么会出现WebSocket

在介绍WebSocket之前,我们先来了解一下为什么会出现WebSocket。我们知道HTTP协议(底层使用的是TCP协议)是无状态的,即一次Request,一个Response就结束了。实际中的场景就是客户端(比如浏览器)向服务器发送一次请求,然后服务器返回一个响应,一次交互就结束了,底层的TCP连接也会断掉,下次请求时,重新再创建新的连接。而且这种通信是被动式的,就是说服务器端不能主动向客户端发响应,只能是客户端一个Request,服务的一个Response这种模式(当然最新的协议里面,可能可以将多个Request合并一次发给服务端,但模型仍旧是这种模式)。

如果你曾经使用TCP协议写过通信程序,应该非常熟悉那种模式:客户端和服务端(有时都没有清晰的界限)通过三步握手建立连接后,就可以相互随便发送数据,除非网络异常或者主动关闭,否则该TCP连接将一直存在。而WebSocket的出现就是为了在应用层实现类似传输层的TCP协议,当然它底层和HTTP一样使用的是TCP协议。这样我们就明白一些了,WebSocket不像HTTP协议,一次交互后就结束,它建立后会一直存在(除非主动断开或者网络异常等),而且客户端和服务端可以任意向对方发送数据,不再像以前那么“傻”了。也就是说,HTTP协议是一次性的,“单工的”;而WebSocket是真正意义上的长连接,且是全双工的。当然,上述提及的需求HTTP通过poll和轮循等方式也可以实现,但弊端非常多:

  • 服务器端需要在底层为每个HTTP连接维护一个TCP连接,比如一个用于发送消息,一个用于接收消息等。
  • 资源浪费,每次的HTTP请求中都需要携带消息头。
  • 客户端还必须通过一些手段知道哪些响应对应发出去的哪些请求。

好吧,这就是为什么出现了WebSocket。

1.2 WebSocket和HTTP、TCP的关系

从前一节的介绍我们知道WebSocket的出现是为了弥补HTTP协议的一些不足,但二者本质上是两个完全不同的协议。但为什么WebSocket在我们印象中总是和HTTP协议有着千丝万缕的关联呢?这是因为HTTP协议出现的比较早,而且各项功能相对比较完善,而WebSocket与HTTP一样也主要是定位于Web开发,所以就借用了HTTP的一些基础设施。说的再细点就是WebSocket借用了HTTP的握手过程(所以复用了HTTP的端口80和443),待认证完成升级为WebSocket协议后,就和HTTP没有半毛钱关系了。RFC6455中是这样描述的:

Its only relationship to HTTP is that its handshake is interpreted by HTTP servers as an Upgrade request.

但是我们心里应该时刻明白,WebSocket本质上和HTTP是两个完全不同的协议。而且按照RFC标准,以后WebSocket可能会实现简单点的完全属于自己的握手过程,这样就真的和HTTP没有什么关系了。

WebSocket和TCP协议的关系很简单:WebSocket底层使用的是TCP协议。

1.3 WebSocket协议介绍

虽然WebSocket是应用层的“TCP”协议(我自己的看法),但相比于TCP协议还是简单了很多。协议包含两部分:

  • 握手过程(handshake)
  • 数据传输(date transfer)

握手过程中客户端的请求格式一般为(摘自RFC6455):

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

服务器端的响应格式一般为:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

下面是我用wireshark抓的一个WebSocket握手过程的包(也可以使用浏览器的开发者工具查看请求和响应的Header...作为C开发者表示还没有习惯和熟练掌握Web的调试开发orz...):

客户端请求:

websocket-client-req.png

服务器端响应:

websocket-server-resp.png

因为头部字段的顺序是没有意义的,而且有些字段是可选的,所以实际的输出和RFC文档有些差异。至于每个字段是什么含义,请参阅https://tools.ietf.org/html/rfc6455#section-1.3

握手成功后,连接就会一直存在,双方就可以任意相互发送数据。WebSocket之间的数据一般称为“消息(Message)”,而一个消息往往由很多帧(Frame)组成,有点类似于数据链路层帧的概念。一个消息在传输到底层时,可能被拆分开。每个数据帧都和一种特定的类型相关联,目前定义了6种类型,预留了10种未来使用:

  • %x0 denotes a continuation frame
  • %x1 denotes a text frame
  • %x2 denotes a binary frame
  • %x3-7 are reserved for further non-control frames
  • %x8 denotes a connection close
  • %x9 denotes a ping
  • %xA denotes a pong
  • %xB-F are reserved for further control frames

使用TCP协议做过设计的应该都知道,一般会分数据面和控制面的消息,WebSocket也一样,提供了两种类型的消息:数据帧类型和控制帧类型。数据帧包括上面的%x1和%x2两种,文本类型消息使用UTF-8,二进制类型的消息一般由程序自己定义;控制帧类型包括上面的%x8、%x9、%xA三种,Close消息很明确,表名要关闭连接,而Ping和Pong则没有明确定定义,一般用于心跳消息,而且Pong一般是讲Ping的消息原封不动的发送回去。

WebSocket协议相对于TCP协议虽然简单,但也有很多细节,不过都不是很复杂,本文就不深入介绍了,而且我觉得协议这种东西除非要经常使用,否则可以在具体用的时候再去查细节,不然时间久了也会忘记,毕竟已经过了实战的时候不是像闭卷考试一样需要死记硬背,只要掌握获取知识方法即可,好吧,又跑题了,最后附上一个协议细节图(摘自RFC6455):

0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - - +-------------------------------+
|                               |Masking-key, if MASK set to 1  |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - - +
:                     Payload Data continued ...                :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
|                     Payload Data continued ...                |
+---------------------------------------------------------------+

2. Go中的WebSocket

上面我们了解了WebSocket协议后,我们来看一下Go中对于WebSocket的实现。Go官方的标准包里面提供了一个WebSocket的包 golang.org/x/net/websocket,但也说的很明确,这个包里面并没有实现WebSocket协议规定的一些特性,而推荐使用github.com/gorilla/websocket这个包。所以实际中,我们如果使用到了Go的WebSocket也一般使用后者,本文也介绍的是后者。这个包的内容不是很多,而且README里面对于这个包的使用也说的比较清楚和详细,所以这里就不再做翻译工作了,这里用一个例子来作为介绍。推荐使用gowalker去查看Go文档,如果使用Mac的话,推荐使用Dash,比如该包的文档可去https://gowalker.org/golang.org/x/net/websocket查看。

我们创建一个Go工程,比如go-websocket,里面包含一个html目录和一个main.go文件,html目录下有一个index.html,代码分别如下:

html/index.html:

<!DOCTYPE HTML>
<html>

<head>
    <script type="text/javascript">
        function myWebsocketStart() {
            var ws = new WebSocket("ws://127.0.0.1:3000/websocket");

            ws.onopen = function() {
                // Web Socket is connected, send data using send()
                ws.send("ping")
                var myTextArea = document.getElementById("textarea1")
                myTextArea.value = myTextArea.value + "\n" + "First message sent";
            };

            ws.onmessage = function(evt) {
                var myTextArea = document.getElementById("textarea1");
                myTextArea.value = myTextArea.value + "\n" + evt.data;
                if (evt.data == "pong") {
                    setTimeout(function() {
                        ws.send("ping");
                    }, 2000);
                }
            };

            ws.onclose = function() {
                var myTextArea = document.getElementById("textarea1");
                myTextArea.value = myTextArea.value + "\n" + "Connection closed";
            };
        }
    </script>
</head>

<body>
    <button onclick="javascript:myWebsocketStart()">Start websocket</button>
    <textarea id="textarea1">MyTextArea</textarea>
</body>

</html>

main.go

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "os"

    "time"

    "github.com/gorilla/websocket"
)

type Person struct {
    Name string
    Age  int
}

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
}

func main() {
    indexFile, err := os.Open("html/index.html")
    checkErr(err)

    index, err := ioutil.ReadAll(indexFile)
    checkErr(err)

    http.HandleFunc("/websocket", func(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            fmt.Println(err)
            return
        }

        for {
            msgType, msg, err := conn.ReadMessage()
            if err != nil {
                fmt.Println(err)
                return
            }
            if string(msg) == "ping" {
                fmt.Println("ping")
                time.Sleep(time.Second * 2)
                err = conn.WriteMessage(msgType, []byte("pong"))
                if err != nil {
                    fmt.Println(err)
                    return
                }
            } else {
                conn.Close()
                fmt.Println(string(msg))
                return
            }
        }

    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, string(index))
    })

    http.ListenAndServe(":3000", nil)
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

运行程序,打开浏览器就可以看到效果了,我们着重讲一下这个程序以及涉及到的知识点。在我的上一篇文章《Go网络编程——Socket》中提到,Go的socket编程中核心结构是Conn,而WebSocket也一样有一个Conn结构,代表一个WebSocket连接,而通过websocket.Upgrader可以得到这样一个结构体的指针。后面就是调用一些方法进行数据的收发了。最后需要强调一个地方就是Upgrader这个类型:

type Upgrader struct {
    // HandshakeTimeout specifies the duration for the handshake to complete.
    HandshakeTimeout time.Duration

    // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
    // size is zero, then buffers allocated by the HTTP server are used. The
    // I/O buffer sizes do not limit the size of the messages that can be sent
    // or received.
    ReadBufferSize, WriteBufferSize int

    // Subprotocols specifies the server's supported protocols in order of
    // preference. If this field is set, then the Upgrade method negotiates a
    // subprotocol by selecting the first match in this list with a protocol
    // requested by the client.
    Subprotocols []string

    // Error specifies the function for generating HTTP error responses. If Error
    // is nil, then http.Error is used to generate the HTTP response.
    Error func(w http.ResponseWriter, r *http.Request, status int, reason error)

    // CheckOrigin returns true if the request Origin header is acceptable. If
    // CheckOrigin is nil, the host in the Origin header must not be set or
    // must match the host of the request.
    CheckOrigin func(r *http.Request) bool

    // EnableCompression specify if the server should attempt to negotiate per
    // message compression (RFC 7692). Setting this value to true does not
    // guarantee that compression will be supported. Currently only "no context
    // takeover" modes are supported.
    EnableCompression bool
}

每个成员都有比较详细的说明,这里我们关注一下CheckOrigin这个函数。WebSocket里面对于安全检测的一个非常关键点就是Origin检测,而这个CheckOrigin函数就是做这个事情的,这里提供这个函数就是为了将这个权利交给应用程序自己去实现,默认是nil,表示请求的Origin域必须和header里面的host一致,否则就会握手失败。我们知道HTTP请求的Origin是不可以自己设置和更改的,但是有些客户端是可以随意设置的,这样就没法保证安全,这也是为什么现在WebSocket还只用HTTP去实现的一个原因。我们可以做一个实验,就是将上面的index.html代码中的ws://127.0.0.1:3000/websocket改为ws://localhost:3000/websocket,然后在网页中打开的时候使用http://127.0.0.1:3000/打开,然后我们会发现程序会失败,报“origin not allowed”,这是因为Origin是localhost,而host却是127.0.0.1,二者不一致,握手失败了。我们可以像上面那样去规避这个问题,也可以将原来upgrader的定义改为:

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        return true
    },
}

即强制返回true,表示Origin是可以接受的。

至此,本文就结束了,更多关于WebSocket的细节可以去查看RFC 6455文档,更多Go WebSocket的实现可以查看godoc。

]]>
3 http://niyanchun.com/websocket-and-golang.html#comments http://niyanchun.com/feed/tag/go/
Go网络编程——Socket http://niyanchun.com/go-network-programming-socket.html http://niyanchun.com/go-network-programming-socket.html Tue, 07 Mar 2017 16:15:00 +0800 NYC 以前主要使用的是Linux C网络编程,现在学习了Golang,就总结一下Go中的socket编程,本文基于Go 1.7.4,和socket相关的代码在go/src/net/net.go文件中。

网络协议模型回顾

我们先来简单回顾一下网络写协模型。说到网络协议一般就两个模型:概念意义上的OSI(Open Systems Interconnect)的七层协议和实际中使用的TCP/IP四层协议。

OSI七层网络模型

iso.gif

从第一层到第七层分别是:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层

TCP/IP四层网络模型:

tcp_stack.gif

从第一层到第四层分别是:网络接口层、网间层、传输层、应用层

最后用一张图来结束回顾:

network-protocol.gif

完整版可下载:http://www.colasoft.com.cn/download/protocols_map.php

Go socket编程

首先需要明确一点,这里介绍socket编程的主要是面向三层和四层的协议,再具体点就是主要是针对IP协议、TCP、UDP的,像HTTP这种基于TCP的七层协议不在讨论范围内。

通用socket编程API

Go中socket编程的API都在net包中。其实和C中类似,Go提供了Dial函数来连接服务器,使用Listen监听,Accept接受连接,所以如果写过其他语言(尤其是POSIX标准的)的网络编程的话,其实Go的网络编程也主要就是熟悉一下API而已,所以我们就直入主题了。

Go socket编程中最常用的类型Conn

type Conn interface {
        // Read reads data from the connection.
        // Read can be made to time out and return an Error with Timeout() == true
        // after a fixed time limit; see SetDeadline and SetReadDeadline.
        Read(b []byte) (n int, err error)

        // Write writes data to the connection.
        // Write can be made to time out and return an Error with Timeout() == true
        // after a fixed time limit; see SetDeadline and SetWriteDeadline.
        Write(b []byte) (n int, err error)

        // Close closes the connection.
        // Any blocked Read or Write operations will be unblocked and return errors.
        Close() error

        // LocalAddr returns the local network address.
        LocalAddr() Addr

        // RemoteAddr returns the remote network address.
        RemoteAddr() Addr

        // SetDeadline sets the read and write deadlines associated
        // with the connection. It is equivalent to calling both
        // SetReadDeadline and SetWriteDeadline.
        //
        // A deadline is an absolute time after which I/O operations
        // fail with a timeout (see type Error) instead of
        // blocking. The deadline applies to all future and pending
        // I/O, not just the immediately following call to Read or
        // Write. After a deadline has been exceeded, the connection
        // can be refreshed by setting a deadline in the future.
        //
        // An idle timeout can be implemented by repeatedly extending
        // the deadline after successful Read or Write calls.
        //
        // A zero value for t means I/O operations will not time out.
        SetDeadline(t time.Time) error

        // SetReadDeadline sets the deadline for future Read calls
        // and any currently-blocked Read call.
        // A zero value for t means Read will not time out.
        SetReadDeadline(t time.Time) error

        // SetWriteDeadline sets the deadline for future Write calls
        // and any currently-blocked Write call.
        // Even if write times out, it may return n > 0, indicating that
        // some of the data was successfully written.
        // A zero value for t means Write will not time out.
        SetWriteDeadline(t time.Time) error
}

Conn是一个接口,在Go中代表一个普通的面向流的网络连接。该接口定义了8个方法,基本上都是最基础的网络操作函数,后面我们会看到TCP和UDP分别实现了该接口,并且加入了一些自己的特性。需要注意的是,不同的goroutine是可以并发的调用同一个Conn的方法的。

再看Dial函数:

func Dial(network, address string) (Conn, error)

如前面所述,该函数用于连接服务器。

  • network:用于指定网络类型,目前支持的值有:tcptcp4(IPv4-only)、tcp6(IPv6-only)、udpudp4(IPv4-only)、udp6(IPv6-only)、ipip4(IPv4-only)、ip6(IPv6-only)、unixunixgramunixpacket
  • address:指定要连接的地址,对于TCP和UDP来说,地址格式为host:port。对于IPv6因为地址中已经有冒号了,所以需要用中括号将IP地址括起来,比如[::1]:80。如果省略掉host的话,比如:80,就认为是本地系统。

一些例子:

Dial("tcp", "192.0.2.1:80")
Dial("tcp", "golang.org:http")
Dial("tcp", "[2001:db8::1]:http")
Dial("tcp", "[fe80::1%lo0]:80")
Dial("tcp", ":80")

如果是IP网络的话,network必须是ipip4ip6,且必须在后面加上冒号说明协议号或者名字,比如:

Dial("ip4:1", "192.0.2.1")
Dial("ip6:ipv6-icmp", "2001:db8::1")

如果address对应多个地址(比如可能是一个域名对用多个IP),那么Dial会挨个尝试直到成功连上某一个。

除了Dial,还有一个DialTimeout多提供了一个超时的功能:

func DialTimeout(network, address string, timeout time.Duration) (Conn, error)

OK,了解了ConnDial我们就可以写一个小的程序来模拟HTTP中的HEAD方法了。

Example 1

main.go:

package main

import (
    "fmt"
    "io/ioutil"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "time-track.cn:80")
    checkErr(err)

    _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
    checkErr(err)

    result, err := ioutil.ReadAll(conn)
    checkErr(err)

    fmt.Println(string(result))
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

运行go run main.go

HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 1452
Content-Type: text/html
Server: Microsoft-IIS/8.5
Set-Cookie: CookieZJWFANGDAOLIAN=113.200.50.42#2017-03-07-14#111.67.196.107; expires=07-03-2017 23:59:59; path=/
Set-Cookie: ASPSESSIONIDQCADSRQC=GAOJKJCDPPGPDFNIABBDGCNI; path=/
X-Powered-By: ASP.NET

接下来我们看ListenAccept

type Listener interface {
        // Accept waits for and returns the next connection to the listener.
        Accept() (Conn, error)

        // Close closes the listener.
        // Any blocked Accept operations will be unblocked and return errors.
        Close() error

        // Addr returns the listener's network address.
        Addr() Addr
}

func Listen(net, laddr string) (Listener, error)

Listener接口定义了普通的面向流的监听器,不同的goroutine可以并发的调用同一个监听器的方法。

Listen函数创建一个监听,

  • net:网络类型必须是面向流的网络,目前可选值为tcptcp4tcp6unixunixpacket
  • laddr:指定监听本地哪些网络接口,对于TCP和UDP,格式为host:port。如果省略host,表示监听所有本地地址。

现在我们来写一个简单的echo服务器程序:

Example 2:

package main

import (
    "io"
    "log"
    "net"
)

func main() {
    l, err := net.Listen("tcp", ":2000")
    checkErr(err)

    defer l.Close()

    for {
        conn, err := l.Accept()
        if err != nil {
            log.Fatal(err)
            continue
        }

        go func(c net.Conn) {
            io.Copy(c, c)
            c.Close()
        }(conn)
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

这里我们对于每个客户端的请求,都分配一个goroutine去处理,这样服务端就不会阻塞在某一个客户端上了,这和C中的fork机制是一样的。

所以,掌握了ConnDialListenAccept等API,我们就可以在简单的Go socket编程了。上面我们也看到,这些API一般都有一个参数用来指定具体的网络类型,比如是IP还是TCP,亦或是UDP、UNIX套接字等,也就是说它将多种不同的协议都整合到了同一些API之中,记忆和使用起来也比较方便,。当然,针对每种具体的协议,也有特定的API,其实质也就是实现了诸如Conn等接口,当然通用的socket API也实现了这些接口(比如conn类型就是Conn接口的通用实现),只不过只是实现了一些比较通用的方法等,但是特定协议的socket API会针对本协议多提供一些方法等。Go推荐使用通用的API进行socket编程,而不是具体某种协议的socket API,除非我们需要使用某些特定的方法。当然,即使要使用也可以使用类型转换等。下面我们分别对各种协议特定的socket API进行简单的介绍,主要目的是熟悉一下类型定义和API。

IP协议的API

Go用IP类型表示一个IP地址(IPv4和IPv6):

type IP []byte

func ParseIP(s string) IP

一般我们通过上面列的ParseIP函数将一个合法的string类型(比如"192.0.2.1""2001:db8::68"等)的IP转换为IP类型的IP。该类型也提供了许多方法用来操作对IP地址进行操作。

Go用IPAddr类型来表示一个IP端:

type IPAddr struct {
        IP   IP
        Zone string // IPv6 scoped addressing zone
}

func ResolveIPAddr(net, addr string) (*IPAddr, error)

func (a *IPAddr) Network() string
func (a *IPAddr) String() string

一般我们使用ResolveIPAddr来获取一个IPAddr类型的指针:

  • net:指明IP网络,可选值为"ip", "ip4", "ip6"。
  • addr:指定网络地址,可以是域名、IP等。

后面我们会看到TCP会有一个ResolveTCPAddr,UDP会有一个ResolveUDPAddr,UNIX套接字会有一个ResolveUnixAddr,其用法和作用其实都是类似的。IPAddr只有两个方法。

IP协议中IPConn实现了Conn接口(同时也实现了PacketConn接口),用来表示一个IP连接:

type IPConn struct {
        // contains filtered or unexported fields
}

func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func ListenIP(netProto string, laddr *IPAddr) (*IPConn, error)

// 方法省略

最后再介绍两个和IP相关的类型IPMaskIPNet,分别代表IP掩码(实质也是IP)和一个IP网络,其实就是我们日常说的子网掩码和子网:

type IPMask []byte

type IPNet struct {
        IP   IP     // network number
        Mask IPMask // network mask
}

总结,其实如前面所说,每种具体的协议其实都是在通用的协议实现的基础上又增加了一些本协议特有的东西,在API层面很多时候就是增加了一些前后缀,然后新增了一些特有的API而已。所以后面的几种协议的介绍会更加精简。最后以一个简单实现的ping作为例子,ping使用的是ICMP协议,协议格式为:

  • 第1个字节是8,表名是echo消息
  • 第2个字节是0
  • 第3、4个字节是整个消息的校验和
  • 第5、6个字节是任意标识符
  • 第7、8个字节是任意的序列号
  • 剩余部分是用户数据。
package main

import (
    "os"
    "fmt"
    "net"
)

func main()  {
    if len(os.Args) != 2 {
        fmt.Println("Usage: ", os.Args[0], "host")
        os.Exit(1)
    }

    addr, err := net.ResolveIPAddr("ip", os.Args[1])
    if err != nil {
        fmt.Println("Resolution error", err.Error())
        os.Exit(1)
    }

    conn, err := net.DialIP("ip4:icmp", addr, addr)
    checkErr(err)

    var msg [512]byte
    msg[0] = 8  // echo
    msg[1] = 0  // code 0
    msg[2] = 0  // checksum
    msg[3] = 0  // checksum
    msg[4] = 0  // identifier[0]
    msg[5] = 13 // identifier[1]
    msg[6] = 0  // sequence[0]
    msg[7] = 37 // sequence[1]
    len := 8

    check := checkSum(msg[0:len])
    msg[2] = byte(check >> 8)
    msg[3] = byte(check & 255)

    _, err = conn.Write(msg[0:len])
    checkErr(err)

    _, err = conn.Read(msg[0:])
    checkErr(err)

    fmt.Println("Got response")
    if msg[5] == 13 {
        fmt.Println("identifier matches")
    }

    if msg[7] == 37 {
        fmt.Println("Sequence matches")
    }

    os.Exit(0)
}

func checkSum(msg []byte) uint16 {
    sum := 0

    // assume even for now
    for n := 1; n < len(msg) - 1; n += 2 {
        sum += int(msg[n])*256 + int(msg[n+1])
    }

    sum = (sum >> 16) + (sum & 0xffff)
    sum += (sum >> 16)
    var answer uint16 = uint16(^sum)
    return answer
}

func checkErr(err error)  {
    if err != nil {
        //fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        //os.Exit(1)
        panic(err)
    }
}

NB:运行这个程序需要root权限。

TCP协议的API

TCPAddr类型表示一个TCP连接端:

type TCPAddr struct {
        IP   IP
        Port int
        Zone string // IPv6 scoped addressing zone
}

// 使用该函数获取一个*TCPAddr
func ResolveTCPAddr(net, addr string) (*TCPAddr, error)

TCPConn类型表示一个TCP连接:

type TCPConn struct {
        // contains filtered or unexported fields
}

func DialTCP(net string, laddr, raddr *TCPAddr) (*TCPConn, error)

TCPListener类型表示一个TCP监听:

type TCPListener struct {
        // contains filtered or unexported fields
}

func ListenTCP(net string, laddr *TCPAddr) (*TCPListener, error)

最后我们实现一个简单的时间服务器作为示例:

package main

import (
    "net"
    "time"
)

func main()  {
    service := ":2000"
    tcpAddr, err := net.ResolveTCPAddr("tcp", service)
    checkErr(err)

    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkErr(err)

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }

        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

UDP协议的API

UDPAddr类型表示一个UDP端:

type UDPAddr struct {
        IP   IP
        Port int
        Zone string // IPv6 scoped addressing zone
}

// 获取一个*UDPAddr
func ResolveUDPAddr(net, addr string) (*UDPAddr, error)

UDPConn类型表示一个UDP连接:

type UDPConn struct {
        // contains filtered or unexported fields
}

func DialUDP(net string, laddr, raddr *UDPAddr) (*UDPConn, error)
func ListenMulticastUDP(network string, ifi *Interface, gaddr *UDPAddr) (*UDPConn, error)
func ListenUDP(net string, laddr *UDPAddr) (*UDPConn, error)

最后我们再用UDP实现一下上一节的时间服务器:

package main

import (
    "net"
    "time"
)

func main() {
    service := ":2000"
    udpAddr, err := net.ResolveUDPAddr("udp4", service)
    checkErr(err)

    conn, err := net.ListenUDP("udp", udpAddr)
    checkErr(err)

    for {
        handleClient(conn)
    }
}

func handleClient(conn *net.UDPConn) {
    var buf [512]byte

    _, addr, err := conn.ReadFromUDP(buf[0:])
    if err != nil {
        return
    }

    daytime := time.Now().String()

    conn.WriteToUDP([]byte(daytime), addr)
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

UNIX套接字的API

UnixAddr类型代表一个UNIX套接字端:

type UnixAddr struct {
        Name string
        Net  string
}

func ResolveUnixAddr(net, addr string) (*UnixAddr, error)

UnixConn类型代表一个UNIX套接字连接:

type UnixConn struct {
        // contains filtered or unexported fields
}

func DialUnix(net string, laddr, raddr *UnixAddr) (*UnixConn, error)
func ListenUnixgram(net string, laddr *UnixAddr) (*UnixConn, error)

UnixListener类型代表一个UNIX监听:

type UnixListener struct {
        // contains filtered or unexported fields
}

func ListenUnix(net string, laddr *UnixAddr) (*UnixListener, error)

最后再用Unix socket实现一个echo servier:

package main

import (
    "net"
    "io"
)

func main()  {
    unixAddr, err := net.ResolveUnixAddr("unix", "/tmp/echo.sock")
    checkErr(err)

    l, err := net.ListenUnix("unix", unixAddr)
    checkErr(err)

    for {
        conn, err := l.Accept()
        if err != nil {
            continue
        }

        go func(c net.Conn) {
            io.Copy(c, c)
            c.Close()
        }(conn)
    }
}

func checkErr(err error) {
    if err != nil {
        panic(err)
    }
}

感觉用Go写网络程序比C简单一些,不需要记很多结构体等。最重要的是跨平台...感觉已经不想再写C了...

参考:

  1. Go官方文档及源码。
  2. Network Programming with Go.
]]>
0 http://niyanchun.com/go-network-programming-socket.html#comments http://niyanchun.com/feed/tag/go/
Golang http server代码原理学习 http://niyanchun.com/go-http-server-learning.html http://niyanchun.com/go-http-server-learning.html Fri, 10 Feb 2017 19:13:00 +0800 NYC
  • 本文基于Go 1.7.1,所有列的Go标准库的代码均来自于go/src/net/http/server.go文件。
  • 代码列的有点多,感觉有点乱,但是感觉代码列不全对于想看代码的人又难受。好吧,其实是写的乱。看起来需要耐心...
  • 拨云见雾

    Go中要实现一个简单的Web server非常的简单:

    package main
    
    import (
        "io"
        "log"
        "net/http"
    )
    
    func main() {
        http.HandleFunc("/", HelloServer)
    
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    func HelloServer(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "hello world!\n")
    }

    上面这个程序运行后,我们在浏览器中访问http://127.0.0.1:8080就会输出"hello world!"。那么第一个问题来了:http.ListenAndServe做了什么,它是怎么和http.HandleFunc关联起来的?OK,我们先来看一下http.ListenAndServe这个函数:

    func ListenAndServe(addr string, handler Handler) error {
        server := &Server{Addr: addr, Handler: handler}
        return server.ListenAndServe()
    }

    这个函数比较简单,它就利用参数构造了一个Server类型的变量server,然后调用了这个类型的成员函数ListenAndServe()Server的结构如下:

    // A Server defines parameters for running an HTTP server.
    // The zero value for Server is a valid configuration.
    type Server struct {
        Addr         string        // TCP address to listen on, ":http" if empty
        Handler      Handler       // handler to invoke, http.DefaultServeMux if nil
        ReadTimeout  time.Duration // maximum duration before timing out read of the request
        WriteTimeout time.Duration // maximum duration before timing out write of the response
        TLSConfig    *tls.Config   // optional TLS config, used by ListenAndServeTLS
    
        // MaxHeaderBytes controls the maximum number of bytes the
        // server will read parsing the request header's keys and
        // values, including the request line. It does not limit the
        // size of the request body.
        // If zero, DefaultMaxHeaderBytes is used.
        MaxHeaderBytes int
    
        // TLSNextProto optionally specifies a function to take over
        // ownership of the provided TLS connection when an NPN/ALPN
        // protocol upgrade has occurred. The map key is the protocol
        // name negotiated. The Handler argument should be used to
        // handle HTTP requests and will initialize the Request's TLS
        // and RemoteAddr if not already set. The connection is
        // automatically closed when the function returns.
        // If TLSNextProto is nil, HTTP/2 support is enabled automatically.
        TLSNextProto map[string]func(*Server, *tls.Conn, Handler)
    
        // ConnState specifies an optional callback function that is
        // called when a client connection changes state. See the
        // ConnState type and associated constants for details.
        ConnState func(net.Conn, ConnState)
    
        // ErrorLog specifies an optional logger for errors accepting
        // connections and unexpected behavior from handlers.
        // If nil, logging goes to os.Stderr via the log package's
        // standard logger.
        ErrorLog *log.Logger
    
        disableKeepAlives int32     // accessed atomically.
        nextProtoOnce     sync.Once // guards setupHTTP2_* init
        nextProtoErr      error     // result of http2.ConfigureServer if used
    }

    注释写的很明确,Server类型定义了一个HTTP服务器的一些参数。另外,而且它还有4个成员函数:

    func (srv *Server) ListenAndServe() error
    func (srv *Server) ListenAndServeTLS(certFile, keyFile string) error
    func (srv *Server) Serve(l net.Listener) error
    func (srv *Server) SetKeepAlivesEnabled(v bool)

    成员函数ListenAndServe的作用就是监听srv.Addr指定的TCP网络,并且调用srv.Serve()函数来处理收到的请求:

    // ListenAndServe listens on the TCP network address srv.Addr and then
    // calls Serve to handle requests on incoming connections.
    // Accepted connections are configured to enable TCP keep-alives.
    // If srv.Addr is blank, ":http" is used.
    // ListenAndServe always returns a non-nil error.
    func (srv *Server) ListenAndServe() error {
        addr := srv.Addr
        if addr == "" {
            addr = ":http"
        }
        ln, err := net.Listen("tcp", addr)
        if err != nil {
            return err
        }
        return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
    }

    再来看这个srv.Serve

    // Serve accepts incoming connections on the Listener l, creating a
    // new service goroutine for each. The service goroutines read requests and
    // then call srv.Handler to reply to them.
    //
    // For HTTP/2 support, srv.TLSConfig should be initialized to the
    // provided listener's TLS Config before calling Serve. If
    // srv.TLSConfig is non-nil and doesn't include the string "h2" in
    // Config.NextProtos, HTTP/2 support is not enabled.
    //
    // Serve always returns a non-nil error.
    func (srv *Server) Serve(l net.Listener) error {
        defer l.Close()
        if fn := testHookServerServe; fn != nil {
            fn(srv, l)
        }
        var tempDelay time.Duration // how long to sleep on accept failure
    
        if err := srv.setupHTTP2_Serve(); err != nil {
            return err
        }
    
        // TODO: allow changing base context? can't imagine concrete
        // use cases yet.
        baseCtx := context.Background()
        ctx := context.WithValue(baseCtx, ServerContextKey, srv)
        ctx = context.WithValue(ctx, LocalAddrContextKey, l.Addr())
        for {
            rw, e := l.Accept()
            if e != nil {
                if ne, ok := e.(net.Error); ok && ne.Temporary() {
                    if tempDelay == 0 {
                        tempDelay = 5 * time.Millisecond
                    } else {
                        tempDelay *= 2
                    }
                    if max := 1 * time.Second; tempDelay > max {
                        tempDelay = max
                    }
                    srv.logf("http: Accept error: %v; retrying in %v", e, tempDelay)
                    time.Sleep(tempDelay)
                    continue
                }
                return e
            }
            tempDelay = 0
            c := srv.newConn(rw)
            c.setState(c.rwc, StateNew) // before Serve can return
            go c.serve(ctx)
        }
    }

    可以看到这里是一个死循环,每收到一个请求,就创建一个goroutine去处理这个请求。这里又出来了一个新的结构conn。代码中srv.newConn(rw)返回的变量c就是这个类型。我们看下这个结构:

    // A conn represents the server side of an HTTP connection.
    type conn struct {
        // server is the server on which the connection arrived.
        // Immutable; never nil.
        server *Server
    
        // rwc is the underlying network connection.
        // This is never wrapped by other types and is the value given out
        // to CloseNotifier callers. It is usually of type *net.TCPConn or
        // *tls.Conn.
        rwc net.Conn
    
        // remoteAddr is rwc.RemoteAddr().String(). It is not populated synchronously
        // inside the Listener's Accept goroutine, as some implementations block.
        // It is populated immediately inside the (*conn).serve goroutine.
        // This is the value of a Handler's (*Request).RemoteAddr.
        remoteAddr string
    
        // tlsState is the TLS connection state when using TLS.
        // nil means not TLS.
        tlsState *tls.ConnectionState
    
        // werr is set to the first write error to rwc.
        // It is set via checkConnErrorWriter{w}, where bufw writes.
        werr error
    
        // r is bufr's read source. It's a wrapper around rwc that provides
        // io.LimitedReader-style limiting (while reading request headers)
        // and functionality to support CloseNotifier. See *connReader docs.
        r *connReader
    
        // bufr reads from r.
        // Users of bufr must hold mu.
        bufr *bufio.Reader
    
        // bufw writes to checkConnErrorWriter{c}, which populates werr on error.
        bufw *bufio.Writer
    
        // lastMethod is the method of the most recent request
        // on this connection, if any.
        lastMethod string
    
        // mu guards hijackedv, use of bufr, (*response).closeNotifyCh.
        mu sync.Mutex
    
        // hijackedv is whether this connection has been hijacked
        // by a Handler with the Hijacker interface.
        // It is guarded by mu.
        hijackedv bool
    }

    如注释所示,这个结构描述/代表了服务端的一个HTTP连接。这个类型也有很多方法,这里我们只介绍上面调用到的方法:func (c *conn) serve(ctx context.Context),因为每个goroutine执行的就是这个方法。这个内容有点多,我们只保留对我们分析有用的部分:

    func (c *conn) serve(ctx context.Context) {
        ...
        
        serverHandler{c.server}.ServeHTTP(w, w.req)
        
        ...
    }

    这里又涉及到了一个serverHandler以及它的一个方法ServeHTTP

    // serverHandler delegates to either the server's Handler or
    // DefaultServeMux and also handles "OPTIONS *" requests.
    type serverHandler struct {
        srv *Server
    }
    
    func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
        handler := sh.srv.Handler
        if handler == nil {
            handler = DefaultServeMux
        }
        if req.RequestURI == "*" && req.Method == "OPTIONS" {
            handler = globalOptionsHandler{}
        }
        handler.ServeHTTP(rw, req)
    }

    serverHandler定义了一个HTTP服务,上面c.serve方法中使用c.server初始化了这个HTTP服务,然后调用了其ServeHTTP方法。而这个ServeHTTP方法里面会去c.server里面找其Handler,如果该Handler不为nil,就调用其ServeHTTP方法;如果为nil,就用一个全局变量DefaultServeMux来初始化这个Handler,再调用其ServeHTTP方法。也就是说,最终都调用了HandlerServeHTTP方法。让我们来看看这个Handler

    type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
    }

    这个Handler竟然是个接口,而且只定义了一个ServeHTTP方法。那我们接下来的任务就是去找看谁实现了这个接口。在这之前,我们先总结一下前面分析的东西。

    1. 我们从http.ListenAndServe开始,先是找到了Server这个类型,它用来描述一个运行HTTP服务的Server。而http.ListenAndServe就是调用了这个它的方法ListenAndServe,这个方法又调用了Serve这个方法。在Serve这个方法中,我们看到对于每个请求,都会创建一个goroutine去执行conn类型的serve方法。
    2. 然后我们又分析了conn类型,它描述了服务端的一个HTTP连接。它的serve方法里面调用了Handler接口的ServeHTTP方法。

    上面的分析基本是根据函数调用来分析的,虽然有点乱,但是还是比较简单的,而且其实主要就涉及到了Serverconn两个类型和一个Handler接口。

    接下来我们就分析Go HTTP中最重要的角色ServeMux

    拨云见日

    上面的分析中我们注意到Server结构中的Handler变量有一个默认值DefaultServeMux。它是一个包级别的全局变量,类型是ServeMux,因为它可以调用ServeHTTP,所以它应该实现了Handler接口。答案是肯定的!

    ServeHTTP是Go中的HTTP请求分发器(HTTP request multiplexer),负责将特定URL来的请求分发给特定的处理函数。匹配的规则我摘抄一些Golang的文档,就不翻译了,基本就是正常思路:

    Patterns name fixed, rooted paths, like "/favicon.ico", or rooted subtrees, like "/images/" (note the trailing slash). Longer patterns take precedence over shorter ones, so that if there are handlers registered for both "/images/" and "/images/thumbnails/", the latter handler will be called for paths beginning "/images/thumbnails/" and the former will receive requests for any other paths in the "/images/" subtree.

    Note that since a pattern ending in a slash names a rooted subtree, the pattern "/" matches all paths not matched by other registered patterns, not just the URL with Path == "/".

    If a subtree has been registered and a request is received naming the subtree root without its trailing slash, ServeMux redirects that request to the subtree root (adding the trailing slash). This behavior can be overridden with a separate registration for the path without the trailing slash. For example, registering "/images/" causes ServeMux to redirect a request for "/images" to "/images/", unless "/images" has been registered separately.

    Patterns may optionally begin with a host name, restricting matches to URLs on that host only. Host-specific patterns take precedence over general patterns, so that a handler might register for the two patterns "/codesearch" and "codesearch.google.com/" without also taking over requests for "http://www.google.com/".

    ServeMux also takes care of sanitizing the URL request path, redirecting any request containing . or .. elements or repeated slashes to an equivalent, cleaner URL.

    然后回到刚开始的那个程序,http.ListenAndServe的第二个参数是nil,根据前面的分析,它就会使用默认ServeMux类型的变量DefaultServeMux作为Handler。然后我们看http.HandleFunc是如何将HelloServer注册给DefaultServeMux的?

    http.HandleFunc函数体如下:

    // HandleFunc registers the handler function for the given pattern
    // in the DefaultServeMux.
    // The documentation for ServeMux explains how patterns are matched.
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        DefaultServeMux.HandleFunc(pattern, handler)
    }
    
    // Handle registers the handler for the given pattern
    // in the DefaultServeMux.
    // The documentation for ServeMux explains how patterns are matched.
    func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }

    可见它还是调用了ServeMuxHandleFunc方法。所以我们还是先来看看这个ServeMux吧:

    type ServeMux struct {
        mu    sync.RWMutex //锁,由于请求涉及到并发处理,因此这里需要一个锁机制
        m     map[string]muxEntry  // 路由规则,一个string对应一个mux实体,这里的string就是注册的路由表达式
        hosts bool // whether any patterns contain hostnames
    }
    
    type muxEntry struct {
        explicit bool // 是否精确匹配
        h        Handler // 这个路由表达式对应哪个handler
        pattern  string //匹配字符串
    }
    
    // NewServeMux allocates and returns a new ServeMux.
    func NewServeMux() *ServeMux { return new(ServeMux) }
    
    // DefaultServeMux is the default ServeMux used by Serve.
    var DefaultServeMux = &defaultServeMux
    
    var defaultServeMux ServeMux
    
    // Handle registers the handler for the given pattern.
    // If a handler already exists for pattern, Handle panics.
    func (mux *ServeMux) Handle(pattern string, handler Handler)
    
    // HandleFunc registers the handler function for the given pattern.
    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))
    
    // ServeHTTP dispatches the request to the handler whose
    // pattern most closely matches the request URL.
    func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)

    上面的代码块中,我列了ServeMux结构的定义以及它的几个重要的方法(有的方法的实现内容后面讲到时再贴)。至此,我们可以看到调用关系了:http.HandleFunc-->func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))-->func (mux *ServeMux) Handle(pattern string, handler Handler)。也就是说最终调用的是ServeMuxHandle方法。有时我们也用http.Handle注册请求处理函数,其内部也调用的是func (mux *ServeMux) Handle(pattern string, handler Handler)

    这里还有个小细节需要注意:在ServeMuxHandle方法的第二个参数是Handler类型,它是个接口。而func (mux *ServeMux) HandleFunc(handler func(ResponseWriter, *Request))方法的第二个参数是func(ResponseWriter, *Request)类型的,对于我们上面的例子就是HelloServer函数,但是这个函数并没有实现Handler接口中定义的ServeHTTP(ResponseWriter, *Request)方法,所以它是不能直接传给Handle方法的的。我们可以看到func (mux *ServeMux) HandleFunc(handler func(ResponseWriter, *Request))函数体是这样的:

    func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
        mux.Handle(pattern, HandlerFunc(handler))
    }

    可以看到,这里用HandlerFunchandler包装了一下,就可以作为Handler接口类型传给Handle方法了。那这里的包装是什么呢?初步看起来好像是函数调用,但其实是一个类型转换。没错,HandlerFunc是一个类型:

    type HandlerFunc func(ResponseWriter, *Request)
    
    // ServeHTTP calls f(w, r).
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
    }

    而且HandlerFunc类型还实现了ServeHTTP方法,也就是说它实现了Handler接口。又因为我们的处理函数的签名与它的一致,所以可以强转。所以说HandlerFunc其实就是一个适配器,它使得的我们可以将普通的函数可以作为HTTP的处理函数,只要这个函数的签名是func(ResponseWriter, *Request)这样子的。这也就是为什么我们注册的HTTP请求处理函数的签名都必须写成这个样子。不得不说,这也是Go中一个非常巧妙的用法。

    OK,现在让我们来看看Handle方法是如何注册处理函数的:

    // Handle registers the handler for the given pattern.
    // If a handler already exists for pattern, Handle panics.
    func (mux *ServeMux) Handle(pattern string, handler Handler) {
        mux.mu.Lock()
        defer mux.mu.Unlock()
    
        if pattern == "" {
            panic("http: invalid pattern " + pattern)
        }
        if handler == nil {
            panic("http: nil handler")
        }
        if mux.m[pattern].explicit {
            panic("http: multiple registrations for " + pattern)
        }
    
        if mux.m == nil {
            mux.m = make(map[string]muxEntry)
        }
        mux.m[pattern] = muxEntry{explicit: true, h: handler, pattern: pattern}
    
        if pattern[0] != '/' {
            mux.hosts = true
        }
    
        // Helpful behavior:
        // If pattern is /tree/, insert an implicit permanent redirect for /tree.
        // It can be overridden by an explicit registration.
        n := len(pattern)
        if n > 0 && pattern[n-1] == '/' && !mux.m[pattern[0:n-1]].explicit {
            // If pattern contains a host name, strip it and use remaining
            // path for redirect.
            path := pattern
            if pattern[0] != '/' {
                // In pattern, at least the last character is a '/', so
                // strings.Index can't be -1.
                path = pattern[strings.Index(pattern, "/"):]
            }
            url := &url.URL{Path: path}
            mux.m[pattern[0:n-1]] = muxEntry{h: RedirectHandler(url.String(), StatusMovedPermanently), pattern: pattern}
        }
    }

    可以看到注册的过程其实就是构造map[string]muxEntry这个map或者往已有的里面添加值,key是url,value是处理函数以及其他一些必要信息。这样,注册过程就算明白了。我们再来看下,不同的请求来了以后,是如何选择到事先注册的处理函数的?

    回想前面介绍的,每个请求来了以后会创建一个goroutine去为这个请求服务,goroutine里面最终执行的是Server结构里面Handler成员(类型是Handler接口类型)的ServeHTTP方法。这里的这个Handler就是DefaultServeMuxServeMux类型),所以也就执行的是ServeMuxServeHTTP方法,我们来看一下:

    // ServeHTTP dispatches the request to the handler whose
    // pattern most closely matches the request URL.
    func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
        if r.RequestURI == "*" {
            if r.ProtoAtLeast(1, 1) {
                w.Header().Set("Connection", "close")
            }
            w.WriteHeader(StatusBadRequest)
            return
        }
        h, _ := mux.Handler(r)
        h.ServeHTTP(w, r)
    }

    这里如果收到的请求是*,则关闭连接并返回StatusBadRequest。否则执行mux.Handler,我们看下这个函数:

    // Handler returns the handler to use for the given request,
    // consulting r.Method, r.Host, and r.URL.Path. It always returns
    // a non-nil handler. If the path is not in its canonical form, the
    // handler will be an internally-generated handler that redirects
    // to the canonical path.
    //
    // Handler also returns the registered pattern that matches the
    // request or, in the case of internally-generated redirects,
    // the pattern that will match after following the redirect.
    //
    // If there is no registered handler that applies to the request,
    // Handler returns a ``page not found'' handler and an empty pattern.
    func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
        if r.Method != "CONNECT" {
            if p := cleanPath(r.URL.Path); p != r.URL.Path {
                _, pattern = mux.handler(r.Host, p)
                url := *r.URL
                url.Path = p
                return RedirectHandler(url.String(), StatusMovedPermanently), pattern
            }
        }
    
        return mux.handler(r.Host, r.URL.Path)
    }
    
    // handler is the main implementation of Handler.
    // The path is known to be in canonical form, except for CONNECT methods.
    func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
        mux.mu.RLock()
        defer mux.mu.RUnlock()
    
        // Host-specific pattern takes precedence over generic ones
        if mux.hosts {
            h, pattern = mux.match(host + path)
        }
        if h == nil {
            h, pattern = mux.match(path)
        }
        if h == nil {
            h, pattern = NotFoundHandler(), ""
        }
        return
    }

    可以看到,函数的核心功能就是根据请求的url去之前注册时构造的map里面查找对应的请求处理函数,并返回这个而处理函数。得到这个处理函数后,就接着上面的执行h.ServeHTTP(w, r)。我们注册时将我们自定义的请求处理函数强制转换为了HandlerFunc类型,所以从map里面取出来的还是这个类型,所以这里调用的就是这个类型的ServeHTTP方法:

    // ServeHTTP calls f(w, r).
    func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
    }

    可以看到,其实就是执行我们注册的自定义的请求处理函数。

    到这里就解析完了,但是感觉好乱有木有,好吧,这里我借用以下astaxie在《build-web-application-with-golang》一书中对这个流程的总结吧。对于刚开始的那段代码,整个执行的流程是这样的:

    • 首先调用http.HandleFunc,然后内部按顺序做了以下事情:

      1. 调用了DefaultServeMuxHandleFunc方法
      2. 调用了DefaultServeMuxHandle方法
      3. DefaultServeMuxmap[string]muxEntry中增加对应的handler和路由规则
    • 其次调用http.ListenAndServe(":8080", nil),依次做了以下事情

      1. 实例化Server
      2. 调用ServerListenAndServe方法
      3. 调用net.Listen("tcp", addr)监听端口
      4. 启动一个for循环,在循环体中Accept请求
      5. 对每个请求实例化一个Conn,并且开启一个goroutine为这个请求进行服务go c.serve(ctx)
      6. 读取每个请求的内容w, err := c.readRequest()
      7. 判断handler是否为空,如果没有设置handler(这个例子就没有设置handler),handler就设置为DefaultServeMux
      8. 调用handlerServeHTTP
      9. 在这个例子中,下面就进入到DefaultServeMux.ServeHTTP
      10. 根据request选择handler,并且进入到这个handler的ServeHTTP
      11. 选择handler:

        • 判断是否有路由能满足这个request(循环遍历ServerMuxmuxEntry
        • 如果有路由满足,调用这个路由handlerServeHTTP
        • 如果没有路由满足,调用NotFoundHandlerServeHTTP

    番外篇

    从上面的分析可以看到,之所以我们能够用Go非常容易的写一个简单的Web Server程序是因为Go不光提供了机制和接口,还为我们实现了一个版本。比如Go实现了一个ServeMux,并内置了一个全局变量DefaultServeMux,还实现了一系列诸如对于HandleFunc之类的函数和方法,使得我们可以非常容易的去注册请求处理函数,去分发请求。

    当然,我们可以不使用Go内部实现的ServeMux,而使用我们自己的。一方面是更加有灵活性(当然需要我们自己做更多的编码工作),另一方面有些人也认为标准库中内置一个全局的变量不是一个好的设计与实践。比如对于刚开始的程序,我们定义自己的"ServeMux"来实现:

    package main
    
    import (
        "io"
        "log"
        "net/http"
    )
    
    type MyMux struct{}
    
    func (m *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/" {
            HelloServer(w, r)
            return
        }
    
        http.NotFound(w, r)
        return
    }
    
    func main() {
        mux := &MyMux{}
    
        log.Fatal(http.ListenAndServe(":8080", mux))
    }
    
    func HelloServer(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "hello world!\n")
    }

    可以看到,实现自己的"ServeMux"其实就是实现Handler接口。当然这里只是一个示例,实际中,如何进行路由分发才是大的工作量。所以我觉得内部实现的那个挺好,至少可以减少开发者很多工作量...

    除了这个ServeMux可以自定义外,如果我们想对Server进行更多更精细的控制,也可以自定义Server

    s := &http.Server{
        Addr:           ":8080",
        Handler:        myHandler,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    log.Fatal(s.ListenAndServe())

    参考:

    ]]>
    5 http://niyanchun.com/go-http-server-learning.html#comments http://niyanchun.com/feed/tag/go/
    Go并发模式——Context http://niyanchun.com/go-concurrency-patterns.html http://niyanchun.com/go-concurrency-patterns.html Fri, 27 Jan 2017 15:45:00 +0800 NYC Context介绍

    在Go服务器程序中,每个请求都会有一个goroutine去处理。然而,处理程序往往还需要创建额外的goroutine去访问后端资源,比如数据库、RPC服务等。由于这些goroutine都是在处理同一个请求,所以它们往往需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等。而且如果请求超时或者被取消后,所有的goroutine都应该马上退出并且释放相关的资源。Go提供了一个Context包专门用来解决这个问题,本文我们来介绍Context包的基本使用方法。

    接口

    了解PostgreSQL源码的应该知道,PG里面也有一个Context,专门用来管理内存等资源,Go的这个Context的定位与之类似。我们看下Go的Context的定义:

    type Context interface {
            // Deadline returns the time when work done on behalf of this context
            // should be canceled. Deadline returns ok==false when no deadline is
            // set. Successive calls to Deadline return the same results.
            Deadline() (deadline time.Time, ok bool)
    
            // Done returns a channel that's closed when work done on behalf of this
            // context should be canceled. Done may return nil if this context can
            // never be canceled. Successive calls to Done return the same value.
            //
            // WithCancel arranges for Done to be closed when cancel is called;
            // WithDeadline arranges for Done to be closed when the deadline
            // expires; WithTimeout arranges for Done to be closed when the timeout
            // elapses.
            //
            // Done is provided for use in select statements:
            //
            //  // Stream generates values with DoSomething and sends them to out
            //  // until DoSomething returns an error or ctx.Done is closed.
            //  func Stream(ctx context.Context, out chan<- Value) error {
            //      for {
            //          v, err := DoSomething(ctx)
            //          if err != nil {
            //              return err
            //          }
            //          select {
            //          case <-ctx.Done():
            //              return ctx.Err()
            //          case out <- v:
            //          }
            //      }
            //  }
            //
            // See https://blog.golang.org/pipelines for more examples of how to use
            // a Done channel for cancelation.
            Done() <-chan struct{}
    
            // Err returns a non-nil error value after Done is closed. Err returns
            // Canceled if the context was canceled or DeadlineExceeded if the
            // context's deadline passed. No other values for Err are defined.
            // After Done is closed, successive calls to Err return the same value.
            Err() error
    
            // Value returns the value associated with this context for key, or nil
            // if no value is associated with key. Successive calls to Value with
            // the same key returns the same result.
            //
            // Use context values only for request-scoped data that transits
            // processes and API boundaries, not for passing optional parameters to
            // functions.
            //
            // A key identifies a specific value in a Context. Functions that wish
            // to store values in Context typically allocate a key in a global
            // variable then use that key as the argument to context.WithValue and
            // Context.Value. A key can be any type that supports equality;
            // packages should define keys as an unexported type to avoid
            // collisions.
            //
            // Packages that define a Context key should provide type-safe accessors
            // for the values stored using that key:
            //
            //     // Package user defines a User type that's stored in Contexts.
            //     package user
            //
            //     import "context"
            //
            //     // User is the type of value stored in the Contexts.
            //     type User struct {...}
            //
            //     // key is an unexported type for keys defined in this package.
            //     // This prevents collisions with keys defined in other packages.
            //     type key int
            //
            //     // userKey is the key for user.User values in Contexts. It is
            //     // unexported; clients use user.NewContext and user.FromContext
            //     // instead of using this key directly.
            //     var userKey key = 0
            //
            //     // NewContext returns a new Context that carries value u.
            //     func NewContext(ctx context.Context, u *User) context.Context {
            //         return context.WithValue(ctx, userKey, u)
            //     }
            //
            //     // FromContext returns the User value stored in ctx, if any.
            //     func FromContext(ctx context.Context) (*User, bool) {
            //         u, ok := ctx.Value(userKey).(*User)
            //         return u, ok
            //     }
            Value(key interface{}) interface{}
    }

    Context接口只定义了四个方法,而且注释部分都有非常详细的说明,这里我们再简单补充介绍一下。

    1. Deadline方法。该方法返回工作结束的时间,代表这个Context可以被删除掉了。如果没有设置结束时间的话,ok的返回值为false。多次调用返回结果相同。
    2. Done方法。该方法返回一个接收channel,这个channel作为当前Context中正在运行的函数的删除信号,表示该函数应该上停止当前的工作并放回。并且Err方法返回一个错误值。
    3. Err方法。当Done方法关闭以后,Err方法返回非nil的错误值,目前该方法之定义了两个返回值:CanceledDeadlineExceeded

      // Canceled is the error returned by Context.Err when the context is canceled.
      var Canceled = errors.New("context canceled")
      
      // DeadlineExceeded is the error returned by Context.Err when the context's deadline passes.
      var DeadlineExceeded error = deadlineExceededError{}
    4. Value方法。该方法返回Context中key对应的值,如果key不存在,返回nil。多次调用返回值相同。这个主要是为了在Context中传递同一请求内可见的数据。当然这些数据必须是并发安全的。

    Context主要是为了解决Go中的并发,所以Context是可以被多个goroutine并发调用的。

    派生(Derived)Context

    Context提供了派生机制,也就是我们可以从一个context再派生出子context,这些context组成一个树。一个context删除后,所有派生自它的context也会被删除。

    Context提供了Background方法用来生成一个根context,它是不可以删除的。一般在主函数中使用:

    //Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.
    
    func Background() Context

    另外提供了三个函数WithCancelWithDeadlineWithTimeout ,用于派生子context,我们可以根据自己的需求选择合适的函数:

    // WithCancel returns a copy of parent with a new Done channel. The returned context's Done channel is closed when the returned cancel function is called or when the parent context's Done channel is closed, whichever happens first.
    func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
    
    // WithDeadline returns a copy of the parent context with the deadline adjusted to be no later than d. If the parent's deadline is already earlier than d, WithDeadline(parent, d) is semantically equivalent to parent. The returned context's Done channel is closed when the deadline expires, when the returned cancel function is called, or when the parent context's Done channel is closed, whichever happens first.
    func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
    
    // WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
    func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
    
    // A CancelFunc tells an operation to abandon its work. A CancelFunc does not wait for the work to stop. After the first call, subsequent calls to a CancelFunc do nothing.
    type CancelFunc func()

    删除context后,所有与该context关联的资源都会马上释放。所以我们一般在请求处理结束后,就应该马上删除该请求的context。

    另外,还有一个函数WithValue,通过该函数,我们可以向Context中增加同一请求内可见的变量。虽然key的类型是interface{},但有一个限制就是提供的key必须是可比较的:

    //WithValue returns a copy of parent in which the value associated with key is val.
    //Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
    //The provided key must be comparable.
    
    func WithValue(parent Context, key, val interface{}) Context

    实例

    这个例子来源于Golang官方博客,实现的功能是通过Google Web Search API搜索并返回结果。但是Google Web Search API现在已经废弃了,所以程序无法获取到结果。不过我们看一下代码结构和Context的用法还是可以的。这个例子由三个包组成:

    • server:提供main函数和/search的处理程序
    • userip:从用户请求中获取用户IP并将其与context关联起来
    • google:提供Search函数并向Google发送查询请求。

    完整程序如下:

    server.go

    // The server program issues Google search requests and demonstrates the use of
    // the go.net Context API. It serves on port 8080.
    //
    // The /search endpoint accepts these query params:
    //   q=the Google search query
    //   timeout=a timeout for the request, in time.Duration format
    //
    // For example, http://localhost:8080/search?q=golang&timeout=1s serves the
    // first few Google search results for "golang" or a "deadline exceeded" error
    // if the timeout expires.
    package main
    
    import (
        "html/template"
        "log"
        "net/http"
        "time"
    
        "golang.org/x/blog/content/context/google"
        "golang.org/x/blog/content/context/userip"
        "golang.org/x/net/context"
    )
    
    func main() {
        http.HandleFunc("/search", handleSearch)
        log.Fatal(http.ListenAndServe(":8080", nil))
    }
    
    // handleSearch handles URLs like /search?q=golang&timeout=1s by forwarding the
    // query to google.Search. If the query param includes timeout, the search is
    // canceled after that duration elapses.
    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.
    
        // 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)
    
        // Run the Google search and print the results.
        start := time.Now()
        results, err := google.Search(ctx, query)
        elapsed := time.Since(start)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        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
        }
    }
    
    var resultsTemplate = template.Must(template.New("results").Parse(`
    <html>
    <head/>
    <body>
      <ol>
      {{range .Results}}
        <li>{{.Title}} - <a href="{{.URL}}">{{.URL}}</a></li>
      {{end}}
      </ol>
      <p>{{len .Results}} results in {{.Elapsed}}; timeout {{.Timeout}}</p>
    </body>
    </html>
    `))

    userip.go

    // Package userip provides functions for extracting a user IP address from a
    // request and associating it with a Context.
    //
    // This package is an example to accompany https://blog.golang.org/context.
    // It is not intended for use by others.
    package userip
    
    import (
        "fmt"
        "net"
        "net/http"
    
        "golang.org/x/net/context"
    )
    
    // FromRequest extracts the user IP address from req, if present.
    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)
        }
    
        userIP := net.ParseIP(ip)
        if userIP == nil {
            return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
        }
        return userIP, nil
    }
    
    // The key type is unexported to prevent collisions with context keys defined in
    // other packages.
    type key int
    
    // userIPkey is the context key for the user IP address.  Its value of zero is
    // arbitrary.  If this package defined other context keys, they would have
    // different integer values.
    const userIPKey key = 0
    
    // NewContext returns a new Context carrying userIP.
    func NewContext(ctx context.Context, userIP net.IP) context.Context {
        return context.WithValue(ctx, userIPKey, userIP)
    }
    
    // FromContext extracts the user IP address from ctx, if present.
    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.go

    // Package google provides a function to do Google searches using the Google Web
    // Search API. See https://developers.google.com/web-search/docs/
    //
    // This package is an example to accompany https://blog.golang.org/context.
    // It is not intended for use by others.
    //
    // Google has since disabled its search API,
    // and so this package is no longer useful.
    package google
    
    import (
        "encoding/json"
        "net/http"
    
        "golang.org/x/blog/content/context/userip"
        "golang.org/x/net/context"
    )
    
    // Results is an ordered list of search results.
    type Results []Result
    
    // A Result contains the title and URL of a search result.
    type Result struct {
        Title, URL string
    }
    
    // Search sends query to Google search and returns the results.
    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()
    
        // Issue the HTTP request and handle the response. The httpDo function
        // cancels the request if ctx.Done is closed.
        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/web-search/docs/#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 issues the HTTP request and calls f with the response. If ctx.Done is
    // closed while the request or f is running, httpDo cancels the request, waits
    // for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
    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
        }
    }

    代码里面都有详细注释,这里就不再赘述了。

    本文主要翻译自:https://blog.golang.org/context

    ]]>
    0 http://niyanchun.com/go-concurrency-patterns.html#comments http://niyanchun.com/feed/tag/go/
    Cobra简介 http://niyanchun.com/cobra-brief-introduction.html http://niyanchun.com/cobra-brief-introduction.html Sun, 15 Jan 2017 14:37:00 +0800 NYC 总览

    Cobra是spf13写的一个编写/生成交互式命令程序的框架,github地址是:https://github.com/spf13/cobra。有很多知名的开源项目都使用了这个框架:

    • Kubernetes
    • Hugo
    • rkt
    • etcd
    • Docker (distribution)
    • OpenShift
    • Delve
    • GopherJS
    • CockroachDB
    • Bleve
    • ProjectAtomic (enterprise)
    • Parse (CLI)
    • GiantSwarm's swarm
    • Nanobox/Nanopack

    了解了这个框架,再去看这些Kubernetes、etcd、Registry等开源项目的代码时,也就大概知道如何去看了,这也是我学习cobra的目的。spf13也写了一个viper框架,主要是用于处理配置文件的,功能非常强大,结合cobra使用时,就更加的niubility了,有兴趣的也可以了解下。cobra的README.md文件非常详细的介绍了该框架的使用方法,本文也基本是精简翻译了这个文档。

    使用cobra我们可以很容器的开发类似git、go这样的交互式命令行工具。它提供了以下特性(这个涉及一些术语,翻译比较别扭,就直接使用原文了):

    • Easy subcommand-based CLIs: app server, app fetch, etc.
    • Fully POSIX-compliant flags (including short & long versions)
    • Nested subcommands
    • Global, local and cascading flags
    • Easy generation of applications & commands with cobra create appname & cobra add cmdname
    • Intelligent suggestions (app srver... did you mean app server?)
    • Automatic help generation for commands and flags
    • Automatic detailed help for app help [command]
    • Automatic help flag recognition of -h, --help, etc.
    • Automatically generated bash autocomplete for your application
    • Automatically generated man pages for your application
    • Command aliases so you can change things without breaking them
    • The flexibilty to define your own help, usage, etc.
    • Optional tight integration with viper for 12-factor apps

    总之,cobra设计的非常强大和人性,不过在具体上手之前,我们先来看一些概念和结构。cobra开发的应用结构由三部分组成:命令(Command)参数(Args)标志(Flag)

    一个命令的结构如下:

    type Command struct {
        Use string       // The one-line usage message.
        Short string    // The short description shown in the 'help' output.
        Long string     // The long message shown in the 'help <this-command>' output.
        Run func(cmd *Command, args []string) // Run runs the command.
    }

    注释里面对于各个字段的说明已经非常清楚了,前三个字段是不同场景下打印的对于命令的说明,由简到详,最后一个是这个命令要执行的动作。

    cobra的标志完全兼容POSIX格式的参数,即有长格式的命令和短格式的命令。而且标志可以只对该命令有效,也可以对该命令的子命令有效。

    cobra按照树状来组织它生成的代码,这个树定义了程序的结构,当一个命令执行的时候,就从该树中找到最终需要执行的命令。

    OK,现在我们对于cobra已经有了一个大概的认识了,下面我们通过具体的例子来看如何使用这个强大的库吧。

    入门

    cobra的安装与使用和其他的go库相同:

    # 安装
    go get -v github.com/spf13/cobra/cobra
    
    # 在代码中引入cobra就可以使用了
    import "github.com/spf13/cobra"

    一般基于cobra的程序的代码结构如下:

    ▾ appName/
        ▾ cmd/
            add.go
            your.go
            commands.go
            here.go
          main.go

    main.go一般内容非常少,它里面一般只是初始化cobra。比如典型的代码如下:

    package main
    
    import (
        "fmt"
        "os"
        "{pathToYourApp}/cmd"
    )
    
    func main() {
        if err := cmd.RootCmd.Execute(); err != nil {
            fmt.Println(err)
            os.Exit(-1)
        }
    }

    cobra提供了一个cobra工具,我们安装完以后,如果没有什么问题,在$GOBIN目录下应该会有一个可执行的cobra文件,我们既可以自己手动写应用,也可以使用该工具来自动生成应用框架以及一些命令支持。如果你使用过beego框架的话,对这个应该不陌生,它就类似于beego的bee命令。下面我们演示使用cobra工具生成应用,当然当你对cobra的机理熟悉了以后,完全可以手写应用框架。

    cobra init [yourApp]

    这个命令可以帮我们生成一个代码框架。在GOPATH/src目录下执行cobra init cobra_exp1便可生成一个机遇cobra的工程:

    ➜ src cobra init cobra_exp1
    Your Cobra application is ready at
    /Users/Allan/workspace/gopath/src/cobra_exp1
    Give it a try by going there and running `go run main.go`
    Add commands to it by running `cobra add [cmdname]`

    生成的代码结构如下:

    ➜  cobra_exp1 ll -R
    .:
    total 16K
    -rw-r--r-- 1 Allan 12K  1 15 13:01 LICENSE
    drwxr-xr-x 3 Allan 102  1 15 13:01 cmd
    -rw-r--r-- 1 Allan 676  1 15 13:01 main.go
    
    ./cmd:
    total 4.0K
    -rw-r--r-- 1 Allan 2.7K  1 15 13:01 root.go

    我们看到生成的代码中包含一个main.go、LICENSE和一个cmd目录,cmd目录里面只有一个root.go文件。需要注意的是,cobra也帮我们生成了LICENSE,这个LICENSE是根据~/.cobra.yaml文件生成的,比如我的文件是:

    ➜  cobra_exp1 cat ~/.cobra.yaml
    author: Allan Ni <allan_ni@163.com>
    license: MIT
    
    # 生成的LICENSE
    ➜  cobra_exp1 cat LICENSE
    The MIT License (MIT)
    
    Copyright © 2017 Allan Ni
    
    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the "Software"), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:
    
    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.
    
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.

    当然,源代码里面也是有生成LICENSE信息的。而且这个版权信息时可以自定义的:

    license:
      header: This file is part of {{ .appName }}.
      text: |
        {{ .copyright }}
        This is my license. There are many like it, but this one is mine.
        My license is my best friend. It is my life. I must master it as I must
        master my life. 

    我们看下cmd/root.go文件(为了节省空间,删掉了版权信息,内容和LICENSE内容是一样的。后续的文件也这样处理):

    package cmd
    
    import (
        "fmt"
        "os"
    
        "github.com/spf13/cobra"
        "github.com/spf13/viper"
    )
    
    var cfgFile string
    
    // RootCmd represents the base command when called without any subcommands
    var RootCmd = &cobra.Command{
        Use:   "cobra_exp1",
        Short: "A brief description of your application",
        Long: `A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.`,
    // Uncomment the following line if your bare application
    // has an action associated with it:
    //    Run: func(cmd *cobra.Command, args []string) { },
    }
    
    // Execute adds all child commands to the root command sets flags appropriately.
    // This is called by main.main(). It only needs to happen once to the rootCmd.
    func Execute() {
        if err := RootCmd.Execute(); err != nil {
            fmt.Println(err)
            os.Exit(-1)
        }
    }
    
    func init() {
        cobra.OnInitialize(initConfig)
    
        // Here you will define your flags and configuration settings.
        // Cobra supports Persistent Flags, which, if defined here,
        // will be global for your application.
    
        RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.cobra_exp1.yaml)")
        // Cobra also supports local flags, which will only run
        // when this action is called directly.
        RootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
    }
    
    // initConfig reads in config file and ENV variables if set.
    func initConfig() {
        if cfgFile != "" { // enable ability to specify config file via flag
            viper.SetConfigFile(cfgFile)
        }
    
        viper.SetConfigName(".cobra_exp1") // name of config file (without extension)
        viper.AddConfigPath("$HOME")  // adding home directory as first search path
        viper.AutomaticEnv()          // read in environment variables that match
    
        // If a config file is found, read it in.
        if err := viper.ReadInConfig(); err == nil {
            fmt.Println("Using config file:", viper.ConfigFileUsed())
        }
    }

    文件内容比较多,这里我们先只关注一下定义RootCmd这个变量的结构体内容以及Execute函数。再看main.go内容:

    package main
    
    import "cobra_exp1/cmd"
    
    func main() {
        cmd.Execute()
    }

    和前面介绍的一样,main中就只是执行了初始化的语句。我们运行一下程序:

    ➜ cobra_exp1 go run main.go
    A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.

    现在我们只有一个空框架,什么子命令和标志也没有。下面我们来添加命令。

    cobra add

    使用cobra add可以增加我们自己的命令,比如我们需要添加如下三条命令:

    • cobra_exp1 serve
    • cobra_exp1 config
    • cobra_exp1 config create

    只需要在工程目录下执行如下三条命令:

    ➜  cobra_exp1 cobra add serve
    Using config file: /Users/Allan/.cobra.yaml
    serve created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/serve.go
    ➜  cobra_exp1 cobra add config
    Using config file: /Users/Allan/.cobra.yaml
    config created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/config.go
    ➜  cobra_exp1 cobra add create -p 'configCmd'
    Using config file: /Users/Allan/.cobra.yaml
    create created at /Users/Allan/workspace/gopath/src/cobra_exp1/cmd/create.go
    
    # 增加完后的文件结构
    ➜  cobra_exp1 ll -R
    .:
    total 9.2M
    -rw-r--r-- 1 Allan 1.1K  1 15 13:04 LICENSE
    drwxr-xr-x 6 Allan  204  1 15 13:32 cmd
    -rwxr-xr-x 1 Allan 9.2M  1 15 13:17 cobra_exp1
    -rw-r--r-- 1 Allan 1.2K  1 15 13:04 main.go
    
    ./cmd:
    total 16K
    -rw-r--r-- 1 Allan 2.2K  1 15 13:32 config.go
    -rw-r--r-- 1 Allan 2.2K  1 15 13:32 create.go
    -rw-r--r-- 1 Allan 3.2K  1 15 13:31 root.go
    -rw-r--r-- 1 Allan 2.2K  1 15 13:32 serve.go

    然后再运行程序:

    ➜  cobra_exp1 go build
    ➜  cobra_exp1 ./cobra_exp1
    A longer description that spans multiple lines and likely contains
    examples and usage of using your application. For example:
    
    Cobra is a CLI library for Go that empowers applications.
    This application is a tool to generate the needed files
    to quickly create a Cobra application.
    
    Usage:
      cobra_exp1 [command]
    
    Available Commands:
      config      A brief description of your command
      serve       A brief description of your command
    
    Flags:
          --config string   config file (default is $HOME/.cobra_exp1.yaml)
    
    Use "cobra_exp1 [command] --help" for more information about a command.
    ➜  cobra_exp1 ./cobra_exp1 serve
    serve called
    ➜  cobra_exp1 ./cobra_exp1 config
    config called
    ➜  cobra_exp1 ./cobra_exp1 config create
    create called

    我们添加的命令已经可以正常使用了,后续我们需要做的就是添加命令的动作了。是不是很强大!OK,我们接着看如何使用标志(Flag)。

    cobra提供了两种flag,一种是全局的,一种是局部的。所谓全局的就是如果A命令定义了一个flag,那A命令下的所有命令都可以使用这个flag。局部的flag当然就是只能被某个特定的命令使用了。想一下我们平时使用的那些命令,是不是很多命令都支持-v这个标志来打印详情?其实就是这里的全局flag的意思。比如对于全局的命令,我们可以直接加到root下面:

    RootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")

    下面我们看个例子:

    package main
    
    import (
        "fmt"
        "strings"
    
        "github.com/spf13/cobra"
    )
    
    func main() {
        var echoTimes int
    
        var cmdPrint = &cobra.Command{
            Use:   "print [string to print]",
            Short: "Print anything to the screen",
            Long: `print is for printing anything back to the screen.
                For many years people have printed back to the screen.
                `,
            Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("Print: " + strings.Join(args, " "))
            },
        }
    
        var cmdEcho = &cobra.Command{
            Use:   "echo [string to echo]",
            Short: "Echo anything to the screen",
            Long: `echo is for echoing anything back.
                Echo works a lot like print, except it has a child command.
                `,
            Run: func(cmd *cobra.Command, args []string) {
                fmt.Println("Print: " + strings.Join(args, " "))
            },
        }
    
        var cmdTimes = &cobra.Command{
            Use:   "times [# times] [string to echo]",
            Short: "Echo anything to the screen more times",
            Long: `echo things multiple times back to the user by providing
                a count and a string.`,
            Run: func(cmd *cobra.Command, args []string) {
                for i := 0; i < echoTimes; i++ {
                    fmt.Println("Echo: " + strings.Join(args, " "))
                }
            },
        }
    
        cmdTimes.Flags().IntVarP(&echoTimes, "times", "t", 1, "times to echo the input")
    
        var rootCmd = &cobra.Command{Use: "app"}
        rootCmd.AddCommand(cmdPrint, cmdEcho)
        cmdEcho.AddCommand(cmdTimes)
    
        rootCmd.Execute()
    }

    这个例子中,我们定义了两个顶级命令echoprint和一个echo的子命令timesechoprint功能相同,但是多了一个子命令times可以控制回显的次数,而且这个times命令不是全局的,而只是注册到echo这个命令下面了。下面我们看一下如何使用:

    # 编译
    ➜  cobra_exp1 go build -o app main.go
    
    # 打印应用帮助信息
    ➜  cobra_exp1 ./app
    Usage:
      app [command]
    
    Available Commands:
      echo        Echo anything to the screen
      print       Print anything to the screen
    
    Use "app [command] --help" for more information about a command.
    
    # 打印print命令帮助信息
    ➜  cobra_exp1 ./app print -h
    print is for printing anything back to the screen.
                For many years people have printed back to the screen.
    
    Usage:
      app print [string to print] [flags]
    
    # 打印echo命令帮助信息
    ➜  cobra_exp1 ./app echo --help
    echo is for echoing anything back.
                Echo works a lot like print, except it has a child command.
    
    Usage:
      app echo [string to echo] [flags]
      app echo [command]
    
    Available Commands:
      times       Echo anything to the screen more times
    
    Use "app echo [command] --help" for more information about a command.
    
    # 打印echo的子命令times的帮助信息
    ➜  cobra_exp1 ./app echo times -h
    echo things multiple times back to the user by providing
                a count and a string.
    
    Usage:
      app echo times [# times] [string to echo] [flags]
    
    Flags:
      -t, --times int   times to echo the input (default 1)
    
    # 使用
    ➜  cobra_exp1 ./app print just a test
    Print: just a test
    ➜  cobra_exp1 ./app echo just a test
    Print: just a test
    ➜  cobra_exp1 ./app echo times just a test -t 5
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test
    ➜  cobra_exp1 ./app echo times just a test --times=5
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test
    ➜  cobra_exp1 ./app echo times just a test --times 5
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test
    Echo: just a test

    可以看到命令使用起来语法也非常的灵活。

    其他特性

    当然cobra还有非常多的其他特性,这里我们只简单说明一下,就不详细介绍了。

    • 自定义帮助。之前我们已经看到了,我们新增的命令都有help功能,这个是cobra内置的,当然我们可以自定义成我们自己想要的样子。
    • 钩子函数。前面我们介绍了命令执行时会去执行命令Run字段定义的回调函数,cobra还提供了四个函数:PersistentPreRunPreRunPostRunPersistentPostRun,可以在执行这个回调函数之前和之后执行。它们的执行顺序依次是:PersistentPreRunPreRunRunPostRunPersistentPostRun。而且对于PersistentPreRunPersistentPostRun,子命令是继承的,也就是说子命令如果没有自定义自己的PersistentPreRunPersistentPostRun,那它就会执行父命令的这两个函数。
    • 可选的错误处理函数。
    • 智能提示。比如下面的:

      ➜  cobra_exp1 ./app eoch
      Error: unknown command "eoch" for "app"
      
      Did you mean this?
       echo
      
      Run 'app --help' for usage.    
    • ...

    本文非常简略的介绍了cobra,主要目的是为了后续阅读Registry等使用cobra框架的开源代码,而非使用cobra去开发。如果你需要使用cobra去开发或者想更深入的了解,可以去仔细阅读一下它的README.md和源代码。

    ]]>
    3 http://niyanchun.com/cobra-brief-introduction.html#comments http://niyanchun.com/feed/tag/go/
    构建最小的Go程序镜像 http://niyanchun.com/build-minimal-go-image.html http://niyanchun.com/build-minimal-go-image.html Mon, 02 Jan 2017 14:22:00 +0800 NYC 我们知道构建一个Docker镜像的时候往往需要引入一些程序依赖的东西,最常见的就是引入一个基础操作系统镜像,但这样往往会使得编译出来的镜像特别大。但是对于go语言,我们可以使用静态编译的方式构建出超小的镜像。有人会问Go本身不就是静态编译吗?请接着往下看。

    示例程序

    package main
    
    import (
        "fmt"
        "io/ioutil"
        "net/http"
        "os"
    )
    
    func main() {
        resp, err := http.Get("http://baidu.com")
        check(err)
        body, err := ioutil.ReadAll(resp.Body)
        check(err)
        fmt.Println(len(body))
    }
    
    func check(err error) {
        if err != nil {
            fmt.Println(err)
            os.Exit(1)
        }
    }

    这个程序很简单,就是访问一个网页,然后打印body的大小。

    构建docker镜像

    使用golang:onbuild镜像

    我们先简单介绍一下golang:onbuild镜像以及如何使用它。这个镜像是golang官方提供的一个用于编译运行go程序的镜像,它包含了很多ONBUILD触发器,可以涵盖大多数Go程序。其实它就是预置了一些自动执行的命令:

    COPY . /go/src/app
    RUN go get -d -v
    RNU go install -v

    它使用起来很简单:创建一个目录,把你的go文件放到该目录。然后增加Dockerfile文件,内容就一行:

    FROM golang:onbuild

    然后执行docker build命令构建镜像。这个golang:onbuild镜像就会自动将这个目录下的所有文件拷贝过去,并编译安装。比如对于本文的例子,我创建了一个app-onbuild目录,里面是Dockerfile和app.go文件:

    ➜  app-onbuild ll
    total 8.0K
    -rw-r--r-- 1 Allan  20 Jan  2 12:21 Dockerfile
    -rw-r--r-- 1 Allan 291 Jan  2 12:20 app.go
    ➜  app-onbuild cat Dockerfile
    FROM golang:onbuild

    然后我执行编译镜像的命令docker build -t app-onbuild .,整个过程如下:

    ➜  app docker build -t app-onbuild .
    Sending build context to Docker daemon 3.072 kB
    Step 1 : FROM golang:onbuild
    onbuild: Pulling from library/golang
    
    75a822cd7888: Already exists
    57de64c72267: Pull complete
    4306be1e8943: Pull complete
    4ba540d49835: Pull complete
    5b23b80eb526: Pull complete
    981c210a3af4: Pull complete
    73f7f7662eed: Pull complete
    520a90f1995e: Pull complete
    Digest: sha256:d3cbc855152e8672412fc32d7f19371816d686b0dfddedb8fce86245910b31ac
    Status: Downloaded newer image for golang:onbuild
    # Executing 3 build triggers...
    Step 1 : COPY . /go/src/app
    Step 1 : RUN go-wrapper download
     ---> Running in 4d183d7e1e8a
    + exec go get -v -d
    Step 1 : RUN go-wrapper install
     ---> Running in 31b6371f1a4f
    + exec go install -v
    app
     ---> 94cc8fb334ea
    Removing intermediate container f13df1977590
    Removing intermediate container 4d183d7e1e8a
    Removing intermediate container 31b6371f1a4f
    Successfully built 94cc8fb334ea

    我们可以看到整个过程如前面所述。编译完以后,golang:onbuild镜像默认还包含CMD ["app"]执行来运行编译出来的镜像。当然如果你的程序有参数,我们可以在启动的时候加命令行参数。

    最后让我们来看看golang:onbuild的Dockerfile吧(这里以目前最新的1.8为例):

    FROM golang:1.8
    RUN mkdir -p /go/src/app
    WORKDIR /go/src/app
    # this will ideally be built by the ONBUILD below ;)
    CMD ["go-wrapper", "run"]
    ONBUILD COPY . /go/src/app
    ONBUILD RUN go-wrapper download
    ONBUILD RUN go-wrapper install

    我们可以看到其实golang:onbuild镜像其实引用的还是golang标准镜像,只不过封装了一些自动执行的动作,使用使用起来更加方便而已。接下里我们看看如何直接基于标准的golang镜像来构建我们自己的镜像。

    使用golang:latest镜像

    相比于使用golang:onbuild的便利性,golang:latest给了我们更多的灵活性。我们以构建app镜像为例:创建app-golang目录,目录内容如下所示:

    ➜  app-golang ll
    total 8.0K
    -rw-r--r-- 1 Allan 101 Jan  2 12:32 Dockerfile
    -rw-r--r-- 1 Allan 291 Jan  2 12:32 app.go
    ➜  app-golang cat Dockerfile
    FROM golang:latest
    
    RUN mkdir /app
    ADD . /app/
    WORKDIR /app
    RUN go build -o main .
    CMD ["/app/main"]

    执行构建命令docker build -t app-golang .

    ➜  app-golang docker build -t app-golang .
    Sending build context to Docker daemon 3.072 kB
    Step 1 : FROM golang:latest
    latest: Pulling from library/golang
    75a822cd7888: Already exists
    57de64c72267: Already exists
    4306be1e8943: Already exists
    4ba540d49835: Already exists
    5b23b80eb526: Already exists
    981c210a3af4: Already exists
    73f7f7662eed: Already exists
    Digest: sha256:5787421a0314390ca8da11b26885502b58837ebdffda0f557521790c13ddb55f
    Status: Downloaded newer image for golang:latest
     ---> 6639f812dbc7
    Step 2 : RUN mkdir /app
     ---> Running in a6f105ecc042
     ---> f73030d40507
    Removing intermediate container a6f105ecc042
    Step 3 : ADD . /app/
     ---> 3fcc194ce29d
    Removing intermediate container 013c2192f90e
    Step 4 : WORKDIR /app
     ---> Running in b8a2ca8d7ae0
     ---> 853dfe15c6cd
    Removing intermediate container b8a2ca8d7ae0
    Step 5 : RUN go build -o main .
     ---> Running in e0de5c273d7b
     ---> 28ef112e8c23
    Removing intermediate container e0de5c273d7b
    Step 6 : CMD /app/main
     ---> Running in 82c67389d9ab
     ---> 139ad10f61dc
    Removing intermediate container 82c67389d9ab
    Successfully built 139ad10f61dc

    其实golang标准镜像还有一个非常方便的用途。比如我们需要开发go应用,但是又不想安装go环境或者还没有安装,那么我们可以直接使用golang标准镜像来在容器里面编译go程序。假设当前目录是我们的工程目录,那么我们可以使用如下命令来编译我们的go工程:

    $ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest go build -v

    这条命令的含义是把当前目录作为一个卷挂载到golang:latest镜像的/usr/src/myapp目录下,并将该目录设置为工作目录,然后在该目录下执行go build -v,这样就会在该目录下编译出一个名为myapp的可执行文件。当然默认编译出的是linux/amd64架构的二进制文件,如果我们需要编译其他系统架构的文件,可以加上相应的参数,比如我们要编译Windows下的32位的二进制文件,可执行如下命令:

    $ docker run --rm -v "$PWD":/usr/src/myapp -w /usr/src/myapp -e GOOS=windows -e GOARCH=386 golang:latest go build -v

    当然,我们也可以shell脚本一次编译出多种OS下的文件:

    $ docker run --rm -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:latest bash
    $ for GOOS in darwin linux windows; do
    > for GOARCH in 386 amd64; do
    > go build -v -o myapp-$GOOS-$GOARCH
    > done
    > done

    OK,言归正传,我们继续来进行本文要讨论的话题。我们看一下上述两种方式编译出来的镜像的大小:

    ➜  app-golang docker images | grep -e golang -e app
    app-golang                                      latest              139ad10f61dc        36 minutes ago      679.3 MB
    app-onbuild                                     latest              94cc8fb334ea        39 minutes ago      679.3 MB
    golang                                          onbuild             a422f764b58c        2 weeks ago         674 MB
    golang                                          latest              6639f812dbc7        2 weeks ago         674 MB

    可以看到,golang-onbuildgolang-latest两个基础镜像大小都为647MB,而我们编译出来的自己的镜像大小为679.3MB,也就是说我们自己的程序其实只有5.3MB。这是为什么呢?因为我们使用的两个基础镜像是通用镜像,他们包含了go依赖的所有东西。比如我们看一下golang-1.8的Dockerfile:

    FROM buildpack-deps:jessie-scm
    # gcc for cgo
    RUN apt-get update && apt-get install -y --no-install-recommends \
            g++ \
            gcc \
            libc6-dev \
            make \
            pkg-config \
        && rm -rf /var/lib/apt/lists/*
    ENV GOLANG_VERSION 1.8beta2
    ENV GOLANG_DOWNLOAD_URL https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz
    ENV GOLANG_DOWNLOAD_SHA256 4cb9bfb0e82d665871b84070929d6eeb4d51af6bedbc8fdd3df5766e937ef84c
    RUN curl -fsSL "$GOLANG_DOWNLOAD_URL" -o golang.tar.gz \
        && echo "$GOLANG_DOWNLOAD_SHA256 golang.tar.gz" | sha256sum -c - \
        && tar -C /usr/local -xzf golang.tar.gz \
        && rm golang.tar.gz
    ENV GOPATH /go
    ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
    RUN mkdir -p "$GOPATH/src" "$GOPATH/bin" && chmod -R 777 "$GOPATH"
    WORKDIR $GOPATH
    COPY go-wrapper /usr/local/bin/

    这样我们就不奇怪了,因为基础镜像里面包含了太多的东西,但其实我们的程序只使用了极少的一部分东西。下面我们就介绍一种方法可以只编译我们程序依赖的东西,这样编译出来的镜像非常的小。

    静态编译

    其实在生产环境中,对于Go应用我们往往是现在本地编译出一个可执行文件,然后将这个文件打进容器里面,而不是在容器里面进行编译,这样可以做到容器里面只有我们需要的东西,而不会引入一些只在编译过程中需要的文件。这种方式一般也有两种操作方法:第一种就是利用go build编译出二进制文件,然后在Dockerfile里面引入一个操作系统镜像作为程序运行环境。这样打出的镜像也比较大,但是却不会有编译过程中依赖的一些文件,所以相比较之前的方式,这种方式打出来的镜像也会小很多。在镜像大小不是特别重要的场景下,我比较喜欢这种方式,因为基础的操作系统往往带了一些基础的命令,如果我们的程序有些异常什么的,我们可以登录到这些容器里面去查看。比如可以使用psnetstat等命令。但如果没有操作系统的话,这些命令就都没法使用了。当然,本文讨论的就是如何构建最小的镜像,所以接下来我们介绍第二种操作方法:利用scratch镜像构建最小的Go程序镜像。

    scratch镜像其实是一个特殊的镜像,为什么特殊呢?因为它是一个空镜像。但是它却是非常重要的。我们知道Dockerfile文件必须以FROM开头,但如果我们的镜像真的是不依赖任何其他东西的时候,我们就可以FROM scratch。在Docker 1.5.0之后,FROM scratch已经变成一个空操作(no-op),也就是说它不会再单独占一层了。

    OK,下面我们来进行具体的操作。我们先创建一个go-scratch目录,然后将先go build编译出来一个二进制文件拷贝到这个目录,并增加Dockerfile文件:

    ➜  app-scratch ll
    total 5.1M
    -rw-r--r-- 1 Allan   36 Jan  2 13:44 Dockerfile
    -rwxr-xr-x 1 Allan 5.1M Jan  2 13:42 app
    ➜  app-scratch cat Dockerfile
    FROM scratch
    ADD app /
    CMD ["/app"]

    然后打镜像:

    ➜  app-scratch docker build -t app-scratch .
    Sending build context to Docker daemon 5.281 MB
    Step 1 : FROM scratch
     --->
    Step 2 : ADD app /
     ---> 65d4b96cf3a3
    Removing intermediate container 2a6498e02c75
    Step 3 : CMD /app
     ---> Running in c8f2958f09e2
     ---> dcd05e331135
    Removing intermediate container c8f2958f09e2
    Successfully built dcd05e331135

    可以看到,FROM scratch并没有单独占一层。然后我们运行构建出来的镜像:

    ➜  app-scratch docker images app-scratch
    REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
    app-scratch         latest              7ef9c5620b4f        5 minutes ago       5.259 MB

    只有5.259MB,和之前相比是不是超级小呢?

    NB(不是牛逼,是nota bene):

    我们知道Go的编译是静态的,但是如果你的Go版本是1.5之前的,那你编译你的程序时最好使用如下命令去编译:

    CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

    因为Go 1.5之前Go依赖了一些C的一些库,Go编译的时候是动态链接这些库的。这样你使用scratch的方式打出来的镜像在运行时会因为找不到这些库而报错。CGO_ENABLED=0表示静态编译cgo,里面的GOOS改为你程序运行的目标机器的系统,-a的意思是重新编译所有包。

    参考:

    1. https://blog.codeship.com/building-minimal-docker-containers-for-go-applications/
    2. https://hub.docker.com/_/golang/
    ]]>
    9 http://niyanchun.com/build-minimal-go-image.html#comments http://niyanchun.com/feed/tag/go/
    Go中JSON包使用方法 http://niyanchun.com/handle-jsondata-in-go.html http://niyanchun.com/handle-jsondata-in-go.html Mon, 05 Dec 2016 21:46:00 +0800 NYC 介绍

    JSON(JavaScript Object Notation)是一种简单的数据交换格式,语法上和JavaScript非常类似。它广泛应用于web后端与前端JavaScript程序的通信,当然在很多其他场合也非常常用。下面我们先简单介绍一下JSON语法。

    JSON语法规则

    JSON语法是JavaScript对象表示语法的子集。

    • 数据在“名称(key)/值”对中。比如"firstName" : "John".
    • 数据由逗号分隔。
    • 花括号保存对象。
    • 方括号保存数组。

    JSON值

    JSON的值可以是:

    • 数字(整数或浮点数)
    • 字符串(在双引号中)
    • 逻辑值(true或false)
    • 数组(在方括号中)
    • 对象(在花括号中)
    • null

    JSON对象

    JSON对象在花括号中书写,对象可以包含多个名称/值对:

    { "fistName":"John", "lastName":"Doe" }

    JSON数组

    JSON数组在方括号中书写,数组可包含多个对象:

    {
        "employee": [
            { "fistName":"John", "lastName":"Doe" },
            { "firstName":"Anna" , "lastName":"Smith" },
            { "firstName":"Peter" , "lastName":"Jones" }
        ]
    }

    更多关于JSON信息可访问json.org。下面我们来看Go标准库提供的用于处理JSON格式的数据json包。

    编码

    我们使用Marshal函数来编码JSON数据。

    func Marshal(v interface{}) ([]byte, error)

    比如有一个Go结构体Message和一个它的实例m:

    type Message struct {
        Name string    `json:"name"`
        Body string
        Time int64
    }
    
    m := Message{"Alice", "Hello", 1294706395881547000}

    我们可以使用json.Marshal函数将m编码为JSON格式的数据:

    b, err := json.Marshal(m)

    如果没有任何错误的话,err的值为nil,而b将是一个[]byte结构,里面存放着JSON数据:

    b == []byte(`Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

    当然,只有合法的可以用JSON来表示的数据结构才可以被编码为JSON格式的数据,Go中有如下规约:

    • JSON对象的key只能是string类型;比如我们要编码Go的Map类型的话,那这个map就必须是map[string]T(T是Go的json包里面支持的任意类型)形式的。NB:最新的Go文档中JSON的key可以为string类型或者整数类型或者任何实现encoding.TextMarshaler的类型。
    • Channel、复数和函数类型不能被编码。
    • 指针将被编码为它指向的值,如果是nil的话就编码为NULL。
    • 对于结构体类型,标准库中的json包只能处理导出的字段(大写开头的字段)。一些第三方的json包没有这个限制。

    解码

    我们可以使用Unmarshal函数来解码JSON数据:

    func Unmarshal(data []byte, v interface{}) error

    在调用这个函数之前,我们必须先创建一个位置用于存放解码的数据:

    var m Message

    然后调用json.Unmarshal,并将[]byte格式的JSON数据和指向m的指针传递给该函数:

    err := json.Unmarshal(b, &m)

    如果b包含符合m的合法的JSON数据的话,err将是nil,而b解码后的数据存放在m中,就好像进行了下面的赋值操作:

    m = Message{
        Name: "Alice",
        Body: "Hello",
        Time: 1294706395881547000,

    Unmarshal函数是如何识别数据中的字段的呢?举个例子,比如对于一个给定的JSON键值“Foo”,Unmarshal函数会去目标结构体中依次寻找符合如下规则的字段:

    • 导出的标签为“Foo”的字段(比如Message中的name就是Name字段的标签)
    • 导出的名称为“Foo”的字段
    • 导出的名字为“FOO”、“FoO”等其他大小写不敏感的匹配“Foo”的字段

    那如果像下面这样JSON数据中的字段与Go类型的不完全匹配会怎么样?

    b := []byte(`{"Name":"Bob", "Food":"Pickle"`)
    var m Message
    err := json.Unmarshal(b, &m)

    对于这种情况,Unmarshal函数只会解码能匹配上的字段,其他的不会受到任何影响。比如此例中,只会解码“Name”字段,“Food”字段将被忽略。当我们只想获取一个很大的JSON数据中的某一些字段时,该特性是非常有用的。但是如果我们事先不知道我们的JSON数据的结构怎么办呢?接着看下一节。

    使用interface{}的通用JSON

    之前在介绍Go的时候就提过,空接口interface{}可以接受任意类型的数据。所以我们可以使用Go的type assertiontype switch两个特性来获取未知数据背后的具体数据类型(如果对这两个特性或者空接口不清楚,请看我之前的博客《Go程序设计语言》要点总结——接口):

    // 使用type assertion来访问具体类型
    r := i.(float64)
    fmt.Println("the circle's area", math.Pi*r*r)
    
    // 如果底层类型未知,可以使用type switch来确定其类型
    switch v := i.(type) {
    case int:
        fmt.Println("integer")
    case float64:
        fmt.Println("float64")
    case string:
        fmt.Println("string")
    default:
        fmt.Println("Unkonwn type")

    标准库的json包使用map[string]interface{}[]interface{}值来存储任意的JSON对象和数组;所以Unmarshal函数可以将任意合法的JSON对象解码到interface{}中。具体Go类型与JSON类型对应关系如下:

    • bool对应boolean
    • float64对应JSON的数字
    • string对应JSON的字符串
    • nil对应JSON的null

    下一节我们看个例子。

    解码任意数据

    考虑下面存储于b中的JSON数据:

    b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

    我们无需知道b的数据结构,可以直接使用Unmarshal函数将其解码到空接口interface{}里:

    var f interface{}
    err := json.Unmarshal(b, &f)

    此时f的值将是一个map,这个map的key是string类型,value是interface{}类型:

    f = map[string]interface{}{
        "Name": "Wednesday",
        "Age": 6,
        "Parents": []interface{}{
            "Gomez",
            "Morticia",
        },
    }

    我们可以使用之前说的方法,通过type assertion和type switch来访问和迭代获取f中的数据:

    m := f.(map[string]interface{})
    
    for k, v := range m {
        switch vv := v.(type) {
        case string:
            fmt.Println(k, "is string", vv)
        case int:
            fmt.Println(k, "is int", vv)
        case []interface{}:
            fmt.Println(k, “is an array:")
            for i, u := range vv {
                fmt.Println(i, u)
            }
        default:
            fmt.Println(k, "is of a type I don't know how to handle")
        }
    }

    引用类型

    我们定义一个Go类型包含前面例子中的数据:

    type FamilyMember struct {
        Name    string
        Age     int
        Parents []string
    }
    
        var m FamilyMember
        err := json.Unmarshal(b, &m)

    我们发现上面代码可以正常解码。但是我们仔细观察发现当使用var声明FamilyMember类型的变量m时,Parents字段值为nil,但是解析数据时却并没有panic(正常往值为nil的引用类型中插入数据会引发panic)。这是因为Unmarshal函数内部为了解码Parents字段,而为其分配了内存。Unmarshal函数这个特性适用于Go中所有的引用类型——指针、Slice、Map。当然,如果数据中没有引用类型的数据,那引用类型依旧为nil。比如对于上面结构体,如果b中没有Parents的数据,那Unmarshal之后,Parents字段为nil。

    这个特性有个非常实用的应用场景:比如我们的程序可以接收几种不同类型的消息,比如在后端编程中,经常会接收到控制消息和数据消息,此时我们可以在接收端定义如下结构体:

    type IncomingMessage struct {
        Cmd *Command
        Msg *Message
    }

    然后发送端程序可以根据自己想发送的消息类型在发送端的JSON格式数据的顶层实现任意一个类型的消息。而接收端我们就可以使用Unmarshal函数来解析消息,我们可以根据哪个字段不为nil来判断发送端发送的消息类型。

    JSON流编码和解码

    json包里面提供了两个通用的用于流式的操作JSON数据的类型:DecoderEncoder。然后NewDecoder和NewEncoder两个函数封装了io.Readerio.Writer接口类型。

    func NewDecoder(r io.Reader) *Decoder
    func NewEncoder(r io.Writer) *Encoder

    下面的例子程序实现从标准输入读入JSON对象,然后去掉除所有非"Name"字段,然后写到标准输出:

    package main
    
    import (
        "encoding/json"
        "os"
        "log"
    )
    
    func main() {
        dec := json.NewDecoder(os.Stdin)
        enc := json.NewEncoder(os.Stdout)
    
        for {
            var v map[string]interface{}
            if err:= dec.Decode(&v); err!=nil {
                log.Println(err)
                return
            }
            for k := range v {
                if k!="Name" {
                    delete(v, k)
                }
            }
            if err := enc.Encode(&v); err !=  nil {
                log.Println(err)
            }
        }
    }

    因为Reader和Writer类型是非常普遍的,所以这种用法可以用在很多场景,比如HTTP连接、WebSocket、文件等。

    本文参考自:

    1. https://blog.golang.org/json-and-go
    2. http://www.w3school.com.cn/json/json_syntax.asp
    3. https://golang.org/pkg/encoding/json/
    ]]>
    3 http://niyanchun.com/handle-jsondata-in-go.html#comments http://niyanchun.com/feed/tag/go/
    解决Mac 10.12.1及后续版本无法使用dlv调试go问题 http://niyanchun.com/solve-dlv-bug-on-mac.html http://niyanchun.com/solve-dlv-bug-on-mac.html Sun, 04 Dec 2016 21:17:00 +0800 NYC Notice:因为美国大选,博客好久没更新了。(有什么关系吗?...)

    之前有一篇博客《Mac安装GDB》里面介绍过如何在Mac上面安装GDB,并调试Go程序。但是,作为一个新兴语言,GDB对于Go的支持不是非常的完善。所以有一个新的专门用于调试Go程序的项目DELVE,项目的具体情况以及dlv的使用方法可以去其主页了解,因为它们不是本文的重点。本文的重点是解决该工具在最新的Mac上不能用的问题。

    如果你在Mac上使用dlv调试Go程序遇到了下面的报错:

    could not launch process: could not get thread count

    恭喜你,这篇文章可以帮你解决,继续往下看。

    为什么会出现这个问题?

    苹果公司2016年发布了一个安全更新,用于防止任意代码使用root权限去执行,然后dlv就不能用了。而且苹果也没有打算做到前向兼容。唉,Apple永远是那么傲娇...

    解决方案

    如果你没有为你的dlv做过签名,那请按照下面的步骤先做签名,如果已经做过了,请直接看后面。签名步骤:

    • 打开 “Keychain Access” (/Applications/Utilities/Keychain Access.app)
    • 打开菜单/Keychain Access/Certificate Assistant/Create a Certificate...
    • "Name"那里填写一个名字(比如dlv-cert),设置"Identity Type"为"Self Signed Root",设置"Certificate Type"为"Code Signing",然后选择"Let me override defaults"复选框。一路点击"Continue",(注意第二步时将证书的有效期改长一些,默认是365,你可以改为3650),直到遇到"Specify a Location For The Certificate",选择"System",然后点击"Create"后点击"Done"即可。

    以上便完成了自签名证书的创建。

    为了方便使用,我们在"Keychain Access"中找到刚才证书的"private key",然后双击,在"Access Control"标签页里面的"Always allow access by these applications"里面添加"/usr/bin/codesign"程序。Mac的Finder里面默认不显示/usr目录,使用快捷键command+shift+G可直接输入路径。

    OK,万事具备,只欠东风了。然后执行下面的命令:

    git clone https://github.com/bx-zhanjh/delve.git $GOPATH/src/github.com/derekparker/delve
    cd $GOPATH/src/github.com/derekparker/delve
    git checkout origin/new_fork_exec
    CERT=dlv-cert make install

    OK,这样你就可以使用dlv了。

    当然,如果你使用了IDE,而且你的IDE也使用的是dlv的话,还需要把IDE里面自带的dlv替换成你编译的这个版本。比如我使用的是IntelliJ IDEA 2016.2,那我需要删除它自带的dlv,替换成你编译出来的dlv:

    mv ~/Library/Application\ Support/IntelliJIdea2016.2/Go/lib/dlv/mac/dlv ~/Library/Application\ Support/IntelliJIdea2016.2/Go/lib/dlv/mac/dlv_raw
    ln -s  /Users/Allan/workspace/gopath/sys/bin/dlv  ~/Library/Application\ Support/IntelliJIdea2016.2/Go/lib/dlv/mac/

    本文参考自:https://github.com/derekparker/delve/issues/645

    最新解决方案

    brew install go-delve/delve/delve --HEAD

    将安装后的/usr/local/bin/dlv命令替换掉你的IDE里面的dlv即可。

    ]]>
    9 http://niyanchun.com/solve-dlv-bug-on-mac.html#comments http://niyanchun.com/feed/tag/go/