go语言入门
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
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编译静态程序:
yingc@yingc:~/gcyin/study/go/test$ ll
total 580
drwxrwxr-x 2 yingc yingc 4096 7月 25 15:16 ./
drwxrwxr-x 3 yingc yingc 4096 7月 16 20:24 ../
-rw-rw-r-- 1 yingc yingc 101 7月 16 20:30 hello.go
-rwxrwxr-x 1 yingc yingc 577952 7月 25 15:16 main*
yingc@yingc:~/gcyin/study/go/test$ file main
main: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, stripped
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
将
将
实例变量的指针作为第二个参数传入:
go语言入门的更多相关文章
- 踢爆IT劣书出版黑幕——由清华大学出版社之《C语言入门很简单》想到的(1)
1.前言与作者 首先声明,我是由于非常偶然的机会获得<C语言入门很简单>这本书的,绝对不是买的.买这种书实在丢不起那人. 去年这书刚出版时,在CU论坛举行试读推广,我当时随口说了几句(没说 ...
- 我为什么反对推荐新人编程C/C++语言入门?
虽然我接触编程以及计算机时间比较早,但是正式打算转入程序员这个行当差不多是大学第四年的事情 从03年接触计算机,07年开始接触计算机编程, 期间接触过的技术包括 缓冲区溢出(看高手写的shellcod ...
- 《C语言入门1.2.3—一个老鸟的C语言学习心得》—清华大学出版社炮制的又一本劣书及伪书
<C语言入门1.2.3—一个老鸟的C语言学习心得>—清华大学出版社炮制的又一本劣书及伪书 [薛非评] 区区15页,有80多个错误. 最严重的有: 通篇完全是C++代码,根本不是C语言代码. ...
- c语言入门教程 / c语言入门经典书籍
用C语言开始编写代码初级:C语言入门必备(以下两本书任选一本即可) C语言是作为从事实际编程工作的程序员的一种工具而出现的,本阶段的学习最主要的目的就是尽快掌握如何用c语言编写程序的技能.对c语言的数 ...
- 【转】c语言入门教程 / c语言入门经典书籍
用C语言开始编写代码 初级:C语言入门必备 (以下两本书任选一本即可) C语言是作为从事实际编程工作的程序员的一种工具而出现的,本阶段的学习最主要的目的就是尽快掌握如何用c语言编写程序的技能.对c语言 ...
- Swift语言入门之旅
Swift语言入门之旅 学习一门新的计算机语言,传统来说都是从编写一个在屏幕上打印"Hello world"的程序開始的.那在 Swift,我们使用一句话来实现它: printl ...
- 《Ruby语言入门教程v1.0》学习笔记-01
<Ruby语言入门教程v1.0> 编著:张开川 邮箱:kaichuan_zhang@126.com 想要学习ruby是因为公司的自动化测试使用到了ruby语言,但是公司关于ruby只给了一 ...
- 【南阳OJ分类之语言入门】80题题目+AC代码汇总
小技巧:本文之前由csdn自动生成了一个目录,不必下拉一个一个去找,可通过目录标题直接定位. 本文转载自本人的csdn博客,复制过来的,排版就不弄了,欢迎转载. 声明: 题目部分皆为南阳OJ题目. 代 ...
- C语言入门(21)——使用DBG对C语言进行调试
C语言入门(21)--使用DBG对C语言进行调试 程序中除了一目了然的Bug之外都需要一定的调试手段来分析到底错在哪.到目前为止我们的调试手段只有一种:根据程序执行时的出错现象假设错误原因,然后在代码 ...
- C语言入门(7)——自定义函数
C源程序是由函数组成的.虽然在C语言入门系列前面几篇的程序中大都只有一个主函数main(),但实用程序往往由多个函数组成.函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能.C语言中的函数相 ...
随机推荐
- Java IO流详尽解析
流的概念和作用 学习Java IO,不得不提到的就是JavaIO流. 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象.即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输 ...
- API地图坐标转化(批量转换坐标)
<!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content ...
- Eclipse导入android包错误
错误提示:Invalid project description… 解决方案:假设你的工作空间是workshop,那么你可以在你的workshop下新建一个文件夹,然后放入你的包,再在Eclipse中 ...
- SQLdiag Utility
使用SQLdiag 会进行信息搜集类型 Windows 系统性能日志 Windows 系统日志 SQL Server 性能信息 SQL Server 阻塞信息 SQL Server 配置信息 如何使用 ...
- 【F#】 入门代码
找下感觉: 语法和go 如出一辙, 都是erlang派的语言 在 vs 中我没有找到自动缩进的快捷键 github上的F#代码也相对较少 // 在 http://fsharp.org 上了解有关 F# ...
- [ios]ios的延迟执行方法
1.最直接的方法performSelector:withObject:afterDelay: 这种方法的缺点:每次要为延时写一个方法 2.使用类别,用BOLCK执行 [代码]c#/cpp/oc代码 ...
- C# 获取数组的子集
private static void PrintSubItems(int[] source) { int i = 1; int total = int.Parse(Math.Pow(2, sourc ...
- 20145120 《Java程序设计》第4周学习总结
20145120 <Java程序设计>第4周学习总结 教材学习内容总结 -定义子类,加"extends+父类名"以继承父类. -子类只能继承一个父类 -编辑器会检查等号 ...
- DB天气app冲刺二阶段第八天
今天突然感觉应该做收尾工作了 因为马上就要考试了,时间一下子就不够用了.. 今天主要修复了一下bug,然后天气基本能够实时准确了,就是多功能按钮还是没有弄好 准备简化一下功能. 明天看看还有什么需要改 ...
- Highcharts-3.0.6
Highcharts-3.0.6 报表插件