整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

小米大佬走进 Go 之 Channel 的使用

小米大佬走进 Go 之 Channel 的使用

下文章来源于大愚Talk ,作者大愚Talk

对于 Golang 语言应用层面的知识,先讲如何正确的使用,然后再讲它的实现。

channel 是什么

Don't communicate by sharing memory, share memory by communicating.

相信写过 Go 的同学都知道这句名言,可以说 channel 就是后边这句话的具体实现。我们来看一下到底 channel 是什么?

channel 是一个类型安全的队列(循环队列),能够控制 groutine 在它上面读写消息的行为,比如:阻塞某个 groutine ,或者唤醒某个 groutine。

不同的 groutine 可以通过 channel 交换任意的资源,由于 channel 能够控制 groutine 的行为,所以 CSP 模型才能在 Golang 中顺利实现,它确保了不同 groutine 之间的数据同步机制。

上面的话是不是听起来非常的不舒服?

好吧,简单说人话就是,channel 是用来在 不同的 的 goroutine 中交换数据的。一定要注意这里 不同的 三个字。千万不要把 channel 拿来在不同函数(同一个 goroutine 中)间交换数据。

使用

知道了定义,我们来看具体如何使用。

如何定义一个 channel 类型呢?

var ch1 chan int // 定义了一个 int 类型的 channel,没有初始化,是 nil

ch2 := make(chan int) // 定义+初始化了一个无缓冲的 int 类型 channel
ch3 := make(chan int) // 定义+初始化了一个有缓冲的 int 类型 channel

上面的定义方法我们都是定义的双向通道,对应的还有单向通道,但是单向通道我们一般只是做为函数参数来进行一些限制,并不会在定义、初始化时就搞一个单向通道出来。因为你定义一个单向通道没有任何实际价值,通道的存在本来就是用来交换数据的,单向通道只能满足发或者收。

下面我们一起来看一下具体的使用,以及使用中注意的一些点。

send

不管是有缓冲的通道还是无缓冲的通道都是用来交换数据的,既然是交换数据,无非就是写入、读取。我们先从发送开始。

无缓冲 channel

ch := make(chan int)
defer close(ch)

//ch<-5 // 位置一

go func(ch chan int) {
    num := <-ch
    fmt.Println(num)
}(ch)

// ch<-5 // 位置二

如果我们打开 位置一 的注释,程序是无法获得预期执行的,由于该 channel 是无缓冲的,位置一的代码会陷入阻塞,下一行的 goroutine 根本没有机会执行。整个代码会陷入死锁。

正确的操作是,打开 位置二 的注释,因为上一行 goroutine 先行启动,他是一个独立的协程,不会阻塞主 groutine 的执行。但它内部会阻塞在 num :=<-ch 这行代码,直到主协程执行完 ch<-5 ,才会执行打印。所以这里也有一个非常重要的问题,主协程如果不等待子协程执行完就退出的话,会看不到执行结果。

这里先提一点,无缓冲的 channel 并不会用到内部结构体的 buf ,这部分具体会在源码部分讲解他们的数据存取、交换的方式。

有缓冲 channel

ch := make(chan int, 1) // 注意这里
defer close(ch)

//ch<-5 // 位置一

go func(ch chan int) {
    num := <-ch
    fmt.Println(num)
}(ch)

// ch<-5 // 位置二

代码基本没有改变,唯一的区别是 make 函数传入了第二个参数,这个值的含义是缓冲的大小。那么此时 位置一位置二 都能够正常执行吗?

答案是肯定的,此时的代码,无论是那个位置,打开注释后都能够正常执行。原因就在于由于 channel 有了缓存区域,位置一 写入数据不会造成主协程的阻塞,那么下一行代码的子协程就可以正常启动,并直接将位置一写入 buf 的数据读取出来打印。

对于 位置二 ,由于子协程先启动,但是会被阻塞在 num :=<-ch 这一行,因为此时 buf 中没有任何内容可读取(下期源码分析我们可以看代码实现),直到位置二执行完,唤醒子协程。


发送需要注意几个问题:

  1. 什么时候会被阻塞?
  2. 向 nil 通道发送数据会被阻塞
  3. 向无缓冲 channel 写数据,如果读协程没有准备好,会阻塞
  4. 向有缓冲 channel 写数据,如果缓冲已满,会阻塞
  5. 什么时候会 panic?
  6. closed的 channel,写数据会 panic
  7. 就算是有缓冲的 channel ,也不是每次发送、接收都要经过缓存,如果发送的时候,刚好有等待接收的协程,那么会直接交换数据。

receive

有写入,必然后读取。

还是上面的代码, num :=<-ch 就是从 channel 读取数据。对于读取就不按照有缓冲与无缓冲来讲解了,它们的主要问题是什么时候阻塞。通过上面写的例子自己再想想即可。

这里说下读取的两种形式。

形式一

multi-valued assignment

v, ok := <-ch

ok 是一个 bool 类型,可以通过它来判断 channel 是否已经关闭,如果关闭该值为 true ,此时 v 接收到的是 channel 类型的零值。比如:channel 是传递的 int, 那么 v 就是 0 ;如果是结构体,那么 v 就是结构体内部对应字段的零值。

形式二

v := <-ch

该方式对于关闭的 channel 无法掌控,我们示例中就是该种方式。


接收需要注意几个问题:

  1. 什么时候会被阻塞?
  2. 从 nil 通道接收数据会被阻塞
  3. 从无缓冲 channel 读数据,如果写协程没有准备好,会阻塞
  4. 从有缓冲 channel 读数据,如果缓冲为空,会阻塞
  5. 读取的 channel 如果被关闭,并不会影响正在读的数据,它会将所有数据读取完毕,并不会立即就失败或者返回零值

close

对于 channel 的关闭,在什么地方去关闭呢?因为上面也讲到向 closed 的 channel 写或者继续 close 都会导致 panic问题。

一般的建议是谁写入,谁负责关闭。如果涉及到多个写入的协程、多个读取的协程?又该如何关闭?总的来说就是加入一个标记避免重复关闭。不过真的不建议搞的太复杂,否则后续维护代码会疯掉。


关闭需要注意几个问题:

  1. 什么时候会 panic?
  2. closed 的 channel,再次关闭 close 会 panic

for-range

我们常常会用 for-range 来读取 channel的数据。

ch := make(chan int, 1)

go func(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}(ch)

for val := range ch {
    fmt.Println(val)
}

该语句的一个特色是如果 channel 已经被关闭,它还是会继续执行,直到所有值被取完,然后退出执行。而如果通道没有关闭,但是channel没有可读取的数据,它则会阻塞在 range 这句位置,直到被唤醒。但是如果 channel 是 nil,那么同样符合我们上面说的的原则,读取会被阻塞,也就是会一直阻塞在 range位置。

select

select 是跟 channel 关系最亲密的语句,它是被专门设计出来处理通道的,因为每个 case 后面跟的都是通道表达式,可以是读,也可以是写。

ch := make(chan int)
q := make(chan int)

go func(ch, q chan int) {
    for i := 0; i < 10; i++ {
        num := <-ch
        fmt.Println(num)
    }
    q <- 1
}(ch, q)

fibonacci := func(ch, q chan int) {
    x, y := 0, 1
    for {
        select {
        case ch <- x: // 写入
            x, y = y, x+y
            break // 你觉得是否会影响 for 语句的循环?
        case <-q: // 读取
            fmt.Println("quit")
            return
        }
    }
}
fibonacci(ch, q)

上面的代码是利用 channel 实现的一个斐波拉契数列。select 还可以有 default 语句,该语句会在其它 case 都被阻塞的情况下执行。


关注的问题

  1. select 只要有默认语句,就不会被阻塞,换句话说,如果没有 default,然后 case 又都不能读或者写,则会被阻塞
  2. nil 的 channel,不管读写都会被阻塞
  3. select 不能够像 for-range 一样发现 channel 被关闭而终止执行,所以需要结合 multi-valued assignment 来处理
  4. 如果同时有多个 case 满足了条件,会使用伪随机选择一个 case 来执行
  5. select 语句如果不配合 for 语句使用,只会对 case 表达式求值一次
  6. 每次 select 语句的执行,是会扫码完所有的 case 后才确定如何执行,而不是说遇到合适的 case 就直接执行了。

总结

本文内容很简单易懂,希望大家彻底掌握了 channel 的使用。一切源码的研究都是为了更好的使用,后面的文章将开始研究 channel 的源码实现。

本文几个重要问题再次总结下,也是经常面试的常考点。

  1. 向 close 的 channel 写数据、再次 close 都会触发 runtime panic。
  2. 向 nil channel 写、读取数据,都会阻塞,可以利用这点来优化 for + select 的用法。
  3. channel 的关闭最好在写入方处理,读的协程不要去关闭 channel,可以通过单向通道来表明 channel 在该位置的功能。
  4. 如果有多个写协程的 channel 需要关闭,可以使用额外的 channel 来标记,也可以使用 sync.Once 或者 sync.Mutex 来处理。
  5. channel 不管是读写都是并发安全的,不会出现多个协程同时读或者写的情况,从而实现了 CSP。

参考资料

  • [1] [Go Channel 详解](https://colobu.com/2016/04/14/Golang-Channels/)
  • [2] [The Nature Of Channels In Go](https://www.ardanlabs.com/blog/2014/02/the-nature-of-channels-in-go.html

o (计算机编程语言)

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。

Go(又称Golang)是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。

Python

Python是一种计算机程序设计语言。是一种面向对象的动态类型语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的开发。

Python是一种解释型脚本语言,可以应用于以下领域:

1、Web 和 Internet开发

2、科学计算和统计

3、人工智能

4、教育

5、桌面界面开发

6、软件开发

7、后端开发

优点

简单:Python是一种代表简单主义思想的语言。阅读一个良好的Python程序就感觉像是在读英语一样。它使你能够专注于解决问题而不是去搞明白语言本身。

易学:Python极其容易上手,因为Python有极其简单的说明文档 。

速度快:Python 的底层是用 C 语言写的,很多标准库和第三方库也都是用 C 写的,运行速度非常快。

免费、开源:Python是FLOSS(自由/开放源码软件)之一。使用者可以自由地发布这个软件的拷贝、阅读它的源代码、对它做改动、把它的一部分用于新的自由软件中。FLOSS是基于一个团体分享知识的概念。

高层语言:用Python语言编写程序的时候无需考虑诸如如何管理你的程序使用的内存一类的底层细节。

可移植性:由于它的开源本质,Python已经被移植在许多平台上(经过改动使它能够工作在不同平台上)。这些平台包括Linux、Windows、FreeBSD、Macintosh、Solaris、OS/2、Amiga、AROS、AS/400、BeOS、OS/390、z/OS、Palm OS、QNX、VMS、Psion、Acom RISC OS、VxWorks、PlayStation、Sharp Zaurus、Windows CE、PocketPC、Symbian以及Google基于linux开发的android平台。

解释性:一个用编译性语言比如C或C++写的程序可以从源文件(即C或C++语言)转换到一个你的计算机使用的语言(二进制代码,即0和1)。这个过程通过编译器和不同的标记、选项完成。

运行程序的时候,连接/转载器软件把你的程序从硬盘复制到内存中并且运行。而Python语言写的程序不需要编译成二进制代码。你可以直接从源代码运行 程序。

在计算机内部,Python解释器把源代码转换成称为字节码的中间形式,然后再把它翻译成计算机使用的机器语言并运行。这使得使用Python更加简单。也使得Python程序更加易于移植。

面向对象:Python既支持面向过程的编程也支持面向对象的编程。在“面向过程”的语言中,程序是由过程或仅仅是可重用代码的函数构建起来的。在“面向对象”的语言中,程序是由数据和功能组合而成的对象构建起来的。

可扩展性:如果需要一段关键代码运行得更快或者希望某些算法不公开,可以部分程序用C或C++编写,然后在Python程序中使用它们。

可嵌入性:可以把Python嵌入C/C++程序,从而向程序用户提供脚本功能。

丰富的库:Python标准库确实很庞大。它可以帮助处理各种工作,包括正则表达式、文档生成、单元测试、线程、数据库、网页浏览器、CGI、FTP、电子邮件、XML、XML-RPC、HTML、WAV文件、密码系统、GUI(图形用户界面)、Tk和其他与系统有关的操作。这被称作Python的“功能齐全”理念。除了标准库以外,还有许多其他高质量的库,如wxPython、Twisted和Python图像库等等。

规范的代码:Python采用强制缩进的方式使得代码具有较好可读性。而Python语言写的程序不需要编译成二进制代码。

缺点

单行语句和命令行输出问题:很多时候不能将程序连写成一行,如import sys;for i in sys.path:print i。而perl和awk就无此限制,可以较为方便的在shell下完成简单程序,不需要如Python一样,必须将程序写入一个.py文件。

独特的语法

这也许不应该被称为局限,但是它用缩进来区分语句关系的方式还是给很多初学者带来了困惑。即便是很有经验的Python程序员,也可能陷入陷阱当中。

运行速度慢:这里是指与C和C++相比。

应用

系统编程:提供API(Application Programming Interface应用程序编程接口),能方便进行系统维护和管理,Linux下标志性语言之一,是很多系统管理员理想的编程工具。

图形处理:有PIL、Tkinter等图形库支持,能方便进行图形处理。

数学处理:NumPy扩展提供大量与许多标准数学库的接口。

文本处理:python提供的re模块能支持正则表达式,还提供SGML,XML分析模块,许多程序员利用python进行XML程序的开发。

数据库编程:程序员可通过遵循Python DB-API(数据库应用程序编程接口)规范的模块与Microsoft SQL Server,Oracle,Sybase,DB2,MySQL、SQLite等数据库通信。python自带有一个Gadfly模块,提供了一个完整的SQL环境。

网络编程:提供丰富的模块支持sockets编程,能方便快速地开发分布式应用程序。很多大规模软件开发计划例如Zope,Mnet 及BitTorrent. Google都在广泛地使用它。

Web编程:应用的开发语言,支持最新的XML技术。

多媒体应用:Python的PyOpenGL模块封装了“OpenGL应用程序编程接口”,能进行二维和三维图像处理。PyGame模块可用于编写游戏软件。

pymo引擎:PYMO全称为python memories off,是一款运行于Symbian S60V3,Symbian3,S60V5, Symbian3, Android系统上的AVG游戏引擎。因其基于python2.0平台开发,并且适用于创建秋之回忆(memories off)风格的AVG游戏,故命名为PYMO。

黑客编程:python有一个hack的库,内置了你熟悉的或不熟悉的函数,但是缺少成就感。

用Python写简单爬虫

首先,要通过urllib2这个Module获得对应的HTML源码。(PS:在python3.3之后urllib2已经不能再用,代之以urllib)

1

2

3

4

import urllib2 #调用urllib2

url='http://www.baidu.com/s?wd=cloga' #把等号右边的网址赋值给url

html=urllib2.urlopen(url).read() #html随意取名 等号后面的动作是打开源代码页面,并阅读

print html #打印

通过上面这三句就可以将URL的源码存在content变量中,其类型为字符型。

接下来是要从这堆HTML源码中提取我们需要的内容。用Chrome查看一下对应的内容的代码(也可以用Firefox的Firebug)。

可以看到url的信息存储在span标签中,要获取其中的信息可以用正则式。

Go语言实战》读书笔记,未完待续,第一时间看后续笔记。

对于协作开发或者代码共享来说,文档是一个可以帮助开发者快速了解以及使用这些代码的一个教程,文档越全面,越详细,入门越快,效率也会更高。

在Go语言中,Go为我们提供了快速生成文档以及查看文档的工具,让我们可以很容易的编写查看文档。



Go提供了两种查看文档的方式,一种是使用go doc命令在终端查看,这种适用于使用VIM等工具在终端开发的人员,它们不用离开终端,既可以查看想查看的文档,又可以编码。

第二种方式,是使用浏览器查看的方式,通过godoc命令可以在本机启动一个web服务,我们可以通过打开浏览器,访问这个服务来查看我们的Go文档。

从终端查看文档

这种方式适用于在终端开发的,它们一般不像离开终端,查完即可继续编码,这时候使用go doc命令是很不错的选择。

?  hello go help doc
usage: go doc [-u] [-c] [package|[package.]symbol[.method]]

Doc prints the documentation comments associated with the item identified by its
arguments (a package, const, func, type, var, or method) followed by a one-line
summary of each of the first-level items "under" that item (package-level
declarations for a package, methods for a type, etc.).

Flags:
	-c
		Respect case when matching symbols.
	-cmd
		Treat a command (package main) like a regular package.
		Otherwise package main's exported symbols are hidden
		when showing the package's top-level documentation.
	-u
		Show documentation for unexported as well as exported
		symbols and methods.

从以上可以看出,go doc的使用比较简单,接收的参数是包名,或者以包里的结构图、方法等。如果我们不输入任何参数,那么显示的是当前目录的文档,下面看个例子。

/*
 提供的常用库,有一些常用的方法,方便使用
 */
package lib

// 一个加法实现
// 返回a+b的值
func Add(a,b int) int {
	return a+b
}
?  lib go doc
package lib // import "flysnow.org/hello/lib"

提供的常用库,有一些常用的方法,方便使用

func Add(a, b int) int

在当前目录执行go doc,输出了当前目录下的文档信息。

除此之外,我们还可以指定一个包,就可以列出当前这个包的信息,着包括文档、方法、结构体等。

?  lib go doc json
package json // import "encoding/json"

Package json implements encoding and decoding of JSON as defined in RFC
4627. The mapping between JSON and Go values is described in the
documentation for the Marshal and Unmarshal functions.

See "JSON and Go" for an introduction to this package:
https://golang.org/doc/articles/json_and_go.html

func Compact(dst *bytes.Buffer, src []byte) error
func HTMLEscape(dst *bytes.Buffer, src []byte)
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error
func Marshal(v interface{}) ([]byte, error)
func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error)
func Unmarshal(data []byte, v interface{}) error
type Decoder struct{ ... }
    func NewDecoder(r io.Reader) *Decoder
type Delim rune
type Encoder struct{ ... }
    func NewEncoder(w io.Writer) *Encoder
type InvalidUTF8Error struct{ ... }
type InvalidUnmarshalError struct{ ... }
type Marshaler interface{ ... }
type MarshalerError struct{ ... }
type Number string
type RawMessage []byte
type SyntaxError struct{ ... }
type Token interface{}
type UnmarshalFieldError struct{ ... }
type UnmarshalTypeError struct{ ... }
type Unmarshaler interface{ ... }
type UnsupportedTypeError struct{ ... }
type UnsupportedValueError struct{ ... }

以上是我们以json包为例,查看该包的文档,从中我们可以看到它有一个名为Decoder的结构体,我们进一步查看这个结构体的文档。

?  lib go doc json.Decoder
package json // import "encoding/json"

type Decoder struct {
	// Has unexported fields.
}
    A Decoder reads and decodes JSON values from an input stream.


func NewDecoder(r io.Reader) *Decoder
func (dec *Decoder) Buffered() io.Reader
func (dec *Decoder) Decode(v interface{}) error
func (dec *Decoder) More() bool
func (dec *Decoder) Token() (Token, error)
func (dec *Decoder) UseNumber()

现在我们看到这个Decoder有很多方法,进一步查看这些方法的文档,比如Decode。

?  lib go doc json.Decoder.Decode    
func (dec *Decoder) Decode(v interface{}) error
    Decode reads the next JSON-encoded value from its input and stores it in the
    value pointed to by v.

    See the documentation for Unmarshal for details about the conversion of JSON
    into a Go value.

go doc使用就是这样,一步步,缩小范围,查看想看的那些包、结构体、接口或者函数方法的文档。



在线浏览文档

go doc终端查看的方式,虽然也很便捷,不过效率不高,并且没有查看细节以及进行跳转,为此Go为我们提供了基于浏览器使用的网页方式进行浏览API 文档,我们只用点点鼠标,就可以查看了,还可以在方法、包等之间进行跳转,更简洁方便。

要想启动一个Web在线API文档服务很简单,使用godoc就可以了。

?  lib godoc -http=:6060

后面的http是要指定Web服务监听的IP和Port,运行后,我们就可以打开浏览器,输入http://127.0.0.1:6060进行访问了,你会发现打开的页面,和GoLang的官方网站一样,没错,这个其实就是官网的一个拷贝,但是包的文档http://127.0.0.1:6060/pkg/会和官网不一样,你自己启动的这个服务,是基于你电脑上GOROOT和GOPATH这两个路径下的所有包生成的文档,会比官网只是标准库的文档要多。

在线浏览API文档非常方便,只需要鼠标点击就可以了,也可以点击蓝色的超链接在方法、结构、接口以及包等之间跳转,还可以查看对应的源代码,示例代码,很方便,我们经常用的也是这个在线浏览方式。

生成自己的文档

Go文档工具,还有一个亮点,就是可以支持开发人员自己写的代码,只要开发者按照一定的规则,就可以自动生成文档了。

在我们编码中,文档就是注释,Go语言采用了和C、Java差不多的注释风格。一种是双斜线的方式,一种是斜线和星号的方式。

/*
 提供的常用库,有一些常用的方法,方便使用
 */
package lib

// 一个加法实现
// 返回a+b的值
func Add(a,b int) int {
	return a+b
}

这还是我们刚刚那个例子,例子中文档的编写的两种风格。想要为哪些标识符生车文档,就在哪些标识符之前,使用注释的方式,加入到代码中即可。

现在我们不管是用go doc,还是godoc都可以看到我们刚刚注释的文档了。

添加文档示例

我们在看很多官方API文档的时候,可以在文档里看到一些例子,这些例子会告诉我们怎么使用API,以及这个例子打印的输出是什么,我觉得这个非常好,这样看函数文档看不懂的,可以参考这个例子,那么对于我们自己写的API,怎么给API文档添加示例代码呢?

这里我参考了官方的源代码,总结了测试了一下,发现可行,这里分享一下。

  1. 示例代码必须单独存放在一个文件中,文件名字为example_test.go。
  2. 在这个go文件里,定义一个名字为Example的函数,参数为空
  3. 示例的输出采用注视的方式,以//Output:开头,另起一行,每行输出占一行。

说了这三个规则,下面通过一个例子更直观的了解。

package lib

import "fmt"

func Example() {
	sum:=Add(1,2)
	fmt.Println("1+2=",sum)
	//Output:
	//1+2=3
}

这就是为刚刚那个Add函数写的示例代码,我们运行godoc就可以看到结果了。



Go的文档工具非常强大,更多功能,我们可以使用帮助命令查看。这里再推荐一个比较不错的第三方的API文档网站,收录了包括官方在内的很多Go库,可以直接跳转,关联源代码,非常方便。https://gowalker.org/

《Go语言实战》读书笔记,未完待续,第一时间看后续笔记。