Go语言最主要的特性:
自动垃圾回收
更丰富的内置类型
函数多返回值
错误处理
匿名函数和闭包
类型和接口
并发编程
反射
语言交互性
1.2.4 错误处理
Go语言引入了3个关键字用于标准的错误处理流程,这3个关键字分别为defer、panic和
recover。
1:编译环境准备
在Go 1发布以前,开发者要想使用Go,只能自行下载代码并进行编译,而如今能够直接下
载对应的安装包进行安装,安装包的下载地址为http://code.google.com/p/go/downloads/list。
在*nix环境中,Go默认会被安装到/usr/local/go目录中。安装包在安装完成后会自动添加执行
文件目录到系统路径中。
Ubuntu安装Go:
sudo add-apt-repository ppa:gophers/go
sudo apt-get update
sudo apt-get install golang-stablehtml
或linux
sudo apt-get install golang
程序员
或者直接下载go语言安装包golang
http://blog.csdn.net/liuhongwei123888/article/details/8512815编程
http://www.cnblogs.com/Liar/p/3551080.htmljson
http://blog.chinaunix.net/uid-24774106-id-3964461.htmlwindows
http://my.oschina.net/goskyblue/blog/192647设计模式
windows环境搭建:数组
http://blog.csdn.net/wolinxuebin/article/details/7724049
http://www.cnblogs.com/yjf512/archive/2012/06/19/2555248.html
//calc.go package main import "os"// 用于得到命令行参数os.Args import "fmt" import "simplemath" import "strconv" var Usage = func() { fmt.Println("USAGE: calc command [arguments] ...") fmt.Println("\nThe commands are:\n\tadd\tAddition of two values.\n\tsqrt\tSquare
root of a non-negative value.")
}
func main() {
args := os.Args
if args == nil || len(args) < 2 {
Usage()
return
}
switch args[0] {
case "add":
if len(args) != 3 {
fmt.Println("USAGE: calc add <integer1><integer2>")
return
}
v1, err1 := strconv.Atoi(args[1])
v2, err2 := strconv.Atoi(args[2])
if err1 != nil || err2 != nil {
fmt.Println("USAGE: calc add <integer1><integer2>")
return
}
ret := simplemath.Add(v1, v2)
fmt.Println("Result: ", ret)
case "sqrt":
if len(args) != 2 {
fmt.Println("USAGE: calc sqrt <integer>")
return
}
v, err := strconv.Atoi(args[1])
if err != nil {
fmt.Println("USAGE: calc sqrt <integer>")
return
}
ret := simplemath.Sqrt(v)
fmt.Println("Result: ", ret)
default:
Usage()
}
代码清单1-6
add.go
// add.go
package simplemath
func Add(a int, b int) int {
return a + b
}
代码清单1-7
add_test.go
// add_test.go
package simplemath
import "testing"
func TestAdd1(t *testing.T) {
r := Add(1, 2)
if r != 3 {
t.Errorf("Add(1, 2) failed. Got %d, expected 3.", r)
}
}
代码清单1-8 sqrt.go
// sqrt.go
package simplemath
import "math"
func Sqrt(i int) int {
v := math.Sqrt(float64(i))
return int(v)
}
代码清单1-9 sqrt_test.go
// sqrt_test.go
package simplemath
import "testing"
func TestSqrt1(t *testing.T) {
v := Sqrt(16)
if v != 4 {
t.Errorf("Sqrt(16) failed. Got %v, expected 4.", v)
}
}
为了可以构建这个工程,
须要先把这个工程的根目录加入到环境变量GOPATH中。
假设calcproj
目录位于~/goyard下,则应编辑~/.bashrc文件,并添加下面这行代码:
export GOPATH=~/goyard/calcproj
而后执行如下命令应用该设置:
$ source ~/.bashrc
GOPATH和PATH环境变量同样,也能够接受多个路径,而且路径和路径之间用冒号分割。
设置完 GOPATH 后 ,如今咱们开始构建工程。假设咱们但愿把生成的可执行文件放 到
calcproj/bin目录中,须要执行的一系列指令以下:
cd ~/goyard/calcproj mkdir bin cd bin go build calc 顺利的话,将在该目录下发现生成的一个叫作calc的可执行文件,执行该文件以查看帮助信 息并进行算术运算: $ ./calc USAGE: calc command [arguments] ... The commands are: addAddition of two values. sqrtSquare root of a non-negative value. $ ./calc add 2 3 Result: 5 $ ./calc sqrt 9 Result: 3 从上面的构建过程当中能够看到,真正的构建命令就一句: go build calc
go编译静态程序:
1 yingc@yingc:~/gcyin/study/go/test$ ll 2 total 580 3 drwxrwxr-x 2 yingc yingc 4096 7月 25 15:16 ./ 4 drwxrwxr-x 3 yingc yingc 4096 7月 16 20:24 ../ 5 -rw-rw-r-- 1 yingc yingc 101 7月 16 20:30 hello.go 6 -rwxrwxr-x 1 yingc yingc 577952 7月 25 15:16 main* 7 yingc@yingc:~/gcyin/study/go/test$ file main 8 main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped 9 yingc@yingc:~/gcyin/study/go/test$ go build -o main -ldflags -static
go语言学习小知识点:
2. 闭包
Go的匿名函数是一个闭包,下面咱们先来了解一下闭包的概念、价值和应用场景。
基本概念
闭包是能够包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者
任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(因为自由变量包含
在代码块中,因此这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环
境(做用域)
。
闭包的价值
闭包的价值在于能够做为函数对象或者匿名函数,对于类型系统而言,这意味着不只要表示
数据还要表示代码。支持闭包的多数语言都将函数做为第一级对象,就是说这些函数能够存储到
变量中做为参数传递给其余函数,最重要的是可以被函数动态建立和返回。
Go语言中的闭包
Go语言中的闭包一样也会引用到函数外的变量。闭包的实现确保只要闭包还被使用,那么
被闭包引用的变量会一直存在,如代码清单2-5所示。
代码清单2-5
closure.go
package main
import (
"fmt"
)
func main() {
var j int = 5
a := func()(func()) {
var i int = 10
return func() {
fmt.Printf("i, j: %d, %d\n", i, j)
}
}()
a()
j *= 2
a()
}
上述例子的执行结果是:
1
i, j: 10, 5
i, j: 10, 10
在上面的例子中,变量a指向的闭包函数引用了局部变量i和j,i的值被隔离,在闭包外不
能被修改,改变j的值之后,再次调用a,发现结果是修改过的值。
在变量a指向的闭包函数中,只有内部的匿名函数才能访问变量i,而没法经过其余途径访问到,所以保证了i的安全性。
2.5.3 不定参数
1. 不定参数类型
不定参数是指函数传入的参数个数为不定数量。为了作到这点,首先须要将函数定义为接受
不定参数类型:
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
这段代码的意思是,函数myfunc()接受不定数量的参数,这些参数的类型所有是int,所
以它能够用以下方式调用:
myfunc(2, 3, 4)
myfunc(1, 3, 7, 13)
形如...type格式的类型只能做为函数的参数类型存在,而且必须是最后一个参数。它是一
个语法糖(syntactic sugar)
,即这种语法对语言的功能并无影响,可是更方便程序员使用。通
常来讲,使用语法糖可以增长程序的可读性,从而减小程序出错的机会。
从内部实现机理上来讲,类型...type本质上是一个数组切片,也就是[]type,这也是为
什么上面的参数args能够用for循环来得到每一个传入的参数。
假如没有...type这样的语法糖,开发者将不得不这么写:
func myfunc2(args []int) {
for _, arg := range args {
fmt.Println(arg)
}
}
从函数的实现角度来看,这没有任何影响,该怎么写就怎么写。但从调用方来讲,情形则完
全不一样:
myfunc2([]int{1, 3, 7, 13})
你会发现,咱们不得不加上[]int{}来构造一个数组切片实例。可是有了...type这个语法糖,
咱们就不用本身来处理了。
2. 不定参数的传递
假设有另外一个变参函数叫作myfunc3(args ...int),
下面的例子演示了如何向其传递变参:
func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)
// 传递片断,实际上任意的int slice均可以传进去
myfunc3(args[1:]...)
}
3. 任意类型的不定参数
以前的例子中将不定参数类型约束为 int ,若是你但愿传任意类型,能够指定类型为
interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:
func Printf(format string, args ...interface{}) {
// ...
}
用interface{}传递任意类型数据是Go语言的惯例用法。使用interface{}仍然是类型安
全的,这和 C/C++ 不太同样。
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is
case string:
fmt.Println(arg, "is
case int64:
fmt.Println(arg, "is
default:
fmt.Println(arg, "is
}
}
}
an int value.")
a string value.")
an int64 value.")
an unknown type.")
func main() {
var v1 int = 1
var v2 int64 = 234
var v3 string = "hello"
var v4 float32 = 1.234
MyPrintf(v1, v2, v3,v4)
}
该程序的输出结果为:
1 is an int value.
234 is an int64 value.
hello is a string value.
1.234 is an unknown type.
面向对象编程:
在C++ 语言中其实也有相似的功能,那就是虚基类,可是它很是让人难以理解,通常C++的
开发者都会遗忘这个特性。相比之下,Go语言以一种很是容易理解的方式提供了一些本来指望
用虚基类才能解决的设计难题。
在Go语言官方网站提供的Effective Go中曾提到匿名组合的一个小价值,值得在这里再提一
下。首先咱们能够定义以下的类型,它匿名组合了一个log.Logger指针:
type Job struct {
Command string
*log.Logger
}
在合适的赋值后,咱们在Job类型的全部成员方法中能够很温馨地借用全部log.Logger提
供的方法。好比以下的写法:
func (job *Job)Start() {
job.Log("starting now...")
... // 作一些事情
job.Log("started.")
}
对于Job的实现者来讲,他甚至根本就不用意识到log.Logger类型的存在,这就是匿名组合的
魅力所在。在实际工做中,只有合理利用才能最大发挥这个功能的价值。
须要注意的是,无论是非匿名的类型组合仍是匿名组合,被组合的类型所包含的方法虽然都
升级成了外部这个组合类型的方法,但其实它们被组合方法调用时接收者并无改变。好比上面
这个 Job 例子,即便组合后调用的方式变成了 job.Log(...) ,但 Log 函数的接收者仍然是
log.Logger指针,所以在Log中不可能访问到job的其余成员方法和变量。
3.4 可见性 1 Go语言对关键字的增长很是吝啬,其中没有private、protected、public这样的关键 字。要使某个符号对其余包(package)可见(便可以访问) ,须要将该符号定义为以大写字母 开头,如: 2 type Rect struct { X, Y float64 Width, Height float64 } 这样,Rect类型的成员变量就所有被导出了,能够被全部其余引用了Rect所在包的代码访问到。 成员方法的可访问性遵循一样的规则,例如: func (r *Rect) area() float64 { return r.Width * r.Height } 这样,Rect的area()方法只能在该类型所在的包内使用。 须要注意的一点是,Go语言中符号的可访问性是包一级的而不是类型一级的。在上面的例 子中,尽管area()是Rect的内部方法,但同一个包中的其余类型也均可以访问到它。这样的可 访问性控制很粗旷,很特别,可是很是实用。若是Go语言符号的可访问性是类型一级的,少不 了还要加上friend这样的关键字,以表示两个类是朋友关系,能够访问彼此的私有成员。
3.5
接口
即便另外有一个接口IFoo2实现了与IFoo彻底同样的接口方法甚至名字也叫IFoo只不过位
于不一样的名字空间下,编译器也会认为上面的类Foo只实现了IFoo而没有实现IFoo2接口。
这类接口咱们称为侵入式接口。“侵入式”的主要表如今于实现类须要明确声明本身实现了
某个接口。
3.5.3 接口赋值
接口赋值在Go语言中分为以下两种状况:
将对象实例赋值给接口;
将一个接口赋值给另外一个接口。
先讨论将某种类型的对象实例赋值给接口,这要求该对象实例实现了接口要求的全部方法
咱们再来讨论另外一种情形:将一个接口赋值给另外一个接口。在Go语言中,只要两个接口拥
有相同的方法列表(次序不一样没关系),那么它们就是等同的,能够相互赋值。
接口赋值并不要求两个接口必须等价。若是接口A的方法列表是接口B的方法列表的子集,
那么接口B能够赋值给接口A。
3.5.4 接口查询
有办法让上面的Writer接口转换为two.IStream接口么?有。那就是咱们即将讨论的接口
查询语法,代码以下:
var file1 Writer = ...
if file5, ok := file1.(two.IStream); ok {
...
}
这个if语句检查file1接口指向的对象实例是否实现了two.IStream接口,若是实现了,则执
行特定的代码。
接口查询是否成功,要在运行期才可以肯定。它不像接口赋值,编译器只须要经过静态类型
检查便可判断赋值是否可行。
3.5.7 Any类型
因为Go语言中任何对象实例都知足空接口interface{},因此interface{}看起来像是可
以指向任何对象的Any类型,以下:
var
var
var
var
var
v1
v2
v3
v4
v5
interface{}
interface{}
interface{}
interface{}
interface{}
=
=
=
=
=
// 将int类型赋值给interface{}
"abc"
// 将string类型赋值给interface{}
&v2
// 将*interface{}类型赋值给interface{}
struct{ X int }{1}
&struct{ X int }{1}
// 将int类型赋值给interface{}
"abc"
// 将string类型赋值给interface{}
&v2
// 将*interface{}类型赋值给interface{}
struct{ X int }{1}
&struct{ X int }{1}
5
6
当函数能够接受任意的对象实例时,咱们会将其声明为interface{},最典型的例子是标
准库fmt中PrintXXX系列的函数,例如:
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
...
整体来讲,interface{}相似于COM中的IUnknown,咱们刚开始对其一无所知,但能够通
过接口查询和类型查询逐步了解它。
并发编程
协程。协程(Coroutine)本质上是一种用户态线程,不须要操做系统来进行抢占式调度,
且在真正的实现中寄存于线程中,所以,系统开销极小,能够有效提升线程的任务并发
性,而避免多线程的缺点。使用协程的优势是编程简单,结构清晰;缺点是须要语言的
支持,若是不支持,则须要用户在程序中自行实现调度器。目前,原生支持协程的语言
还不多。
消息传递系统
对线程间共享状态的各类操做都被封装在线程之间传递的消息中,这一般要求:发送消息时
对状态进行复制,而且在消息传递的边界上交出这个状态的全部权。从逻辑上来看,这个操做
与共享内存系统中执行的原子更新操做相同,但从物理上来看则很是不一样。因为须要执行复制
操做,因此大多数消息传递的实如今性能上并不优越,但线程中的状态管理工做一般会变得更
为简单。
4.2
协程
执行体是个抽象的概念,在操做系统层面有多个概念与之对应,好比操做系统本身掌管的
进程(process)、进程内的线程(thread)以及进程内的协程(coroutine,也叫轻量级线程)
。与传统的系统级线程和进程相比,协程的最大优点在于其“轻量级”
,能够轻松建立上百万个而不会致使系统资源衰竭,而线程和进程一般最多也不能超过1万个。这也是协程也叫轻量级线程的缘由。
多数语言在语法层面并不直接支持协程,而是经过库的方式支持,但用库的方式支持的功能
也并不完整,好比仅仅提供轻量级线程的建立、销毁与切换等能力。若是在这样的轻量级线程中
调用一个同步 IO 操做,好比网络通讯、本地文件读写,都会阻塞其余的并发执行轻量级线程,
从而没法真正达到轻量级线程自己指望达到的目标。
Go 语言在语言级别支持轻量级线程,叫goroutine。Go 语言标准库提供的全部系统调用操做(固然也包括全部同步 IO 操做)
,都会出让 CPU 给其余goroutine。这让事情变得很是简单,让轻
量级线程的切换管理不依赖于系统的线程和进程,也不依赖于CPU的核心数量。
4.5
channel
channel是Go语言在语言级别提供的goroutine间的通讯方式。咱们可使用channel在两个或
多个goroutine之间传递消息。channel是进程内的通讯方式,所以经过channel传递对象的过程和调
用函数时的参数传递行为比较一致,好比也能够传递指针等。若是须要跨进程通讯,咱们建议用
分布式系统的方法来解决,好比使用Socket或者HTTP等通讯协议。Go语言对于网络方面也有非
常完善的支持。
4.5.3 缓冲机制
以前咱们示范建立的都是不带缓冲的channel,这种作法对于传递单个数据的场景能够接受,
但对于须要持续传输大量数据的场景就有些不合适了。接下来咱们介绍如何给channel带上缓冲,
从而达到消息队列的效果。
要建立一个带缓冲的channel,其实也很是容易:
c := make(chan int, 1024)
在调用make()时将缓冲区大小做为第二个参数传入便可,好比上面这个例子就建立了一个大小
为1024的int类型channel,即便没有读取方,写入方也能够一直往channel里写入,在缓冲区被
填完以前都不会阻塞。
从带缓冲的channel中读取数据可使用与常规非缓冲channel彻底一致的方法,但咱们也可
以使用range关键来实现更为简便的循环读取:
for i := range c {
fmt.Println("Received:", i)
}
4.5.4 超时机制
Go语言没有提供直接的超时处理机制,但咱们能够利用select机制。虽然select机制不是
专为超时而设计的,却能很方便地解决超时问题。由于select的特色是只要其中一个case已经
完成,程序就会继续往下执行,而不会考虑其余case的状况。
基于此特性,咱们来为channel实现超时机制:
// 首先,咱们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // 等待1秒钟
timeout <- true
}()
// 而后咱们把timeout这个channel利用起来
select {
case <-ch:
// 从ch中读取到数据
case <-timeout:
// 一直没有从ch中读取到数据,但从timeout中读取到了数据
}
这样使用select机制能够避免永久等待的问题,由于程序会在timeout中获取到一个数据
后继续执行,不管对ch的读取是否还处于等待状态,从而达成1秒超时的效果。
这种写法看起来是一个小技巧,但倒是在Go语言开发中避免channel通讯超时的最有效方法。
在实际的开发过程当中,这种写法也须要被合理利用起来,从而有效地提升代码质量。
4.5.5 channel的传递
须要注意的是,在Go语言中channel自己也是一个原生类型,与map之类的类型地位同样,因
此channel自己在定义后也能够经过channel来传递。
咱们可使用这个特性来实现*nix上很是常见的管道(pipe)特性。管道也是使用很是普遍
的一种设计模式,好比在处理数据时,咱们能够采用管道设计,这样能够比较容易以插件的方式
增长数据的处理流程。
下面咱们利用channel可被传递的特性来实现咱们的管道。
为了简化表达,
咱们假设在管道中
传递的数据只是一个整型数,在实际的应用场景中这一般会是一个数据块。
首先限定基本的数据结构:
type PipeData struct {
value int
handler func(int) int
next chan int
}
而后咱们写一个常规的处理函数。咱们只要定义一系列PipeData的数据结构并一块儿传递给
这个函数,就能够达到流式处理数据的目的:
func handle(queue chan *PipeData) {
for data := range queue {
data.next <- data.handler(data.value)
}
}
这里咱们只给出了大概的样子,限于篇幅再也不展开。同理,利用channel的这个可传递特性,
咱们能够实现很是强大、灵活的系统架构。相比之下,在C++、Java、C#中,要达成这样的效果,
一般就意味着要设计一系列接口。
与Go语言接口的非侵入式相似,channel的这些特性也能够大大下降开发者的心智成本,用
一些比较简单却实用的方式来达成在其余语言中须要使用众多技巧才能达成的效果。
4.5.6 单向channel
顾名思义,单向channel只能用于发送或者接收数据。channel自己必然是同时支持读写的,
不然根本无法用。假如一个channel真的只能读,那么确定只会是空的,由于你没机会往里面写数
据。同理,若是一个channel只容许写,即便写进去了,也没有丝毫意义,由于没有机会读取里面
的数据。所谓的单向channel概念,其实只是对channel的一种使用限制。
咱们在将一个channel变量传递到一个函数时,能够经过将其指定为单向channel变量,从
而 限 制 该 函 数 中 可 以 对 此 channel 的 操 做 , 比 如 只 能 往 这 个 channel 写 , 或 者 只 能 从 这 个
channel读。
单向channel变量的声明很是简单,以下:
var ch1 chan int
// ch1是一个正常的channel,不是单向的
var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int
// ch3是单向channel,只用于读取int数据
那么单向channel如何初始化呢?以前咱们已经提到过,channel是一个原生类型,所以不只
支持被传递,还支持类型转换。只有在介绍了单向channel的概念后,读者才会明白类型转换对于
channel的意义:就是在单向channel和双向channel之间进行转换。示例以下:
ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取channel
ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel
基于ch4,咱们经过类型转换初始化了两个单向channel:单向读的ch5和单向写的ch6。
为何要作这样的限制呢?从设计的角度考虑,全部的代码应该都遵循“最小权限原则”
,
从而避免不必地使用泛滥问题,
进而致使程序失控。
写过C++程序的读者确定就会联想起const
指针的用法。非const指针具有const指针的全部功能,将一个指针设定为const就是明确告诉
函数实现者不要试图对该指针进行修改。单向channel也是起到这样的一种契约做用。
下面咱们来看一下单向channel的用法:
func Parse(ch <-chan int) {
for value := range ch {
fmt.Println("Parsing value", value)
}
}
除非这个函数的实现者无耻地使用了类型转换,不然这个函数就不会由于各类缘由而对ch
进行写,避免在ch中出现非指望的数据,从而很好地实践最小权限原则。
4.5.7 关闭channel
关闭channel很是简单,直接使用Go语言内置的close()函数便可:
close(ch)
在介绍了如何关闭channel以后,咱们就多了一个问题:如何判断一个channel是否已经被关
闭?咱们能够在读取的时候使用多重返回值的方式:
x, ok := <-ch
这个用法与map中的按键获取value的过程比较相似,只须要看第二个bool返回值便可,如
果返回值是false则表示ch已经被关闭。
4.6
多核并行化
4.7
出让时间片
咱们能够在每一个goroutine中控制什么时候主动出让时间片给其余goroutine,这可使用runtime
包中的Gosched()函数实现。
实际上,若是要比较精细地控制goroutine的行为,就必须比较深刻地了解Go语言开发包中
runtime包所提供的具体功能。
4.8
同步
咱们以前喊过一句口号,倡导用通讯来共享数据,而不是经过共享数据来进行通讯,但考虑到即便成功地用channel来做为通讯手段,
仍是避免不了多个goroutine之间共享数据的问题,Go 语言的设计者虽然对channel有极高的指望,但也提供了妥善的资源锁方案。
4.8.1 同步锁
Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RWMutex。Mutex是最简单
的一种锁类型,同时也比较暴力,当一个goroutine得到了Mutex后,其余goroutine就只能乖乖等
到这个goroutine释放该Mutex。RWMutex相对友好些,是经典的单写多读模型。在读锁占用的情
况下,会阻止写,但不阻止读,也就是多个goroutine可同时获取读锁(调用RLock()方法;而写
锁
(调用Lock()方法)
会阻止任何其余goroutine
(不管读和写)
进来,
整个锁至关于由该goroutine
独占。从RWMutex的实现看,RWMutex类型其实组合了Mutex:
type RWMutex struct {
w Mutex
writerSem uint32
readerSem
uint32
readerCount int32
readerWait int32
}
4.8.2 全局惟一性操做
对于从全局的角度只须要运行一次的代码,好比全局初始化操做,Go语言提供了一个Once
类型来保证全局的惟一性操做,具体代码以下:
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
若是这段代码没有引入Once,
setup()将会被每个goroutine先调用一次,
这至少对于这个
例子是多余的。在现实中,咱们也常常会遇到这样的状况。Go语言标准库为咱们引入了Once类
型以解决这个问题。 once 的 Do()方法能够保证在全局范围内只调用指定的函数一次(这里指
setup() 函数)
,并且全部其余goroutine在调用到此语句时,将会先被阻塞,直至全局惟一的
once.Do()调用结束后才继续。
这个机制比较轻巧地解决了使用其余语言时开发者不得不自行设计和实现这种Once效果的
难题,也是Go语言为并发性编程作了尽可能多考虑的一种体现。
第五章:
网络编程
Go语言标准库里提供的net包,支持基
于IP层、TCP/UDP层及更高层面(如HTTP、FTP、SMTP)的网络操做,其中用于IP层的称为Raw
Socket。
Go语言标准库对此过程进行了抽象和封装。不管咱们指望使用什么协议创建什么形式的连
接,都只须要调用net.Dial()便可。
5.1.1 Dial()函数
Dial()函数的原型以下:
func Dial(net, addr string) (Conn, error)
其中net参数是网络协议的名字,addr参数是IP地址或域名,而端口号以“:”的形式跟随在地址
或域名的后面,端口号可选。若是链接成功,返回链接对象,不然返回error。
咱们来看一下几种常见协议的调用方式。
TCP连接:
conn, err := net.Dial("tcp", "192.168.0.10:2100")
UDP连接:
conn, err := net.Dial("udp", "192.168.0.12:975")
ICMP连接(使用协议名称):
conn, err := net.Dial("ip4:icmp", "www.baidu.com")
ICMP连接(使用协议编号)
:
conn, err := net.Dial("ip4:1", "10.0.0.3")
5.1.4 更丰富的网络通讯
实际上,Dial()函数是对DialTCP()、DialUDP()、DialIP()和DialUnix()的封装。我
们也能够直接调用这些函数,它们的功能是一致的。这些函数的原型以下:
func DialTCP(net string, laddr, raddr *TCPAddr) (c *TCPConn, err error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err error)
func DialIP(netProto string, laddr, raddr *IPAddr) (*IPConn, error)
func DialUnix(net string, laddr, raddr *UnixAddr) (c *UnixConn, err error)
代码清单5-3 simplehttp2.go
package main
import (
"net"
"os"
"fmt"
"io/ioutil"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
result, err := ioutil.ReadAll(conn)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
与以前使用Dail()的例子相比,这里有两个不一样:
net.ResolveTCPAddr(),用于解析地址和端口号;
net.DialTCP(),用于创建连接。
这两个函数在Dial()中都获得了封装。
此外,net包中还包含了一系列的工具函数,合理地使用这些函数能够更好地保障程序的
质量。
验证IP地址有效性的代码以下:
func net.ParseIP()
建立子网掩码的代码以下:
func IPv4Mask(a, b, c, d byte) IPMask
获取默认子网掩码的代码以下:
func (ip IP) DefaultMask() IPMask
根据域名查找IP的代码以下:
func ResolveIPAddr(net, addr string) (*IPAddr, error)
func LookupHost(name string) (cname string, addrs []string, err error);
5.2
HTTP 编程
HTTP(HyperText Transfer Protocol,超文本传输协议)是互联网上应用最为普遍的一种网络
协议,定义了客户端和服务端之间请求与响应的传输标准。
Go语言标准库内建提供了 net/http 包,涵盖了HTTP客户端和服务端的具体实现。使用
net/http包,咱们能够很方便地编写HTTP客户端或服务端的程序。
阅读本节内容,读者须要具有以下知识点:
了解 HTTP 基础知识
了解 Go 语言中接口的用法
5.2.1 HTTP客户端
Go内置的net/http包提供了最简洁的HTTP客户端实现,咱们无需借助第三方网络通讯库
(好比libcurl)就能够直接使用HTTP中用得最多的GET和POST方式请求数据。
1. 基本方法
net/http包的Client类型提供了以下几个方法,让咱们能够用最简洁的方式实现 HTTP
请求:
func (c *Client)
func (c *Client)
error)
func (c *Client)
func (c *Client)
func (c *Client)
Get(url string) (r *Response, err error)
Post(url string, bodyType string, body io.Reader) (r *Response, err
PostForm(url string, data url.Values) (r *Response, err error)
Head(url string) (r *Response, err error)
Do(req *Request) (resp *Response, err error)
下面概要介绍这几个方法。
http.Get()
要请求一个资源,只需调用http.Get()方法(等价于http.DefaultClient.Get())即
可,示例代码以下:
resp, err := http.Get("http://example.com/")
if err != nil {
// 处理错误 ...
return
}
defer resp.Body.close()
io.Copy(os.Stdout, resp.Body)
上面这段代码请求一个网站首页,并将其网页内容打印到标准输出流中。
http.Post()
要以POST的方式发送数据,也很简单,只需调用http.Post()方法并依次传递下面的3个
参数便可:
请求的目标 URL
将要 POST 数据的资源类型(MIMEType)
数据的比特流([]byte形式)
下面的示例代码演示了如何上传一张图片:
resp, err := http.Post("http://example.com/upload", "image/jpeg", &imageDataBuf)
if err != nil {
// 处理错误
return
}
if resp.StatusCode != http.StatusOK {
// 处理错误
return
}
// ...
http.PostForm()
http.PostForm() 方法实现了标准编码格式为 application/x-www-form-urlencoded
的表单提交。下面的示例代码模拟HTML表单提交一篇新文章:
resp, err := http.PostForm("http://example.com/posts",
{"article title"}, "content": {"article body"}})
if err != nil {
// 处理错误
return
}
// ...
http.Head()
HTTP 中的 Head 请求方式代表只请求目标 URL 的头部信息,即 HTTP Header 而不返回 HTTP
Body。Go 内置的 net/http 包一样也提供了 http.Head() 方法,该方法同 http.Get() 方法一
样,只需传入目标 URL 一个参数便可。下面的示例代码请求一个网站首页的 HTTP Header信息:
resp, err := http.Head("http://example.com/")
(*http.Client).Do()
在多数状况下,http.Get()和http.PostForm() 就能够知足需求,可是若是咱们发起的
HTTP 请求须要更多的定制信息,咱们但愿设定一些自定义的 Http Header 字段,好比:
设定自定义的"User-Agent",而不是默认的 "Go http package"
传递 Cookie
此时可使用net/http包http.Client对象的Do()方法来实现:
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("User-Agent", "Gobook Custom User-Agent")
// ...
client := &http.Client{ //... }
resp, err := client.Do(req)
// ...
2. 高级封装
除了以前介绍的基本HTTP操做,Go语言标准库也暴露了比较底层的HTTP相关库,让开发
者能够基于这些库灵活定制HTTP服务器和使用HTTP服务。
自定义http.Client
前面咱们使用的http.Get()、http.Post()、http.PostForm()和http.Head()方法其
实都是在http.DefaultClient的基础上进行调用的,好比http.Get()等价于http.Default-
Client.Get(),依次类推。
http.DefaultClient 在字面上就向咱们传达了一个信息,既然存在默认的 Client,那么
HTTP Client 大概是能够自定义的。实际上确实如此,在net/http包中,的确提供了Client类
型。让咱们来看一看http.Client类型的结构:
type Client struct {
// Transport用于肯定HTTP请求的建立机制。
// 若是为空,将会使用DefaultTransport
Transport RoundTripper
// CheckRedirect定义重定向策略。
// 若是CheckRedirect不为空,客户端将在跟踪HTTP重定向前调用该函数。
// 两个参数req和via分别为即将发起的请求和已经发起的全部请求,最先的
// 已发起请求在最前面。
// 若是CheckRedirect返回错误,客户端将直接返回错误,不会再发起该请求。
// 若是CheckRedirect为空,Client将采用一种确认策略,将在10个连续
// 请求后终止
CheckRedirect func(req *Request, via []*Request) error
// 若是Jar为空,Cookie将不会在请求中发送,并会
// 在响应中被忽略
Jar CookieJar
}
在Go语言标准库中,http.Client类型包含了3个公开数据成员:
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
其中 Transport 类型必须实现 http.RoundTripper 接口。 Transport 指定了执行一个
HTTP 请求的运行机制,假若不指定具体的Transport,默认会使用http.DefaultTransport,
这意味着http.Transport也是能够自定义的。net/http包中的http.Transport类型实现了
http.RoundTripper接口。
CheckRedirect函数指定处理重定向的策略。当使用 HTTP Client 的Get()或者是Head()
方法发送 HTTP 请求时,若响应返回的状态码为 30x (好比 301 / 302 / 303 / 307)
,HTTP Client 会在遵循跳转规则以前先调用这个CheckRedirect函数。
Jar可用于在 HTTP Client 中设定 Cookie,Jar的类型必须实现了 http.CookieJar 接口,
该接口预约义了 SetCookies()和Cookies()两个方法。若是 HTTP Client 中没有设定 Jar,
Cookie将被忽略而不会发送到客户端。实际上,咱们通常都用 http.SetCookie() 方法来设定
Cookie。
使用自定义的http.Client及其Do()方法,咱们能够很是灵活地控制 HTTP 请求,好比发
送自定义 HTTP Header 或是改写重定向策略等。建立自定义的 HTTP Client 很是简单,具体代码
以下:
client := &http.Client {
CheckRedirect: redirectPolicyFunc,
}
resp, err := client.Get("http://example.com")
// ...
req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("User-Agent", "Our Custom User-Agent")
req.Header.Add("If-None-Match", `W/"TheFileEtag"`)
resp, err := client.Do(req)
// ...
自定义 http.Transport
在http.Client 类型的结构定义中,
咱们看到的第一个数据成员就是一个 http.Transport
对象,该对象指定执行一个 HTTP 请求时的运行规则。下面咱们来看看 http.Transport 类型
的具体结构:
type Transport struct {
// Proxy指定用于针对特定请求返回代理的函数。
// 若是该函数返回一个非空的错误,请求将终止并返回该错误。
// 若是Proxy为空或者返回一个空的URL指针,将不使用代理
Proxy func(*Request) (*url.URL, error)
// Dial指定用于建立TCP链接的dail()函数。
// 若是Dial为空,将默认使用net.Dial()函数
Dial func(net, addr string) (c net.Conn, err error)
// TLSClientConfig指定用于tls.Client的TLS配置。
// 若是为空则使用默认配置
TLSClientConfig *tls.Config
DisableKeepAlives bool
DisableCompression bool
// 若是MaxIdleConnsPerHost为非零值,它用于控制每一个host所须要
// 保持的最大空闲链接数。若是该值为空,则使用DefaultMaxIdleConnsPerHost
MaxIdleConnsPerHost int
// ...
}
在上面的代码中,咱们定义了 http.Transport 类型中的公开数据成员,下面详细说明其
中的各行代码。
Proxy func(*Request) (*url.URL, error)
Proxy 指定了一个代理方法,该方法接受一个 *Request 类型的请求实例做为参数并返回
一个最终的 HTTP 代理。若是 Proxy 未指定或者返回的 *URL 为零值,将不会有代理被启用。
Dial func(net, addr string) (c net.Conn, err error)
Dial 指定具体的dial()方法来建立 TCP 链接。若是不指定,默认将使用 net.Dial() 方法。
TLSClientConfig *tls.Config
SSL链接专用,TLSClientConfig 指定 tls.Client 所用的 TLS 配置信息,若是不指定,
也会使用默认的配置。
DisableKeepAlives bool
是否取消长链接,默认值为 false,即启用长链接。
DisableCompression bool
是否取消压缩(GZip),默认值为 false,即启用压缩。
MaxIdleConnsPerHost int
指定与每一个请求的目标主机之间的最大非活跃链接(keep-alive)数量。若是不指定,默认使
用 DefaultMaxIdleConnsPerHost 的常量值。
除了 http.Transport 类型中定义的公开数据成员之外,它同时还提供了几个公开的成员
方法。
func(t *Transport) CloseIdleConnections() 。该方法用于关闭全部非活跃的链接。
func(t *Transport) RegisterProtocol(scheme string, rt RoundTripper)。
该方法可用于注册并启用一个新的传输协议,好比 WebSocket 的传输协议标准(ws),或者 FTP、File 协议等。
func(t *Transport) RoundTrip(req *Request) (resp *Response, err error)。
用于实现 http.RoundTripper 接口。
自定义http.Transport也很简单,以下列代码所示:
tr := &http.Transport{
TLSClientConfig:
&tls.Config{RootCAs: pool},
DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
Client和Transport在执行多个 goroutine 的并发过程当中都是安全的,但出于性能考虑,应
当建立一次后反复使用。
灵活的 http.RoundTripper 接口
在前面的两小节中,咱们知道 HTTP Client 是能够自定义的,而 http.Client 定义的第一
个 公 开 成 员 就 是 一 个 http.Transport 类 型 的 实 例 , 且 该 成 员 所 对 应 的 类 型 必 须 实 现
http.RoundTripper 接口。下面咱们来看看 http.RoundTripper 接口的具体定义:
type RoundTripper interface {
// RoundTrip执行一个单一的HTTP事务,返回相应的响应信息。
// RoundTrip函数的实现不该试图去理解响应的内容。若是RoundTrip获得一个响应,
// 不管该响应的HTTP状态码如何,都应将返回的err设置为nil。非空的err
// 只意味着没有成功获取到响应。
// 相似地,RoundTrip也不该试图处理更高级别的协议,好比重定向、认证和
// Cookie等。
//
// RoundTrip不该修改请求内容, 除非了是为了理解Body内容。每个请求
// 的URL和Header域都应被正确初始化
RoundTrip(*Request) (*Response, error)
}
从上述代码中能够看到,http.RoundTripper接口很简单,只定义了一个名为RoundTrip
的方法。任何实现了 RoundTrip() 方法的类型便可实现http.RoundTripper接口。前面咱们
看到的http.Transport类型正是实现了 RoundTrip() 方法继而实现了该接口。
http.RoundTripper 接口定义的 RoundTrip() 方法用于执行一个独立的 HTTP 事务,接
受传入的 \*Request 请求值做为参数并返回对应的 \*Response 响应值,以及一个 error 值。
在实现具体的 RoundTrip() 方法时,不该该试图在该函数里边解析 HTTP 响应信息。若响应成
功,error 的值必须为nil,而与返回的 HTTP 状态码无关。若不能成功获得服务端的响应,
error必须为非零值。相似地,也不该该试图在 RoundTrip() 中处理协议层面的相关细节,好比重定
向、认证或是 cookie 等。
非必要状况下,不该该在 RoundTrip() 中改写传入的请求体(\*Request)
,请求体的内容(好比 URL 和 Header 等)必须在传入 RoundTrip() 以前就已组织好并完成初始化。
一般,咱们能够在默认的 http.Transport 之上包一层 Transport 并实现 RoundTrip()方法
设计优雅的 HTTP Client
综上示例讲解能够看到,Go语言标准库提供的 HTTP Client 是至关优雅的。一方面提供了极
其简单的使用方式,另外一方面又具有极大的灵活性。
Go语言标准库提供的HTTP Client 被设计成上下两层结构。
一层是上述提到的 http.Client类及其封装的基础方法,咱们不妨将其称为“业务层”
。之因此称为业务层,是由于调用方一般只须要关心请求的业务逻辑自己,而无需关心非业务相关的技术细节,这些细节包括:
HTTP 底层传输细节
HTTP 代理
gzip 压缩
链接池及其管理
认证(SSL或其余认证方式)
之 所 以 HTTP Client 可 以 作 到 这 么 好 的 封 装 性 , 是 因 为 HTTP Client 在 底 层 抽 象 了
http.RoundTripper 接口,而http.Transport 实现了该接口,从而可以处理更多的细节,我
们不妨将其称为“传输层”。HTTP Client 在业务层初始化 HTTP Method、目标URL、请求参数、
请求内容等重要信息后,通过“传输层”“传输层”在业务层处理的基础上补充其余细节,而后,
再发起 HTTP 请求,接收服务端返回的 HTTP 响应。
5.2.2 HTTP服务端
本节咱们将介绍HTTP服务端技术,包括如何处理HTTP请求和HTTPS请求。
1. 处理HTTP请求
使用 net/http 包提供的 http.ListenAndServe() 方法,能够在指定的地址进行监听,
开启一个HTTP,服务端该方法的原型以下:
func ListenAndServe(addr string, handler Handler) error
该方法用于在指定的 TCP 网络地址 addr 进行监听,而后调用服务端处理程序来处理传入的连
接请求。该方法有两个参数:第一个参数 addr 即监听地址;第二个参数表示服务端处理程序,
一般为空,这意味着服务端调用 http.DefaultServeMux 进行处理,而服务端编写的业务逻
辑处理程序 http.Handle() 或 http.HandleFunc() 默认注入 http.DefaultServeMux 中,
具体代码以下:
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServe(":8080", nil))
若是想更多地控制服务端的行为,能够自定义 http.Server,代码以下:
s := &http.Server{
Addr:
":8080",
Handler:
myHandler,
ReadTimeout:
10 * time.Second,
WriteTimeout:
10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
2. 处理HTTPS请求
net/http 包还提供 http.ListenAndServeTLS() 方法,用于处理 HTTPS 链接请求:
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler)
error
ListenAndServeTLS() 和 ListenAndServe()的行为一致,
区别在于只处理HTTPS请求。此外,服务器上必须存在包含证书和与之匹配的私钥的相关文件,好比certFile对应SSL证书
文件存放路径,keyFile对应证书私钥文件路径。若是证书是由证书颁发机构签署的,certFile
参数指定的路径必须是存放在服务器上的经由CA认证过的SSL证书。
开启 SSL 监听服务也很简单,以下列代码所示:
http.Handle("/foo", fooHandler)
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
log.Fatal(http.ListenAndServeTLS(":10443", "cert.pem", "key.pem", nil))
或者是:
ss := &http.Server{
Addr:":10443",
Handler:myHandler,
ReadTimeout:10 * time.Second,
WriteTimeout:10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(ss.ListenAndServeTLS("cert.pem", "key.pem"))
5.3
RPC 编程
RPC(Remote Procedure Call,远程过程调用)是一种经过网络从远程计算机程序上请求服
务,而不须要了解底层网络细节的应用程序通讯协议。RPC协议构建于TCP或UDP,或者是 HTTP
之上,容许开发者直接调用另外一台计算机上的程序,而开发者无需额外地为这个调用过程编写网
络通讯相关代码,使得开发包括网络分布式程序在内的应用程序更加容易。
RPC 采用客户端—服务器(Client/Server)的工做模式。请求程序就是一个客户端(Client)
,而服务提供程序就是一个服务器(Server)。当执行一个远程过程调用时,客户端程序首先发送一
个带有参数的调用信息到服务端,而后等待服务端响应。在服务端,服务进程保持睡眠状态直到
客户端的调用信息到达为止。当一个调用信息到达时,服务端得到进程参数,计算出结果,并向
客户端发送应答信息,而后等待下一个调用。最后,客户端接收来自服务端的应答信息,得到进
程结果,而后调用执行并继续进行。
net/rpc包容许 RPC 客户端程序经过网络或是其余 I/O 链接调用一个远端对象的公开方法
(必须是大写字母开头、可外部调用的)。在 RPC 服务端,可将一个对象注册为可访问的服务,
以后该对象的公开方法就可以以远程的方式提供访问。一个 RPC 服务端能够注册多个不一样类型
的对象,但不容许注册同一类型的多个对象。
一个对象中只有知足以下这些条件的方法,才能被 RPC 服务端设置为可供远程访问:
必须是在对象外部可公开调用的方法(首字母大写)
;
必须有两个参数,且参数的类型都必须是包外部能够访问的类型或者是Go内建支持的类
型;
第二个参数必须是一个指针;
方法必须返回一个error类型的值。
以上4个条件,能够简单地用以下一行代码表示:
func (t *T) MethodName(argType T1, replyType *T2) error
在上面这行代码中,类型T、T1 和 T2 默认会使用 Go 内置的 encoding/gob 包进行编码解码。
关于encoding/gob 包的内容,稍后咱们将会对其进行介绍。
该方法(MethodName)的第一个参数表示由 RPC 客户端传入的参数,第二个参数表示要返
回给RPC客户端的结果,该方法最后返回一个 error 类型的值。
RPC 服务端能够经过调用 rpc.ServeConn 处理单个链接请求。多数状况下,经过 TCP 或
是 HTTP 在某个网络地址上进行监听来建立该服务是个不错的选择。
在 RPC 客户端,Go 的 net/rpc 包提供了便利的 rpc.Dial() 和 rpc.DialHTTP() 方法
来与指定的 RPC 服务端创建链接。在创建链接以后,Go 的 net/rpc 包容许咱们使用同步或者
异步的方式接收 RPC 服务端的处理结果。调用 RPC 客户端的 Call() 方法则进行同步处理,这
时候客户端程序按顺序执行,只有接收完 RPC 服务端的处理结果以后才能够继续执行后面的程
序。当调用 RPC 客户端的 Go() 方法时,则能够进行异步处理,RPC 客户端程序无需等待服务
端的结果便可执行后面的程序,而当接收到 RPC 服务端的处理结果时,再对其进行相应的处理。
不管是调用 RPC 客户端的 Call() 或者是 Go() 方法,都必须指定要调用的服务及其方法名称,
以及一个客户端传入参数的引用,还有一个用于接收处理结果参数的指针。
若是没有明确指定 RPC 传输过程当中使用何种编码解码器,默认将使用 Go 标准库提供的
encoding/gob 包进行数据传输。
5.3.2 Gob简介
Gob 是 Go 的一个序列化数据结构的编码解码工具,在 Go 标准库中内置encoding/gob包
以供使用。一个数据结构使用 Gob 进行序列化以后,可以用于网络传输。与 JSON 或 XML 这种
基于文本描述的数据交换语言不一样,Gob 是二进制编码的数据流,而且 Gob 流是能够自解释的,
它在保证高效率的同时,也具有完整的表达能力。
做为针对 Go 的数据结构进行编码和解码的专用序列化方法,这意味着 Gob 没法跨语言使
用。在 Go 的net/rpc包中,传输数据所须要用到的编码解码器,默认就是 Gob。因为 Gob 仅局
限于使用 Go 语言开发的程序,这意味着咱们只能用 Go 的 RPC 实现进程间通讯。然而,大多数
时候,咱们用 Go 编写的 RPC 服务端(或客户端),可能更但愿它是通用的,与语言无关的,无
论是Python 、 Java 或其余编程语言实现的 RPC 客户端,都可与之通讯。
5.3.3 设计优雅的RPC接口
Go 的net/rpc很灵活,它在数据传输先后实现了编码解码器的接口定义。这意味着,开发
者能够自定义数据的传输方式以及 RPC 服务端和客户端之间的交互行为。
RPC 提供的编码解码器接口以下:
type ClientCodec interface {
WriteRequest(*Request, interface{}) error
ReadResponseHeader(*Response) error
ReadResponseBody(interface{}) error
Close() error
}
type ServerCodec interface {
ReadRequestHeader(*Request) error
ReadRequestBody(interface{}) error
WriteResponse(*Response, interface{}) error
Close() error
}
接口ClientCodec定义了 RPC 客户端如何在一个 RPC 会话中发送请求和读取响应。
客户端程
序经过 WriteRequest() 方法将一个请求写入到 RPC 链接中,并经过 ReadResponseHeader()
和 ReadResponseBody() 读取服务端的响应信息。当整个过程执行完毕后,再经过 Close() 方
法来关闭该链接。
接口ServerCodec定义了 RPC 服务端如何在一个 RPC 会话中接收请求并发送响应。服务端
程序经过 ReadRequestHeader() 和 ReadRequestBody() 方法从一个 RPC 链接中读取请求
信息,而后再经过 WriteResponse() 方法向该链接中的 RPC 客户端发送响应。当完成该过程
后,经过 Close() 方法来关闭链接。
经过实现上述接口,咱们能够自定义数据传输先后的编码解码方式,而不只仅局限于 Gob。
一样,能够自定义RPC 服务端和客户端的交互行为。实际上,Go 标准库提供的net/rpc/json
包,就是一套实现了rpc.ClientCodec和rpc.ServerCodec接口的 JSON-RPC 模块。
5.4
JSON 处理
Go语言内建对JSON的支持。使用Go语言内置的encoding/json 标准库,开发者能够轻松
使用Go程序生成和解析JSON格式的数据。在Go语言实现JSON的编码和解码时,遵循RFC4627
协议标准。
5.4.1 编码为JSON格式
使用json.Marshal()函数能够对一组数据进行JSON格式的编码。json.Marshal()函数
的声明以下:
func Marshal(v interface{}) ([]byte, error)
假若有以下一个Book类型的结构体:
type Book struct {
Title string
Authors []string
Publisher string
IsPublished bool
Price float
}
而且有以下一个 Book 类型的实例对象:
gobook := Book{
"Go语言编程",
["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"ituring.com.cn",
true,
9.99
}
而后,咱们可使用 json.Marshal() 函数将gobook实例生成一段JSON格式的文本:
b, err := json.Marshal(gobook)
若是编码成功,err 将赋于零值 nil,变量b 将会是一个进行JSON格式化以后的[]byte
类型:
b == []byte(`{
"Title": "Go语言编程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99
}`)
当咱们调用json.Marshal(gobook)语句时,会递归遍历gobook对象,若是发现gobook这个
数据结构实现了json.Marshaler接口且包含有效的值,Marshal()就会调用其MarshalJSON()
方法将该数据结构生成 JSON 格式的文本。Go语言的大多数数据类型均可以转化为有效的JSON文本,
但channel、complex和函数这几种类型除外。
若是转化前的数据结构中出现指针,那么将会转化指针所指向的值,若是指针指向的是零值,
那么null将做为转化后的结果输出。
在Go中,JSON转化先后的数据类型映射以下。
布尔值转化为JSON后仍是布尔类型。
浮点数和整型会被转化为JSON里边的常规数字。
字符串将以UTF-8编码转化输出为Unicode字符集的字符串,
特殊字符好比<将会被转义为
\u003c。
数组和切片会转化为JSON里边的数组,但[]byte类型的值将会被转化为 Base64 编码后
的字符串,slice类型的零值会被转化为 null。
结构体会转化为JSON对象,而且只有结构体里边以大写字母开头的可被导出的字段才会
被转化输出,而这些可导出的字段会做为JSON对象的字符串索引。
转 化一个 map 类 型 的数据 结构时 ,该 数据的 类型 必须是 map[string]T ( T 可 以是
encoding/json 包支持的任意数据类型)。
5.4.2 解码JSON数据
可使用 json.Unmarshal() 函数将JSON格式的文本解码为Go里边预期的数据结构。
json.Unmarshal()函数的原型以下:
func Unmarshal(data []byte, v interface{}) error
该函数的第一个参数是输入,即JSON格式的文本(比特序列),第二个参数表示目标输出容器,用于存放解码后的值。
要解码一段JSON数据,首先须要在Go中建立一个目标类型的实例对象,用于存放解码后的值:
var book Book
而后将调用 json.Unmarshal() 函数, []byte 类型的JSON数据做为第一个参数传入, book
实例变量的指针做为第二个参数传入:
若是 b 是一个有效的JSON数据并能和 book 结构对应起来,那么JSON解码后的值将会一一
存放到book结构体中。解码成功后的 book 数据以下:
book := Book{
"Go语言编程",
["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"ituring.com.cn",
true,
9.99
}
json.Unmarshal()函数会根据一个约定的顺序查找目标结构中的字段,
若是找到一个即发生匹配。
假设一个JSON对象有个名为"Foo"的索引,
要将"Foo"所对应的值填充到目标
结构体的目标字段上,json.Unmarshal()将会遵循以下顺序进行查找匹配:
(1) 一个包含Foo标签的字段;
(2) 一个名为Foo的字段;
(3) 一个名为 Foo 或者 Foo 或者除了首字母其余字母不区分大小写的名为 Foo 的字段。
这些字段在类型声明中必须都是以大写字母开头、可被导出的字段。
可是当JSON数据里边的结构和Go里边的目标类型的结构对不上时,会发生什么呢?示例代
码以下:
b := []byte(`{"Title": "Go语言编程", "Sales": 1000000}`)
var gobook Book
err := json.Unmarshal(b, &gobook)
若是JSON中的字段在Go目标类型中不存在,json.Unmarshal()函数在解码过程当中会丢弃
该字段。在上面的示例代码中,因为Sales字段并无在Book类型中定义,因此会被忽略,只有
Title这个字段的值才会被填充到gobook.Title中。
这个特性让咱们能够从同一段JSON数据中筛选指定的值填充到多个Go语言类型中。固然,
前提是已知JSON数据的字段结构。这也一样意味着,目标类型中不可被导出的私有字段(非首
字母大写)将不会受到解码转化的影响。
但若是JSON的数据结构是未知的,应该如何处理呢?
5.4.3 解码未知结构的JSON数据
咱们已经知道,Go语言支持接口。在Go语言里,接口是一组预约义方法的组合,任何一个
类型都可经过实现接口预约义的方法来实现,且无需显示声明,因此没有任何方法的空接口能够
表明任何类型。换句话说,每个类型其实都至少实现了一个空接口。
Go内建这样灵活的类型系统,向咱们传达了一个颇有价值的信息:空接口是通用类型。如
果要解码一段未知结构的JSON,
只需将这段JSON数据解码输出到一个空接口便可。
在解码JSON数据的过程当中,JSON数据里边的元素类型将作以下转换:
JSON中的布尔值将会转换为Go中的bool类型;
数值会被转换为Go中的float64类型;
字符串转换后仍是string类型;
JSON数组会转换为[]interface{}类型;
JSON对象会转换为map[string]interface{}类型;
null值会转换为nil。
在Go的标准库encoding/json包中,容许使用map[string]interface{}和[]interface{}
类型的值来分别存放未知结构的JSON对象或数组,示例代码以下:
b := []byte(`{
"Title": "Go语言编程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99,
"Sales": 1000000
}`)
var r interface{}
err := json.Unmarshal(b, &r)
在上述代码中,r被定义为一个空接口。json.Unmarshal() 函数将一个JSON对象解码到
空接口r中,最终r将会是一个键值对的 map[string]interface{} 结构:
map[string]interface{}{
"Title": "Go语言编程",
"Authors": ["XuShiwei", "HughLv", "Pandaman", "GuaguaSong", "HanTuo", "BertYuan",
"XuDaoli"],
"Publisher": "ituring.com.cn",
"IsPublished": true,
"Price": 9.99,
"Sales": 1000000
}
要访问解码后的数据结构,须要先判断目标结构是否为预期的数据类型:
gobook, ok := r.(map[string]interface{})
而后,咱们能够经过for循环搭配range语句一一访问解码后的目标数据:
if ok {
for k, v := range gobook {
switch v2 := v.(type) {
case string:
..........
虽然有些烦琐,但的确是一种解码未知结构的JSON数据的安全方式。
5.4.4 JSON的流式读写
Go内建的encoding/json 包还提供Decoder和Encoder两个类型,用于支持JSON数据的
流式读写,并提供NewDecoder()和NewEncoder()两个函数来便于具体实现:
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
代码清单5-6从标准输入流中读取JSON数据,而后将其解码,但只保留Title字段(书名)
,
再写入到标准输出流中。
代码清单5-6 jsondemo.go
package main
import (
"encoding/json"
"log"
"os"
)
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 != "Title" {
v[k] = nil, false
}
}
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
}
使用Decoder 和Encoder对数据流进行处理能够应用得更为普遍些,好比读写 HTTP 链接、
WebSocket或文件等,Go的标准库net/rpc/jsonrpc就是一个应用了Decoder和Encoder的实际例子。
5.5
网站开发
在这一节中,咱们将向你按部就班地讲解怎样使用Go进行Web开发。本节将围绕一个简单的
相册程序进行,尽管示例程序比较简单,但体现的都是使用Go开发网站的几处关键环节,旨在
让你系统了解基于原生的Go语言开发Web应用程序的基本思路及其相关细节的具体实现。
进阶话题
9.1
反射
反射(reflection)是在Java出现后迅速流行起来的一种概念。经过反射,你能够获取丰富的
类型信息,并能够利用这些类型信息作很是灵活的工做。
在Java中,你能够读取配置并根据类型名称建立对应的类型,这是一种常见的编程手法。
Java中的不少重要框架和技术(好比Spring/IoC、Hibernate、Struts)等都严重依赖于反射功能。虽然
时,使用Java EE时不少人都以为很麻烦,好比须要配置大量XML格式的配置程序,但这毕竟不
是反射的错,反而更加说明了反射所带来的高可配置性。
大多数现代的高级语言都以各类形式支持反射功能,除了一切以性能为上的C++语言。Go
语言的反射实现了反射的大部分功能,但没有像Java语言那样内置类型工厂,
故而没法作到像Java那样经过类型字符串建立对象实例。
反射是把双刃剑,功能强大但代码可读性并不理想。若非必要,咱们并不推荐使用反射,这
也是咱们把反射放到进阶话题来介绍的缘由。
9.1.1 基本概念
Go语言中的反射与其余语言有比较大的不一样。首先咱们要理解两个基本概念—— Type 和
Value,它们也是Go语言包中reflect空间里最重要的两个类型。咱们先看一下下面的定义:
type MyReader struct {
Name string
}
func (r MyReader)Read(p []byte) (n int, err error) {
// 实现本身的Read方法
}
由于MyReader类型实现了io.Reader接口的全部方法(其实就是一个Read()函数)
,因此MyReader实现了接口io.Reader。咱们能够按以下方式来进行MyReader的实例化和赋值:
var reader io.Reader
reader = &MyReader{"a.txt"}
如今咱们能够再来解释一下什么是Type,什么是Value。对全部接口进行反射,均可以获得一个包含Type和Value的信息结构。好比咱们对上例的
reader进行反射,也将获得一个Type和Value,Type为io.Reader,Value为MyReader{"a.txt"}。
顾名思义,Type主要表达的是被反射的这个变量自己的类型信息, Value则为该变量实例自己而的信息。
9.1.2 基本用法
经过使用Type和Value,咱们能够对一个类型进行各项灵活的操做。接下来咱们分别演示反射的几种最基本用途。
1. 获取类型信息
package main
import (
"fmt"
"reflect"
)
func main() {
var x float64 = 3.4
fmt.Println("type:", reflect.TypeOf(x))
}
以上代码将输出以下的结果:
type: float64
Type和Value都包含了大量的方法,其中第一个有用的方法应该是Kind,这个方法返回该
类型的具体信息:Uint、Float64等。Value类型还包含了一系列类型方法,好比Int(),用于
返回对应的值。查看如下示例:
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
结果为:
type: float64
kind is float64: true
value: 3.4
2. 获取值类型
类型Type中有一个成员函数CanSet(),
其返回值为bool类型。
若是你在注意到这个函数之
前就直接设置了值,颇有可能会收到一些看起来像异常的错误处理消息。
可能不少人会置疑为何要有这么个奇怪的函数,
能够设置全部的域不是很好吗?这里先解
释一下这个函数存在的缘由。
咱们在第2章中提到过Go语言中全部的类型都是值类型,即这些变量在传递给函数的时候将
发生一次复制。基于这个原则,咱们再次看一下下面的语句:
var x float64 = 3.4
v := reflect.ValueOf(x)
v.Set(4.1)
最后一条语句试图修改v的内容。是否能够成功地将x的值改成4.1呢?先要理清v和x的关系。在
调用ValueOf()的地方,须要注意到x将会产生一个副本,所以ValueOf()内部对x的操做其实
都是对着x的一个副本。假如v容许调用Set(),那么咱们也能够想象出,被修改的将是这个x的
副本,而不是x自己。若是容许这样的行为,那么执行结果将会很是困惑。调用明明成功了,为
什么x的值仍是原来的呢?为了解决这个问题Go语言,引入了可设属性这个概念(Settability)
。
若是 CanSet() 返回 false ,表示你不该该调用 Set() 和 SetXxx() 方法,不然会收到这样的
错误:
panic: reflect.Value.SetFloat using unaddressable value
如今咱们知道,有些场景下不能使用反射修改值,那么到底什么状况下能够修改的呢?其实
这仍是跟传值的道理相似。咱们知道,直接传递一个float到函数时,函数不能对外部的这个
float变量有任何影响,要想有影响的话,能够传入该float变量的指针。下面的示例小幅修改
了以前的例子,成功地用反射的方式修改了变量x的值:
var x float64 = 3.4
p := reflect.ValueOf(&x) // 注意:获得X的地址
fmt.Println("type of p:", p.Type())
fmt.Println("settability of p:" , p.CanSet())
v := p.Elem()
fmt.Println("settability of v:" , v.CanSet())
v.SetFloat(7.1)
fmt.Println(v.Interface())
fmt.Println(x)
184 func test_reflect(){
185 ▸ var x float64 =3.4
186 ▸ fmt.Println("type:",reflect.TypeOf(x))
187 ▸ v :=reflect.ValueOf(x)
188 ▸ fmt.Println("v type:",v.Type())
189 ▸ fmt.Println("kind of float64:",v.Kind()==reflect.Float64)
190 ▸ fmt.Println("value float:",v.Float())
191 ▸ p :=reflect.ValueOf(&x)
192 ▸ fmt.Println("p type:",p.Type())
193 ▸ fmt.Println("setability of p:",p.CanSet())
194 ▸ v1 :=p.Elem()
195 ▸ fmt.Println("setability of v1:",v1.CanSet())
196 ▸ v1.SetFloat(4.1)
197 ▸ fmt.Println(v1.Interface())
198 ▸ fmt.Println(x)
199 }
go run hello.go
type: float64
v type: float64
kind of float64: true
value float: 3.4
p type: *float64
setability of p: false
setability of v1: true
4.1
4.1
9.1.3 对结构的反射操做
以前讨论的都是简单类型的反射操做,如今咱们讨论一下结构的反射操做。下面的示例演示
了如何获取一个结构中全部成员的值:
type T struct {
A int
B string
}
t := T{203, "mh203"}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
以上例子的输出为:
0: A int = 203
1: B string = mh203
能够看出,对于结构的反射操做并无根本上的不一样,只是用了Field()方法来按索引获取
对应的成员。固然,在试图修改为员的值时,也须要注意可赋值属性
9.2 语言交互性
1 package main
2
3 import (
4 ▸ "fmt"
5 )
6 //#include <stdio.h>
7 //#include <stdlib.h>
8 import "C"
9
10 func Random() int {
11 ▸ return int(C.random())
12 }
13 func Seed(i int) {
14 ▸ C.srandom(C.uint(i))
15 }
16 func main() {
17 ▸ Seed(100)
18 ▸ fmt.Println("Random:",Random())
19 }
事实上,根本就不存在一个名为C的包。这个import语句其实就是一个信号,告诉Cgo它应
该开始工做了。作什么事情呢?就是对应这条import语句以前的块注释中的C源代码自动生成
包装性质的Go代码。
这时候咱们该注意到import语句前紧跟的注释了。这个注释的内容是有意义的,而不是传
统意义上的注释做用。这个例子里用的是一个块注释,实际上用行注释也是没问题的,只要是紧
贴在import语句以前便可。好比下面也是正确的Cgo写法:
// #include <stdio.h>
// #include <stdlib.h>
import "C"
9.2.1 类型映射
在跨语言交互中,比较复杂的问题有两个:类型映射以及跨越调用边界传递指针所带来的对
象生命周期和内存管理的问题。好比Go语言中的string类型须要跟C语言中的字符数组进行对
应,而且要保证映射到C语言的对象的生命周期足够长,以免在C语言执行过程当中该对象就已
经被垃圾回收。这一节咱们先谈类型映射的问题。
9.2.2 字符串映射
由于Go语言中有明确的string原生类型,而C语言中用字符数组表示,二者之间的转换是
一 个 必 须 考 虑 的 问 题 。 Cgo 提 供 了 一 系 列 函 数 来 提 供 支 持 : C.CString 、 C.GoString 和
C.GoStringN。须要注意的是,每次转换都将致使一次内存复制,所以字符串内容实际上是不可
修改的(实际上,Go语言的string也不容许对其中的内容进行修改
因为C.CString的内存管理方式与Go语言自身的内存管理方式不兼容,咱们设法期待Go语
言能够帮咱们作垃圾收集,所以在使用完后必须显式释放调用C.CString所生成的内存块,
不然将致使严重的内存泄露。结合咱们以前已经学过的defer用法,全部用到C.CString的代码大体
均可以写成以下的风格:
var gostr string
// ... 初始化gostr
cstr := C.CString(gostr)
defer C.free(unsafe.Pointer(cstr))
// 接下来大胆地使用cstr吧,由于保证能够被释放掉了
// C.sprintf(cstr, "content is: %d", 123)
9.2.3 C程序
在9.2节开头的示例中,块注释中只写了一条include语句,其实在这个块注释中,能够写
任意合法的C源代码,而Cgo都会进行相应的处理并生成对应的Go代码。代码清单9-3是一个稍微
复杂一些的例子。
package hello
/*
#include <stdio.h>
void hello() {
printf("Hello, Cgo! -- From C world.\n");
}
*/
import "C"
func Hello() int {
return int(C.hello())
}
这个块注释里就直接写了个C函数,它使用C标准库里的printf()打印了一句话。
还有另一个问题,那就是若是这里的C代码须要依赖一个非C标准库的第三方库,怎么办
呢?若是不解决的话必然会有连接时错误。Cgo提供了#cgo这样的伪C文法,让开发者有机会指
定依赖的第三方库和编译选项。
下面的例子示范了#cgo的第一种用法:
// #cgo CFLAGS: -DPNG_DEBUG=1
// #cgo linux CFLAGS: -DLINUX=1
// #cgo LDFLAGS: -lpng
// #include <png.h>
import "C"
这个例子示范了如何使用CFLAGS来传入编译选项,使用LDFLAGS来传入连接选项。#cgo还有另
外一种更简便一些的用法,以下所示:
// #cgo pkg-config: png cairo
// #include <png.h>
import "C"
9.2.5 编译Cgo
编译Cgo代码很是容易,咱们不须要作任何特殊的处理。Go安装后,会自带一个cgo命令行
工具,它用于处理全部带有Cgo代码的Go文件,生成Go语言版本的调用封装代码。而Go工具集
对cgo命令行工具再次进行了良好的封装,使构建过程可以自动识别和处理带有Cgo代码的Go源
代码文件,彻底不给用户增长额外的工做负担。
9.3 连接符号
。。。。
因为 Go 语言并没有重载,故此语言的“连接符号”由以下信息构成。
Package。Package 名能够是多层,例如A/B/C。
ClassType。 很特别的是,Go 语言中 ClassType 能够是指针,也能够不是。
Method。
其“连接符号”的组成规则以下:
Package.Method
Package.ClassType·Method
这样说可能比较抽象,下面举个实际的例子。假设在 qbox.us/mockfs 模块中,有以下几个
函数:
func New(cfg Config) *MockFS
func (fs *MockFS) Mkdir(dir string) (code int, err error)
func (fs MockFS) Foo(bar Bar)
它们的连接符号分别为:
qbox.us/mockfs.New
qbox.us/mockfs.*MockFS·Mkdir
qbox.us/mockfs.MockFS·Foo
9.4
goroutine 机理
。。。。。。
9.4.1 协程
协程,也有人称之为轻量级线程,具有如下几个特色。
可以在单一的系统线程中模拟多个任务的并发执行。
在一个特定的时间,只有一个任务在运行,即并不是真正地并行。
被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外
部没有办法停止它。要进行任务切换,只能经过由该任务自身调用yield()来主动出让
CPU使用权。
每一个协程都有本身的堆栈和局部变量。
每一个协程都包含3种运行状态:挂起、运行和中止。中止一般表示该协程已经执行完成(包
括遇到问题后明确退出执行的状况),挂起则表示该协程还没有执行完成,但出让了时间片,之后
有机会时会由调度器继续执行。
9.4.2 协程的C语言实现
为了更好地剖析协程的运行原理,咱们在本节中将引入Go语言的做者之一拉斯·考克斯
(Russ Cox)在Go语言出世以前所设计实现的一个轻量级协程库libtask,这个库的下载地址
为http://swtch.com/libtask/,读者能够自行到该页面下载完整的源代码。这个库的做者使用的是
很是开放的受权协议,所以读者能够随意修改和使用这些代码,但必须保持该份代码所附带的
版权声明。
虽然咱们没有具体地比对goroutine实现代码和libtask的直接关系,但咱们有足够充分的理
由相信goroutine和用于goroutine之间通讯的channel都是参照libtask库实现的(甚至可能直接使
用这个库)
。至于go关键字等Go语言特性,咱们均可以将其认为只是为了便于开发者使用而设计
的语法糖。
本节咱们将对这个代码库作一次结构化的阅读,并在必要的地方贴出一些关键的代码段。相
信读者在阅读完本节后,对于协程的原理会有比较全面的理解。理解了协程的概念,对于正确使
用Go语言的goroutine以及分析使用goroutine时遇到的各类问题都会大有帮助。
9.4.3 协程库概述
nn
var book Book而后调用 json.Unmarshal() 函数, []byte 类型的JSON数据做为第一个参数传入, book将将实例变量的指针做为第二个参数传入: