go语言入门

jingzhishen · · 5893 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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-stable

sudo apt-get install golang

或者直接下载go语言安装包

http://blog.csdn.net/liuhongwei123888/article/details/8512815

 

 

Let's go! (Ubuntu下搭建Go语言环境)

http://www.cnblogs.com/Liar/p/3551080.html

 

go 编程环境搭建及vim设置

http://blog.chinaunix.net/uid-24774106-id-3964461.html

 

golang的vim工作环境配置

http://my.oschina.net/goskyblue/blog/192647

 

windows环境搭建:

Go语言的开发环境配置的经验总结

http://blog.csdn.net/wolinxuebin/article/details/7724049

Go语言_eclipse环境搭建

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解码后的值将

有疑问加站长微信联系(非本文作者)

本文来自:博客园

感谢作者:jingzhishen

查看原文:go语言入门

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

5893 次点击  
加入收藏 微博
上一篇:golang 扫雷
下一篇:go语言中var
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传