【整理】go语言基础参考文档
go语言基础参考文档
一、
1.go语言反对函数和操作符的重载
2.go语言支持类、类成员方法、类的组合,反对继承,反对虚函数和虚函数重载;不过go语言也提供了重载,采用的是组合的文件提供。
3.go语言放弃了构造函数和析构函数
4.go语言提供接口【interface】 ,非入侵性
5.go语言主要特性:
自动垃圾回收
更丰富的内置类型
函数多返回值
错误处理
匿名函数和闭包
类型和接口
并发编程
反射
语言交互性
6.go语言可多返回值功能,对于不感兴趣的返回值可以使用下划线避免;
7.错误处理:go语言的错误处理机制可以大量减少代码量
3个关键词:defer、panic、recover
1.每个go源代码文件的开头都是一个package申明,表示该go所属的包,包是go语言里最基本的分发单位,也是工程管理中依赖关系的体现,执行go语言需建立一个main的包
2.main函数不能带参数,也不能定义返回值,命令行传入的参数在os.Args变量中保存,如需支持命令行开关,需使用flag包
3.不得包含代码文件中没有用到的包,否则go编译器会报错
4.函数以关键字func 开头
func 函数名(参数列表)(返回值列表){
//函数体
}
5.go代码中不要求开发者在每个语句后面加上分号表示语句结束
6.查看go编译器是否安装
go version
7.程序编译和运行
go run 文件名 #编译+链接+运行 3个步骤合并为一步
8.只编译
go build 文件名
./文件名
9.变量:go语言中的变量使用方式与C语言接近,但具备更大的灵活性
10.变量申明
1).关键字var
2).类型放在变量名之后
3).变量申明不需要使用分号作为结束符
var v1 int
var v2 string
var v3 [10]int //数组
var v4 []int //数组切片
var v5 struct {
f int
}
var v6 *int //指针
var v7 map[string]int
var v8 func(a int) int
11.变量初始化 var关键字可以保留,但不是必要的元素
var v1 int = 10
var v2 = 10
v3 := 10
出现在:=左侧的变量不应该是已经申明过的
12.go语言中变量初始化和赋值是两个不同的概念
13.变量交换语句
i, j = j, i
14.匿名变量
func GetName() (firstName, lastName, nickName string) {
return "May", "Chan", "Chibi Maruko"
}
//若只需要获得nickName,可以这样写
_, _, nickName := GetName()
15.常量 :可以是:整形、浮点型、复数类型、布尔类型、字符串类型
16.常量定义
1).关键字const
2).如果定义常量时没有指定类型,那么他与字面常量一样,是无类型常量
17.由于常量的赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式,比如试图以如下方式定义常量就会导致编译错误:
const Home = os.GetEnv("HOME")
18.Go语言预定义了这些常量: true、 false和iota,iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,然后在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
19.枚举
const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
numberOfDays // 这个常量没有导出
)
同Go语言的其他符号( symbol)一样,以大写字母开头的常量在包外可见
20.类型
布尔类型: bool。
整型: int8、 byte、 int16、 int、 uint、 uintptr等。
浮点类型: float32、 float64。
复数类型: complex64、 complex128。
字符串: string。
字符类型: rune。
错误类型: error。
此外, Go语言也支持以下这些复合类型:
指针( pointer)
数组( array)
切片( slice)
字典( map)
通道( chan)
结构体( struct)
接口( interface)
21.布尔类型:布尔不接受其他类型的赋值,不支持自动或强制类型转换
22.整型:int和int32在go语言中被认为是两种不同的类型,编译器也不会帮你自动做类型转换
23.不同类型的整型数不能直接进行比较,但各种类型的整型变量可以和字面常量进行比较
24.运算符:go语言基本同C语言,除了取反
go语言中:取反^x
25.浮点型:表示包含小数点的数据,采用IEEE-754标准的表达式
go语言中定义了两个类型float32和float64,其中float32表示C语言中的float型,float64表示C语言中的double类型
26.复数类型:实部(real),虚部(imag)
var value1 complex64
value1 = 3.2 + 12i
27.字符串:
1).字符串操作
x+y 字符串连接
len(s) len("hello") //结果为5
s[i] 取字符 "hello"[1] //结果为e
中文字符UTF-8占3个字节
28.字符类型:一个是byte ,代表UTF-8字符串的单个字节的值,
rune,代表Unicode字符
29.数组:在go语言中,数组长度在定以后就不可更改
元素访问:可以使用数组下标来访问数组中的元素
关键字range 用于便捷地遍历容器中的元素,数组也是range的支持范围
for i, v := range array {
fmt.Println("Array element[", i, "]=", v)
}
range具有两个返回值,第一个返回值是元素的数组下标,第二个返回值是元素的值
需要特别注意的是,在Go语言中数组是一个值类型( value type)。所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。如果将数组作为函数的参数类型,则在函数调用时该参数将发生数据复制。因此,在函数体中无法修改传入的数组的内容,因为函数内操作的只是所传入数组的一个副本。
30.数组切片:
Go语言提供了数组切片( slice)这个非常酷的功能来弥补数组的不足数组切片就像一个指向数组的指针,实际上它拥有自己的数据结构,而不仅仅是个指针。数组切片的数据结构可以抽象为以下3个变量:
1).一个指向原生数组的指针
2).数组切片中的元素个数
3).数组切片已分配的内存空间
创建数组切片的方法主要有两种——基于数组和直接创建
基于数组:
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基于数组创建一个数组切片
var mySlice []int = myArray[:5]
Go语言支持用myArray[first:last]这样的方式来基于数组生成一
个数组切片,而且这个用法还很灵活,
基于myArray的所有元素创建数组切片:
mySlice = myArray[:]
基于myArray的前5个元素创建数组切片:
mySlice = myArray[:5]
直接创建: Go语言提供的内置函数make()可以用于灵活地创建数组切片
创建一个初始元素个数为5的数组切片,元素初始值为0:
mySlice1 := make([]int, 5)
创建一个初始元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间:
mySlice2 := make([]int, 5, 10)
直接创建并初始化包含5个元素的数组切片:
mySlice3 := []int{1, 2, 3, 4, 5}
数组切片支持Go语言内置的cap()函数和len()函数,cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是
数组切片中当前所存储的元素个数;append()函数可以往尾端加入新的元素
mySlice := make([]int, 5, 10)
mySlice = append(mySlice, 1, 2, 3)
函数append()的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素
甚至直接将一个数组切片追加到另一个数组切片的末尾
mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片
mySlice = append(mySlice, mySlice2...)
在第二个参数mySlice2后面加了三个点,即一个省略号,如果没有这个省略号的话,会有编译错误
数组切片会自动处理存储空间不足的问题。如果追加的内容长度超过当前已分配的存储空间
(即cap()调用返回的信息),数组切片会自动分配一块足够大的内存
基于数组切片创建数组切片
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 基于oldSlice的前3个元素构建新数组切片
内容复制
数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制、
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
31.map
map是一堆键值对的未排序集合
1).变量申明:map的申明基本没有多余的元素
var myMap map[string] PersonInfo
myMap是声明的map变量名, string是键的类型, PersonInfo则是其中所存放的值类型
2).创建
创建了一个键类型为string、值类型为PersonInfo的map:
myMap = make(map[string] PersonInfo)
也可以选择是否在创建时指定该map的初始存储能力,下面的例子创建了一个初始存储能力为100的map:
myMap = make(map[string] PersonInfo, 100)
3).元素赋值 将键和值用下面的方式对应起来即可
myMap["1234"] = PersonInfo{"1", "Jack", "Room 101,..."}
4).元素删除
Go语言提供了一个内置函数delete(),用于删除容器内的元素
delete(myMap, "1234")
5).元素查找
<1>. 声明并初始化一个变量为空
<2>. 试图从map中获取相应键的值到该变量中
<3>. 判断该变量是否依旧为空,如果为空则表示map中没有包含该变量
32.控制流程
选择、循环、跳转
条件语句 if、 else和else if
选择语句 switch、 case和select
循环语句 for和range
跳转语句 goto
1).条件语句:
<1>. 条件语句不需要使用括号将条件包含起来();
<2>. 无论语句体内有几条语句,花括号{}都必须存在;
<3>. 左花括号{必须与if或者else处于同一行;
<4>. 在if之后,条件语句之前,可以添加变量初始化语句,使用;间隔;
<5>. 在有返回值的函数中,不允许将“最终的” return语句包含在if...else...结构中,否则会编译失败
编译失败的案例如下:
func example(x int) int {
if x == 0 {
return 5
} else {
return x
}
}
2).选择语句
<1>. 左花括号{必须与switch处于同一行;
<2>. 条件表达式不限制为常量或者整数;
<3>. 单个case中,可以出现多个结果选项;
<4>. 与C语言等规则相反, Go语言不需要用break来明确退出一个case;
<5>. 只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
<6>. 可以不设定switch之后的条件表达式,在此种情况下,整个switch结构与多个
if...else...的逻辑作用等同
switch i {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3:
fmt.Printf("3")
case 4, 5, 6:
fmt.Printf("4, 5, 6")
default:
fmt.Printf("Default")
}
switch后面的表达式甚至不是必需的,比如下面的例子:
switch {
case 0 <= Num && Num <= 3:
fmt.Printf("0-3")
case 4 <= Num && Num <= 6:
fmt.Printf("4-6")
case 7 <= Num && Num <= 9:
mt.Printf("7-9")
}
3).循环语句 Go语言中的循环语句只支持for关键字,而不支持while和do-while结构
<1>. 左花括号{必须与for处于同一行
<2>. Go语言中的for循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量
<3>. Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环
for后面的条件表达式不需要用圆括号()包含起来
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
Go语言无限循环的场景
sum := 0
for {
sum++
if sum > 100 {
break
}
}
a := []int{1, 2, 3, 4, 5, 6}
for i, j := 0, len(a) – 1; i < j; i, j = i + 1, j – 1 {
a[i], a[j] = a[j], a[i]
}
4).跳转语句
goto语句的语义非常简单,就是跳转到本函数内的某个标签
func myfunc() {
i := 0
HERE:
fmt.Println(i)
i++
if i < 10 {
goto HERE
}
}
33.函数 基本组成:关键字func、函数名、参数列表、返回值、函数体和返回值语句
如果参数列表中若干个相邻的参数类型的相同,则可以在参数列表中省略前面变量的类型声明
func Add(a, b int)(ret int, err error) {
// ...
}
34.函数调用
注意:小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。
这个规则也适用于类型和变量的可见性。
35.不定参数
1).不定参数类型
不定参数是指函数传入的参数个数为不定数量
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
函数myfunc()接受不定数量的参数,这些参数的类型全部是int
形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。它是一
个语法糖( syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用。通
常来说,使用语法糖能够增加程序的可读性,从而减少程序出错的机会
2).不定参数的传递
func myfunc(args ...int) {
// 按原样传递
myfunc3(args...)
// 传递片段,实际上任意的int slice都可以传进去
myfunc3(args[1:]...)
}
3).任意类型的不定参数
可以指定类型为interface{}
func Printf(format string, args ...interface{}) {
// ...
}
36.多返回值
37.匿名函数
匿名函数由一个不带函数名的函数声明和函数体组成
func(a, b int, z float64) bool {
return a*b <int(z)
}
匿名函数可以直接赋值给一个变量或者直接执行
f := func(x, y int) int {
return x + y
}
func(ch chan int) {
ch <- ACK
} (reply_chan) // 花括号后直接跟参数列表表示函数调用
闭包:闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者
任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含
在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环境(作用域)
38.错误处理
1).error接口
type error interface {
Error() string
}
2).defer
如果觉得一句话干不完清理的工作,也可以使用在defer后加一个匿名函数的做法:
defer func() {
// 做你复杂的清理工作
} ()
defer语句的调用是遵照 :先进后出的原则
3).panic() 和recover()
Go语言引入了两个内置函数panic()和recover()以报告和处理运行时错误和程序中的错误场景
func panic(interface{})
func recover() interface{}
recover()函数用于终止错误处理流程
defer func() {
if r := recover(); r != nil {
log.Printf("Runtime error caught: %v", r)
}
}()
foo()
二、面向对象编程
1.为类型添加方法
可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
/**************************************************/
func (a Integer) Less(b Integer) bool { // 面向对象
return a < b
}
func Integer_Less(a Integer, b Integer) bool { // 面向过程
return a < b
}
a.Less(2) // 面向对象的用法
Integer_Less(a, 2) // 面向过程的用法
面向对象
func (a *Integer) Add(b Integer) {
*a += b
}
这里为Integer类型增加了Add()方法。由于Add()方法需要修改对象的值,所以需要用指
针引用。调用如下:
func main() {
var a Integer = 1
a.Add(2)
fmt.Println("a =", a)
}
2.值语义和引用语义
b = a
b.Modify()
如果b的修改不会影响a的值,那么此类型属于值类型。如果会影响a的值,那么此类型是引用类型
3.结构体
定义:
type Rect struct {
x, y float64
width, height float64
}
初始化:4种方法都行
rect1 := new(Rect)
rect2 := &Rect{}
rect3 := &Rect{0, 0, 100, 200}
rect4 := &Rect{width: 100, height: 200}
未进行显式初始化的变量都会被初始化为该类型的零值
类实例化时指针或者实例区别:
注:
实例化类的时候,一般有2个方法:
(1) 用new(structName):这个方法得到的是*structName类型,即类的指针类型;
var t_t tmpInterface=new(tmpStruct)
(2) 用structName{init para}:这个方法得到的是structName类型,即类的实例类型,不是指针。
var t_ptr tmpInterface=tmpStruct{}
4.匿名组合
5.可见性
Go语言对关键字的增加非常吝啬,其中没有private、 protected、 public这样的关键
字。要使某个符号对其他包( package)可见(即可以访问),需要将该符号定义为以大写字母开头
6.接口 非侵入式接口
定义:type name interface {
method1(param_list)return_list
method2(param_list)return_list
...
}
7.接口赋值
将对象实例赋值给接口
将接口赋值给另一个接口
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
func (a *Integer) Add(b Integer) {
*a += b
}
type LessAdder interface {
Less(b Integer) bool
Add(b Integer)
}
var a Integer = 1
var b LessAdder = &a
8.Any类型
var v1 interface{} = 1 // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
三、并发编程
1.多进程
简单、进程间互不影响,但系统开销大,因为所有的进程都是由内核管理的。
2.多线程
开销小很多,但是其开销依旧比较大,且在高并发模式下,效率会有影响。
3.基于回调的非阻塞/异步IO
通过事件驱动的方式使用异步IO,使服务器持续运转,且尽可能地少用线程,降
低开销,它目前在Node.js中得到了很好的实践。但是使用这种模式,编程比多线程要复
杂,因为它把流程做了分割,对于问题本身的反应不够自然
4.协程(coroutine)
系统开销极小,可以有效提高线程的任务并发性,而避免多线程的缺点。
使用协程的优点是编程简单,结构清晰;缺点是需要语言的支持,
如果不支持,则需要用户在程序中自行实现调度器。目前,原生支持协程的语言还很少。
5.goroutine :轻量级线程
关键字 go
常见的通信模型 共享数据和消息
6.channel:
消息机制:每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发
单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息
channel是类型相关的,一个channel只能传递一种类型的值,这个类型需要在声明channel时指定。
申明:
var chanName chan ElementType //ElementType指定这个channel所能传递的元素类型
例:var ch chan int
定义channel,直接使用内置函数make()
ch :=make(chan int) //申明并初始化一个int型的名为ch的channel
数据写入channel 向channel写入数据通常会导致阻塞,直到goroutine从这个channel中读取数据
ch <- value
从channel中读取数据
value := <-ch
注:如果channel中没有数据,从channel中读取数据也会导致程序阻塞,直到channel写入数据为止
7.select 处理异步IO
结构:
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句 ,写入1
default:
// 如果上面都没有成功,则进入default处理流程
}
8.缓存机制
创建一个带缓冲的channel
c := make(chan int, 1024) //创建一个大小为1024的int类型channel,可以一直写入数据,直到缓冲区写满阻塞
从channel中读取数据,同费缓冲区channel读取方式一致,可以使用range
for i := range c {
fmt.Println("Received:", i)
}
9.超时机制
实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // 等待1秒钟
timeout <- true
}()
// 然后我们把timeout这个channel利用起来
select {
case <-ch:
// 从ch中读取到数据
case <-timeout:
// 一直没有从ch中读取到数据,但从timeout中读取到了数据
}
10.单向channel
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据
channel是一个原生类型,支持被传递,支持类型转换
例:
func Parse(ch <-chan int) {
for value := range ch {
fmt.Println("Parsing value", value)
}
}
11.关闭channel 内置函数close()
close(ch)
判断一个channel是否已经被关闭,可以在读取的时候使用多重返回值的方式:
x, ok := <-ch
返回值是false则表示ch已经被关闭
12.让出时间片
使用runtime包中Gosched()函数实现
13.同步锁
Go语言包中的sync包提供了两种锁类型: sync.Mutex和sync.RWMutex。 Mutex是最简单
的一种锁类型,同时也比较暴力,当一个goroutine获得了Mutex后,其他goroutine就只能乖乖等
到这个goroutine释放该Mutex。
var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
//...
}
四、网络编程
1.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")
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)
net.ResolveTCPAddr(),用于解析地址和端口号;
net.DialTCP(),用于建立链接。
验证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)
2.HTTP编程
HTTP( HyperText Transfer Protocol,超文本传输协议)是互联网上应用最为广泛的一种网络协议,
定义了客户端和服务端之间请求与响应的传输标准。
HTTP客户端
基本方法:
func (c *Client) Get(url string) (r *Response, err error)
func (c *Client) Post(url string, bodyType string, body io.Reader) (r *Response, err
error)
func (c *Client) PostForm(url string, data url.Values) (r *Response, err error)
func (c *Client) Head(url string) (r *Response, err error)
func (c *Client) Do(req *Request) (resp *Response, err error)
<1>.http.Get() 请求一个源
resp, err := http.Get("http://example.com/")
if err != nil {
// 处理错误 ...
return
}
defer resp.Body.close()
io.Copy(os.Stdout, resp.Body)
上面这段代码请求一个网站首页,并将其网页内容打印到标准输出流中。
<2>.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
}
// ...
<3>.http.PostForm()
http.PostForm()方法实现了标准编码格式为application/x-www-form-urlencoded的表单提交
resp, err := http.PostForm("http://example.com/posts", url.Values{"title":
{"article title"}, "content": {"article body"}})
if err != nil {
// 处理错误
return
}
// ...
<4>.http.Head()
HTTP 中的 Head 请求方式表明只请求目标 URL 的头部信息,即 HTTP Header 而不返回 HTTP Body
下面的示例代码请求一个网站首页的 HTTP Header信息:
resp, err := http.Head("http://example.com/")
<5>.(*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)
// ...
高级封装
自定义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
// ...
}
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")
灵活的 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服务器
<1>.处理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"))
3.RPC编程
RPC( Remote Procedure Call,远程过程调用)是一种通过网络从远程计算机程序上请求服
务,而不需要了解底层网络细节的应用程序通信协议RPC协议构建于TCP或UDP,或者是 HTTP
之上,允许开发者直接调用另一台计算机上的程序,而开发者无需额外地为这个调用过程编写网
络通信相关代码,使得开发包括网络分布式程序在内的应用程序更加容易。
RPC 采用客户端—服务器( Client/Server)的工作模式。请求程序就是一个客户端( Client),
而服务提供程序就是一个服务器( Server)。当执行一个远程过程调用时,客户端程序首先发送一
个带有参数的调用信息到服务端,然后等待服务端响应。在服务端,服务进程保持睡眠状态直到
客户端的调用信息到达为止。当一个调用信息到达时,服务端获得进程参数,计算出结果,并向
客户端发送应答信息,然后等待下一个调用。最后,客户端接收来自服务端的应答信息,获得进
程结果,然后调用执行并继续进行
标准库:net/rpc
net/rpc包允许 RPC 客户端程序通过网络或是其他 I/O 连接调用一个远端对象的公开方法
在 RPC 服务端,可将一个对象注册为可访问的服务,
之后该对象的公开方法就能够以远程的方式提供访问。一个 RPC 服务端可以注册多个不同类型
的对象,但不允许注册同一类型的多个对象。
一个对象中只有满足如下这些条件的方法,才能被 RPC 服务端设置为可供远程访问:
必须是在对象外部可公开调用的方法(首字母大写);
必须有两个参数,且参数的类型都必须是包外部可以访问的类型或者是Go内建支持的类型;
第二个参数必须是一个指针;
方法必须返回一个error类型的值。
func (t *T) MethodName(argType T1, replyType *T2) error
该方法( MethodName)的第一个参数表示由 RPC 客户端传入的参数,第二个参数表示要返
回给RPC客户端的结果,该方法最后返回一个 error 类型的值。
RPC 服务端可以通过调用 rpc.ServeConn 处理单个连接请求。多数情况下,通过 TCP 或
是 HTTP 在某个网络地址上进行监听来创建该服务是个不错的选择。
Gob是Go的一个序列化数据结构的编码解码工具
God是二进制编码的数据流,并且God流是自解释的,God无法跨语言使用,在 Go 的net/rpc包中,
传输数据所需要用到的编码解码器,默认就是 Gob
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() 方法来关闭连接。
4.JSON处理
JSON ( JavaScript Object Notation)是一种比XML更轻量级的数据交换格式,在易于人们阅
读和编写的同时,也易于程序解析和生成。尽管JSON是JavaScript的一个子集,但JSON采用完全
独立于编程语言的文本格式,且表现为键/值对集合的文本描述形式(类似一些编程语言中的字
典结构),这使它成为较为理想的、跨平台、跨语言的数据交换语言。
可以用 JSON 传输简单的字符串、数字、布尔值,也可以传输一个数组,或者一个更复杂的复合结构
Go语言内置的encoding/json 标准库,在Go语言实现JSON的编码和解码时,遵循RFC4627协议标准。
<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
}`)
Go语言的大多数数据类型都可以转化为有效的JSON文本,但channel、 complex和函数这几种类型除外。
在Go中, JSON转化前后的数据类型映射如下。
布尔值转化为JSON后还是布尔类型。
浮点数和整型会被转化为JSON里边的常规数字。
字符串将以UTF-8编码转化输出为Unicode字符集的字符串,特殊字符比如<将会被转义为\u003c。
数组和切片会转化为JSON里边的数组,但[]byte类型的值将会被转化为 Base64 编码后
的字符串,slice类型的零值会被转化为null。
结构体会转化为JSON对象,并且只有结构体里边以大写字母开头的可被导出的字段才会
被转化输出,而这些可导出的字段会作为JSON对象的字符串索引。
转化一个map类型的数据结构时,该数据的类型必须是 map[string]T( T可以是encoding/json 包支持的任意数据类型)。
<2>.json.Unmarshal()函数将JSON格式的文本解码为Go里边预期的数据结构。
func Unmarshal(data []byte, v interface{}) error
该函数的第一个参数是输入,即JSON格式的文本(比特序列),第二个参数表示目标输出容器,用于存放解码后的值。
json.Unmarshal()函数会根据一个约定的顺序查找目标结构中的字段,如果找到
一个即发生匹配。假设一个JSON对象有个名为"Foo"的索引,要将"Foo"所对应的值填充到目标
结构体的目标字段上, json.Unmarshal()将会遵循如下顺序进行查找匹配:
(1) 一个包含Foo标签的字段;
(2) 一个名为Foo的字段;
(3) 一个名为Foo或者Foo或者除了首字母其他字母不区分大小写的名为Foo的字段。
这些字段在类型声明中必须都是以大写字母开头、可被导出的字段。
<3>.解码未知结构的json数据
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:
fmt.Println(k, "is string", v2)
case int:
fmt.Println(k, "is int", v2)
case bool:
fmt.Println(k, "is bool", v2)
case []interface{}:
fmt.Println(k, "is an array:")
for i, iv := range v2 {
fmt.Println(i, iv)
}
default:
fmt.Println(k, "is another type not handle yet")
}
}
}
<4>.json的流式读写
Go内建的encoding/json 包还提供Decoder和Encoder两个类型,用于支持JSON数据的
流式读写,并提供NewDecoder()和NewEncoder()两个函数来便于具体实现:
func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder
五、安全编程
1.数据加密
采用单密钥的加密算法,我们称为对称加密,整个系统由如下几部分构成:需要加密的明文、
加密算法和密钥。在加密和解密中,使用的密钥只有一个。常见的单密钥加密算法有DES、 AES、RC4等。
采用双密钥的加密算法,我们称为非对称加密,整个系统由如下几个部分构成:需要加密的明文、加密算法、私钥和公钥
在该系统中,私钥和公钥都可以被用作加密或者解密,但是用私钥加密的明文,必须要用对应的公钥解密,
用公钥加密的明文,必须用对应的私钥解密。常见的双密钥加密算法有RSA等
2.数字签名
数字签名,是指用于标记数字文件拥有者、创造者、分发者身份的字符串。数字签名拥有标记文件身份、分发的不可抵赖性等作用。
常用的数字签名采用了非对称加密
3.数字证书
4.PKI体系
PKI,全称公钥基础设施,是使用非对称加密理论,提供数字签名、加密、数字证书等服务
的体系,一般包括权威认证机构( CA)、数字证书库、密钥备份及恢复系统、证书作废系统、应用接口( API)等。
5.加密通信
一般的HTTPS是基于SSL( Secure Sockets Layer)协议,SSL是网景公司开发的位于TCP与
HTTP之间的透明安全协议,通过SSL,可以把HTTP包数据以非对称加密的形式往返于浏览器和站点之间,从而避免被第三方非法获取。
六、工程管理
1.gotool可以帮你完成以下几类工作
<1>.代码格式化
<2>.代码质量分析和修复
<3>.单元测试与性能测试
<4>.工程构建
<5>.代码文档的提取和展示
<6>.依赖包管理
<7>.执行其他的包含指令,比如6g等
2.强制性编码规范
1.命名
任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
Go语言明确宣告了拥护骆驼命名法而排斥下划线法
2.格式化工具
go help fmt
go fmt filename
特点:
<1>.调整了每条语句的位置
<2>.重新摆放花括号的位置
<3>.以制表符缩进代码
<4>.添加空格
3.远程import支持
Go语言不仅允许我们导入本地包,还支持在语言级别调用远程的包。
我们有一个用于计算CRC32的包托管于Github,那么可以这样写:
package main
import (
"fmt"
"github.com/myteam/exp/crc32"
)
然后,在执行go build或者go install之前,只需要加这么一句:
go get github.com/myteam/exp/crc32
当我们执行完go get之后 ,我们会在src目录中看到github.com目录 ,其中包含myteam/exp/crc32目录
4.Android 支持
要在Android上执行Go程序,首先要定制出能够生成对应目标二进制文件的Go工具链。
在编译Go源代码之前,我们可以作如下设置:
$ export GOARCH=ARM
$ export GOOS=linux
$ ./all.bash
会生成5g和5l,其中5g是编译器, 5l是链接器
5.单元测试
Go的单元测试函数分为两类:功能测试函数和性能测试函数,分别为以Test和Benchmark
为函数名前缀并以*testing.T为单一参数的函数,
func TestAdd1(t *testing.T)
func BenchmarkAdd1(t *testing.T)
执行功能单元测试非常简单,直接执行go test命令即可
func BenchmarkAdd1(b *testing.B) {
b.StopTimer() // 暂停计时器
DoPreparation() // 一个耗时较长的准备工作,比如读文件
b.StartTimer() // 开启计时器,之前的准备时间未计入总花费时间内
for i := 0; i < b.N; i++ {
Add(1, 2)
}
}
性能单元测试的执行与功能测试一样简单,不过需要调用时需要增加-test.bench参数,
go test–test.bench add.go
6.编译环境
配置几个常用的工程构建命令:
构建当前工程( Go Build)
编译当前打开的Go文件( Go Compile)
运行单元测试( Go Test)
安装( Go Install)
7.反射
8.类型映射
对于C语言的原生类型, Cgo都会将其映射为Go语言中的类型: C.char和C.schar(对应于
C语言中的signed char)、 C.uchar(对应于C语言中的unsigned char)、 C.short和C.ushort
(对应于unsigned short)、 C.int和C.uint(对应于unsigned int)、 C.long和C.ulong
(对应于unsigned long)、 C.longlong(对应于C语言中的long long)、 C.ulonglong(对
应于C语言中的unsigned long long类型)以及C.float和C.double。 C语言中的void*指针
类型在Go语言中则用特殊的unsafe.Pointer类型来对应。
C语言中的struct、 union和enum类型,对应到Go语言中都会变成带这样前缀的类型名称:
struct_、 union_和enum_。 比 如 一 个 在 C语 言 中 叫 做person的struct会 被 Cgo 翻 译 为
C.struct_person。
9.字符串映射
因为Go语言中有明确的string原生类型,而C语言中用字符数组表示,两者之间的转换是
一个必须考虑的问题。 Cgo提供了一系列函数来提供支持: C.CString、 C.GoString和
C.GoStringN。需要注意的是,每次转换都将导致一次内存复制,因此字符串内容其实是不可
修改的。
10.符号链接
在Go 语言中,一般化的函数原型如下:
package Package
func Method(arg1 ArgType1, arg2 ArgType2, ...) (ret1 RetType1, ret2 RetType2, ...)
func (v ClassType) Method(arg1 ArgType1, arg2 ArgType2, ...) (ret1 RetType1, ret2
RetType2, ...)
func (this *ClassType) Method(arg1 ArgType1, arg2 ArgType2, ...) (ret1 RetType1, ret2
RetType2, ...) // 这种可以认为是上一种情况的特例
由于 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
11.协程
协程,也有人称之为轻量级线程,具备以下几个特点。
能够在单一的系统线程中模拟多个任务的并发执行。
在一个特定的时间,只有一个任务在运行,即并非真正地并行。
被动的任务调度方式,即任务没有主动抢占时间片的说法。当一个任务正在执行时,外
部没有办法中止它。要进行任务切换,只能通过由该任务自身调用yield()来主动出
请发表评论