diff --git a/2.2.md b/2.2.md index 05c543f6..706f300b 100644 --- a/2.2.md +++ b/2.2.md @@ -1,452 +1,449 @@ -# 2.2 Go基础 - -这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 - -## 定义变量 - -Go语言里面定义变量有多种方式。 - -使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: - - //定义一个名称为“variableName”,类型为"type"的变量 - var variableName type - -定义多个变量 - - //定义三个类型都是“type”的三个变量 - var vname1, vname2, vname3 type - -定义变量并初始化值 - - //初始化“variableName”的变量为“value”值,类型是“type” - var variableName type = value - -同时初始化多个变量 - - /* - 定义三个类型都是"type"的三个变量,并且它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - */ - var vname1, vname2, vname3 type= v1, v2, v3 - -你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: - - /* - 定义三个变量,它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - 然后Go会根据其相应值的类型来帮你初始化它们 - */ - var vname1, vname2, vname3 = v1, v2, v3 - -你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: - - /* - 定义三个变量,它们分别初始化相应的值 - vname1为v1,vname2为v2,vname3为v3 - 编译器会根据初始化的值自动推导出相应的类型 - */ - vname1, vname2, vname3 := v1, v2, v3 - -现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 - -`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: - - _, b := 34, 35 - -Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 - - package main - - func main() { - var i int - } - -## 常量 - -所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时则无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 - -它的语法如下: - - const constantName = value - //如果需要,也可以明确指定常量的类型: - const Pi float32 = 3.1415926 - -下面是一些常量声明的例子: - - const Pi = 3.1415926 - const i = 10000 - const MaxThread = 10 - const prefix = "astaxie_" - - -## 内置基础类型 - -### Boolean - -在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 - - //示例代码 - var isActive bool // 全局变量声明 - var enabled, disabled = true, false // 忽略类型的声明 - func test() { - var available bool // 一般声明 - valid := false // 简短声明 - available = true // 赋值操作 - } - - -### 数值类型 - -整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。当前的gcc和gccgo编译器在32位和64位平台上都使用32位来表示`int`和`uint`,但未来在64位平台上可能增加到64位。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 - ->需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 -> ->如下的代码会产生错误 -> ->> var a int8 - ->> var b int32 - ->> c:=a + b -> ->另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 - -浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 - -这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: - - var c complex64 = 5+5i - //output: (5+5i) - fmt.Printf("Value is: %v", c) - - -### 字符串 - -我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 - - //示例代码 - var frenchHello string // 声明变量为字符串的一般方法 - var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 - func test() { - no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 - japaneseHello := "Ohaiou" // 同上 - frenchHello = "Bonjour" // 常规赋值 - } - -在Go中字符串是不可变的,例如下面的代码编译时会报错: - - var s string = "hello" - s[0] = 'c' - - -但如果真的想要修改怎么办呢?下面的代码可以实现: - - s := "hello" - c := []byte(s) // 将字符串 s 转换为 []byte 类型 - c[0] = 'c' - s2 := string(c) // 再转换回 string 类型 - fmt.Printf("%s\n", s2) - - -Go中可以使用`+`操作符来连接两个字符串: - - s := "hello," - m := " world" - a := s + m - fmt.Printf("%s\n", a) - -修改字符串也可写为: - - s := "hello" - s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 - fmt.Printf("%s\n", s) - -如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: - - m := `hello - world` - -`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。 - -### 错误类型 -Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: - - err := errors.New("emit macho dwarf: elf header corrupted") - if err != nil { - fmt.Print(err) - } - -### Go数据底层的存储 - -下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 - -![](images/2.2.basic.png?raw=true) - -## 一些技巧 - -### 分组声明 - -在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 - -例如下面的代码: - - import "fmt" - import "os" - - const i = 100 - const pi = 3.1415 - const prefix = "Go_" - - var i int - var pi float32 - var prefix string - -可以分组写成如下形式: - - import( - "fmt" - "os" - ) - - const( - i = 100 - pi = 3.1415 - prefix = "Go_" - ) - - var( - i int - pi float32 - prefix string - ) - ->除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 - -### iota枚举 - -Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,每调用一次加1: - - const( - x = iota // x == 0 - y = iota // y == 1 - z = iota // z == 2 - w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" - ) - - const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 - -### Go程序设计的一些规则 -Go之所以会那么简洁,是因为它有一些默认的行为: -- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。 -- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 - -## array、slice、map - -### array -`array`就是数组,它的定义方式如下: - - var arr [n]type - -在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: - - var arr [10]int // 声明了一个int类型的数组 - arr[0] = 42 // 数组下标是从0开始的 - arr[1] = 13 // 赋值操作 - fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 - fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 - -由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 - -数组可以使用另一种`:=`来声明 - - a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 - - b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 - - c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 - -也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: - - // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 - doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} - - // 如果内部的元素和外部的一样,那么上面的声明可以简化,直接忽略内部的类型 - easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} - -数组的分配如下所示: - -![](images/2.2.array.png?raw=true) - - -### slice - -在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` - -`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 - - // 和声明array一样,只是少了长度 - var fslice []int - -接下来我们可以声明一个`slice`,并初始化数据,如下所示: - - slice := []byte {'a', 'b', 'c', 'd'} - -`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 - - // 声明一个含有10个元素元素类型为byte的数组 - var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - - // 声明两个含有byte的slice - var a, b []byte - - // a指向数组的第3个元素开始,并到第五个元素结束, - a = ar[2:5] - //现在a含有的元素: ar[2]、ar[3]和ar[4] - - // b是数组ar的另一个slice - b = ar[3:5] - // b的元素是:ar[3]和ar[4] - ->注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 - -它们的数据结构如下所示 - -![](images/2.2.slice.png?raw=true) - -slice有一些简便的操作 - - - `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` - - `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` - - 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` - -下面这个例子展示了更多关于`slice`的操作: - - // 声明一个数组 - var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - // 声明两个slice - var aSlice, bSlice []byte - - // 演示一些简便操作 - aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c - aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j - aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素 - - // 从slice中获取slice - aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 - bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f - bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f - bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h - bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g - -`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 - -从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: -- 一个指针,指向数组中`slice`指定的开始位置 -- 长度,即`slice`的长度 -- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 - - Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} - Slice_a := Array_a[2:5] - -上面代码的真正存储结构如下图所示 - -![](images/2.2.slice2.png?raw=true) - -对于`slice`有几个有用的内置函数: - -- `len` 获取`slice`的长度 -- `cap` 获取`slice`的最大容量 -- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` -- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 - -注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 -但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 - -### map - -`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` - -我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`key`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 - - // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 - var numbers map[string] int - // 另一种map的声明方式 - numbers := make(map[string]int) - numbers["one"] = 1 //赋值 - numbers["ten"] = 10 //赋值 - numbers["three"] = 3 - - fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 - // 打印出来如:第三个数字是: 3 - -这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 - -使用map过程中需要注意的几点: -- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 -- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 -- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 -- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` - -`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 - -通过`delete`删除`map`的元素: - - // 初始化一个字典 - rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } - // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true - csharpRating, ok := rating["C#"] - if ok { - fmt.Println("C# is in the map and its rating is ", csharpRating) - } else { - fmt.Println("We have no rating associated with C# in the map") - } - - delete(rating, "C") // 删除key为C的元素 - - -上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: - - m := make(map[string]string) - m["Hello"] = "Bonjour" - m1 := m - m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 - - -### make、new操作 - -`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 - -内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: - ->`new`返回指针。 - -内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 - ->`make`返回初始化后的(非零)值。 - -下面这个图详细的解释了`new`和`make`之间的区别。 - -![](images/2.2.makenew.png?raw=true) - -关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 -此处罗列 部分类型 的 “零值” - - int 0 - int8 0 - int32 0 - int64 0 - uint 0x0 - rune 0 //rune的实际类型是 int32 - byte 0x0 // byte的实际类型是 uint8 - float32 0 //长度为 4 byte - float64 0 //长度为 8 byte - bool false - string "" - - - - -## links - * [目录]() - * 上一章: [你好,Go](<2.1.md>) - * 下一节: [流程和函数](<2.3.md>) +# 2.2 Go基础 + +这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。 + +## 定义变量 + +Go语言里面定义变量有多种方式。 + +使用`var`关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面: + + //定义一个名称为“variableName”,类型为"type"的变量 + var variableName type + +定义多个变量 + + //定义三个类型都是“type”的三个变量 + var vname1, vname2, vname3 type + +定义变量并初始化值 + + //初始化“variableName”的变量为“value”值,类型是“type” + var variableName type = value + +同时初始化多个变量 + + /* + 定义三个类型都是"type"的三个变量,并且它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + */ + var vname1, vname2, vname3 type= v1, v2, v3 + +你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了: + + /* + 定义三个变量,它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + 然后Go会根据其相应值的类型来帮你初始化它们 + */ + var vname1, vname2, vname3 = v1, v2, v3 + +你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化: + + /* + 定义三个变量,它们分别初始化相应的值 + vname1为v1,vname2为v2,vname3为v3 + 编译器会根据初始化的值自动推导出相应的类型 + */ + vname1, vname2, vname3 := v1, v2, v3 + +现在是不是看上去非常简洁了?`:=`这个符号直接取代了`var`和`type`,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用`var`方式来定义全局变量。 + +`_`(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值`35`赋予`b`,并同时丢弃`34`: + + _, b := 34, 35 + +Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了`i`但未使用。 + + package main + + func main() { + var i int + } + +## 常量 + +所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时则无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。 + +它的语法如下: + + const constantName = value + //如果需要,也可以明确指定常量的类型: + const Pi float32 = 3.1415926 + +下面是一些常量声明的例子: + + const Pi = 3.1415926 + const i = 10000 + const MaxThread = 10 + const prefix = "astaxie_" + + +## 内置基础类型 + +### Boolean + +在Go中,布尔值的类型为`bool`,值是`true`或`false`,默认为`false`。 + + //示例代码 + var isActive bool // 全局变量声明 + var enabled, disabled = true, false // 忽略类型的声明 + func test() { + var available bool // 一般声明 + valid := false // 简短声明 + available = true // 赋值操作 + } + + +### 数值类型 + +整数类型有无符号和带符号两种。Go同时支持`int`和`uint`,这两种类型的长度相同,但具体长度取决于不同编译器的实现。当前的gcc和gccgo编译器在32位和64位平台上都使用32位来表示`int`和`uint`,但未来在64位平台上可能增加到64位。Go里面也有直接定义好位数的类型:`rune`, `int8`, `int16`, `int32`, `int64`和`byte`, `uint8`, `uint16`, `uint32`, `uint64`。其中`rune`是`int32`的别称,`byte`是`uint8`的别称。 + +>需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。 +> +>如下的代码会产生错误 +> +>> var a int8 + +>> var b int32 + +>> c:=a + b +> +>另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。 + +浮点数的类型有`float32`和`float64`两种(没有`float`类型),默认是`float64`。 + +这就是全部吗?No!Go还支持复数。它的默认类型是`complex128`(64位实数+64位虚数)。如果需要小一些的,也有`complex64`(32位实数+32位虚数)。复数的形式为`RE + IMi`,其中`RE`是实数部分,`IM`是虚数部分,而最后的`i`是虚数单位。下面是一个使用复数的例子: + + var c complex64 = 5+5i + //output: (5+5i) + fmt.Printf("Value is: %v", c) + + +### 字符串 + +我们在上一节中讲过,Go中的字符串都是采用`UTF-8`字符集编码。字符串是用一对双引号(`""`)或反引号(`` ` `` `` ` ``)括起来定义,它的类型是`string`。 + + //示例代码 + var frenchHello string // 声明变量为字符串的一般方法 + var emptyString string = "" // 声明了一个字符串变量,初始化为空字符串 + func test() { + no, yes, maybe := "no", "yes", "maybe" // 简短声明,同时声明多个变量 + japaneseHello := "Ohaiou" // 同上 + frenchHello = "Bonjour" // 常规赋值 + } + +在Go中字符串是不可变的,例如下面的代码编译时会报错: + + var s string = "hello" + s[0] = 'c' + + +但如果真的想要修改怎么办呢?下面的代码可以实现: + + s := "hello" + c := []byte(s) // 将字符串 s 转换为 []byte 类型 + c[0] = 'c' + s2 := string(c) // 再转换回 string 类型 + fmt.Printf("%s\n", s2) + + +Go中可以使用`+`操作符来连接两个字符串: + + s := "hello," + m := " world" + a := s + m + fmt.Printf("%s\n", a) + +修改字符串也可写为: + + s := "hello" + s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作 + fmt.Printf("%s\n", s) + +如果要声明一个多行的字符串怎么办?可以通过`` ` ``来声明: + + m := `hello + world` + +`` ` `` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。 + +### 错误类型 +Go内置有一个`error`类型,专门用来处理错误信息,Go的`package`里面还专门有一个包`errors`来处理错误: + + err := errors.New("emit macho dwarf: elf header corrupted") + if err != nil { + fmt.Print(err) + } + +### Go数据底层的存储 + +下面这张图来源于[Russ Cox Blog](http://research.swtch.com/)中一篇介绍[Go数据结构](http://research.swtch.com/godata)的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。 + +![](images/2.2.basic.png?raw=true) + +## 一些技巧 + +### 分组声明 + +在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。 + +例如下面的代码: + + import "fmt" + import "os" + + const i = 100 + const pi = 3.1415 + const prefix = "Go_" + + var i int + var pi float32 + var prefix string + +可以分组写成如下形式: + + import( + "fmt" + "os" + ) + + const( + i = 100 + pi = 3.1415 + prefix = "Go_" + ) + + var( + i int + pi float32 + prefix string + ) + +>除非被显式设置为其它值或`iota`,每个`const`分组的第一个常量被默认设置为它的0值,第二及后续的常量被默认设置为它前面那个常量的值,如果前面那个常量的值是`iota`,则它也被设置为`iota`。 + +### iota枚举 + +Go里面有一个关键字`iota`,这个关键字用来声明`enum`的时候采用,它默认开始值是0,每调用一次加1: + + const( + x = iota // x == 0 + y = iota // y == 1 + z = iota // z == 2 + w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota" + ) + + const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0 + +### Go程序设计的一些规则 +Go之所以会那么简洁,是因为它有一些默认的行为: +- 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。 +- 大写字母开头的函数也是一样,相当于`class`中的带`public`关键词的公有函数;小写字母开头的就是有`private`关键词的私有函数。 + +## array、slice、map + +### array +`array`就是数组,它的定义方式如下: + + var arr [n]type + +在`[n]type`中,`n`表示数组的长度,`type`表示存储元素的类型。对数组的操作和其它语言类似,都是通过`[]`来进行读取或赋值: + + var arr [10]int // 声明了一个int类型的数组 + arr[0] = 42 // 数组下标是从0开始的 + arr[1] = 13 // 赋值操作 + fmt.Printf("The first element is %d\n", arr[0]) // 获取数据,返回42 + fmt.Printf("The last element is %d\n", arr[9]) //返回未赋值的最后一个元素,默认返回0 + +由于长度也是数组类型的一部分,因此`[3]int`与`[4]int`是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的`slice`类型了。 + +数组可以使用另一种`:=`来声明 + + a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组 + + b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0 + + c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度 + +也许你会说,我想数组里面的值还是数组,能实现吗?当然咯,Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组: + + // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 + doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} + + // 如果内部的元素和外部的一样,那么上面的声明可以简化,直接忽略内部的类型 + easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}} + +数组的分配如下所示: + +![](images/2.2.array.png?raw=true) + + +### slice + +在很多应用场景中,数组并不能满足我们的需求。在初始定义数组时,我们并不知道需要多大的数组,因此我们就需要“动态数组”。在Go里面这种数据结构叫`slice` + +`slice`并不是真正意义上的动态数组,而是一个引用类型。`slice`总是指向一个底层`array`,`slice`的声明也可以像`array`一样,只是不需要长度。 + + // 和声明array一样,只是少了长度 + var fslice []int + +接下来我们可以声明一个`slice`,并初始化数据,如下所示: + + slice := []byte {'a', 'b', 'c', 'd'} + +`slice`可以从一个数组或一个已经存在的`slice`中再次声明。`slice`通过`array[i:j]`来获取,其中`i`是数组的开始位置,`j`是结束位置,但不包含`array[j]`,它的长度是`j-i`。 + + // 声明一个含有10个元素元素类型为byte的数组 + var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + + // 声明两个含有byte的slice + var a, b []byte + + // a指向数组的第3个元素开始,并到第五个元素结束, + a = ar[2:5] + //现在a含有的元素: ar[2]、ar[3]和ar[4] + + // b是数组ar的另一个slice + b = ar[3:5] + // b的元素是:ar[3]和ar[4] + +>注意`slice`和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用`...`自动计算长度,而声明`slice`时,方括号内没有任何字符。 + +它们的数据结构如下所示 + +![](images/2.2.slice.png?raw=true) + +slice有一些简便的操作 + + - `slice`的默认开始位置是0,`ar[:n]`等价于`ar[0:n]` + - `slice`的第二个序列默认是数组的长度,`ar[n:]`等价于`ar[n:len(ar)]` + - 如果从一个数组里面直接获取`slice`,可以这样`ar[:]`,因为默认第一个序列是0,第二个是数组的长度,即等价于`ar[0:len(ar)]` + +下面这个例子展示了更多关于`slice`的操作: + + // 声明一个数组 + var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + // 声明两个slice + var aSlice, bSlice []byte + + // 演示一些简便操作 + aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c + aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j + aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素 + + // 从slice中获取slice + aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7 + bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f + bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f + bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h + bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g + +`slice`是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的`aSlice`和`bSlice`,如果修改了`aSlice`中元素的值,那么`bSlice`相对应的值也会改变。 + +从概念上面来说`slice`像一个结构体,这个结构体包含了三个元素: +- 一个指针,指向数组中`slice`指定的开始位置 +- 长度,即`slice`的长度 +- 最大长度,也就是`slice`开始位置到数组的最后位置的长度 + + Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'} + Slice_a := Array_a[2:5] + +上面代码的真正存储结构如下图所示 + +![](images/2.2.slice2.png?raw=true) + +对于`slice`有几个有用的内置函数: + +- `len` 获取`slice`的长度 +- `cap` 获取`slice`的最大容量 +- `append` 向`slice`里面追加一个或者多个元素,然后返回一个和`slice`一样类型的`slice` +- `copy` 函数`copy`从源`slice`的`src`中复制元素到目标`dst`,并且返回复制的元素的个数 + +注:`append`函数会改变`slice`所引用的数组的内容,从而影响到引用同一数组的其它`slice`。 +但当`slice`中没有剩余空间(即`(cap-len) == 0`)时,此时将动态分配新的数组空间。返回的`slice`数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的`slice`则不受影响。 + +### map + +`map`也就是Python中字典的概念,它的格式为`map[keyType]valueType` + +我们看下面的代码,`map`的读取和设置也类似`slice`一样,通过`key`来操作,只是`slice`的`key`只能是`int`类型,而`map`多了很多类型,可以是`int`,可以是`string`及所有完全定义了`==`与`!=`操作的类型。 + + // 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化 + var numbers map[string] int + // 另一种map的声明方式 + numbers := make(map[string]int) + numbers["one"] = 1 //赋值 + numbers["ten"] = 10 //赋值 + numbers["three"] = 3 + + fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据 + // 打印出来如:第三个数字是: 3 + +这个`map`就像我们平常看到的表格一样,左边列是`key`,右边列是值 + +使用map过程中需要注意的几点: +- `map`是无序的,每次打印出来的`map`都会不一样,它不能通过`index`获取,而必须通过`key`获取 +- `map`的长度是不固定的,也就是和`slice`一样,也是一种引用类型 +- 内置的`len`函数同样适用于`map`,返回`map`拥有的`key`的数量 +- `map`的值可以很方便的修改,通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为`11` + +`map`的初始化可以通过`key:val`的方式初始化值,同时`map`内置有判断是否存在`key`的方式 + +通过`delete`删除`map`的元素: + + // 初始化一个字典 + rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 } + // map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true + csharpRating, ok := rating["C#"] + if ok { + fmt.Println("C# is in the map and its rating is ", csharpRating) + } else { + fmt.Println("We have no rating associated with C# in the map") + } + + delete(rating, "C") // 删除key为C的元素 + + +上面说过了,`map`也是一种引用类型,如果两个`map`同时指向一个底层,那么一个改变,另一个也相应的改变: + + m := make(map[string]string) + m["Hello"] = "Bonjour" + m1 := m + m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了 + + +### make、new操作 + +`make`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。 + +内建函数`new`本质上说跟其它语言中的同名函数功能一样:`new(T)`分配了零值填充的`T`类型的内存空间,并且返回其地址,即一个`*T`类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型`T`的零值。有一点非常重要: + +>`new`返回指针。 + +内建函数`make(T, args)`与`new(T)`有着不同的功能,make只能创建`slice`、`map`和`channel`,并且返回一个有初始值(非零)的`T`类型,而不是`*T`。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个`slice`,是一个包含指向数据(内部`array`)的指针、长度和容量的三项描述符;在这些项目被初始化之前,`slice`为`nil`。对于`slice`、`map`和`channel`来说,`make`初始化了内部的数据结构,填充适当的值。 + +>`make`返回初始化后的(非零)值。 + +下面这个图详细的解释了`new`和`make`之间的区别。 + +![](images/2.2.makenew.png?raw=true) + +关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 +此处罗列 部分类型 的 “零值” + + int 0 + int8 0 + int32 0 + int64 0 + uint 0x0 + rune 0 //rune的实际类型是 int32 + byte 0x0 // byte的实际类型是 uint8 + float32 0 //长度为 4 byte + float64 0 //长度为 8 byte + bool false + string "" + +## links + * [目录]() + * 上一章: [你好,Go](<2.1.md>) + * 下一节: [流程和函数](<2.3.md>) diff --git a/2.3.md b/2.3.md index 11dc24d7..def2fd0c 100644 --- a/2.3.md +++ b/2.3.md @@ -1,462 +1,518 @@ -# 2.3 流程和函数 -这小节我们要介绍Go里面的流程控制以及函数操作 -## 流程控制 -流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。流程控制包含分三大类:条件判断,循环控制和无条件跳转。 -### if -`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。 - -Go里面`if`条件判断语句中不需要括号,如下代码所示 - - if x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - -Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示 - - // 计算获取值x,然后根据x返回的大小,判断是否大于10。 - if x := computedValue(); x > 10 { - fmt.Println("x is greater than 10") - } else { - fmt.Println("x is less than 10") - } - - //这个地方如果这样调用就编译出错了,因为x是条件里面的变量 - fmt.Println(x) - -多个条件的时候如下所示: - - if integer == 3 { - fmt.Println("The integer is equal to 3") - } else if integer < 3 { - fmt.Println("The integer is less than 3") - } else { - fmt.Println("The integer is greater than 3") - } - -### goto - -Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环: - - func myFunc() { - i := 0 - Here: //这行的第一个词,以冒号结束作为标签 - println(i) - i++ - goto Here //跳转到Here去 - } - ->标签名是大小写敏感的。 - -### for -Go里面最强大的一个控制逻辑就是`for`,它即可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下: - - for expression1; expression2; expression3 { - //... - } - -`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。 - -一个例子比上面讲那么多更有用,那么我们看看下面的例子吧: - - package main - import "fmt" - - func main(){ - sum := 0; - for index:=0; index < 10 ; index++ { - sum += index - } - fmt.Println("sum is equal to ", sum) - } - // 输出:sum is equal to 45 - -有些时候需要进行多个赋值操作,由于Go里面没有`,`操作,那么可以使用平行赋值`i, j = i+1, j-1` - - -有些时候如果我们忽略`expression1`和`expression3`: - - sum := 1 - for ; sum < 1000; { - sum += sum - } - -其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。 - - sum := 1 - for sum < 1000 { - sum += sum - } - -在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子: - - for index := 10; index>0; index-- { - if index == 5{ - break // 或者continue - } - fmt.Println(index) - } - // break打印出来10、9、8、7、6 - // continue打印出来10、9、8、7、6、4、3、2、1 - -`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环 - -`for`配合`range`可以用于读取`slice`和`map`的数据: - - for k,v:=range map { - fmt.Println("map's key:",k) - fmt.Println("map's val:",v) - } - -由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值 -例如 - - for _, v := range map{ - fmt.Println("map's val:", v) - } - - -### switch -有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下 - - switch sExpr { - case expr1: - some instructions - case expr2: - some other instructions - case expr3: - some other instructions - default: - other code - } - -`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。 - - i := 10 - switch i { - case 1: - fmt.Println("i is equal to 1") - case 2, 3, 4: - fmt.Println("i is equal to 2, 3 or 4") - case 10: - fmt.Println("i is equal to 10") - default: - fmt.Println("All I know is that i is an integer") - } - -在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。 - - integer := 6 - switch integer { - case 4: - fmt.Println("The integer was <= 4") - fallthrough - case 5: - fmt.Println("The integer was <= 5") - fallthrough - case 6: - fmt.Println("The integer was <= 6") - fallthrough - case 7: - fmt.Println("The integer was <= 7") - fallthrough - case 8: - fmt.Println("The integer was <= 8") - fallthrough - default: - fmt.Println("default case") - } - -上面的程序将输出 - - The integer was <= 6 - The integer was <= 7 - The integer was <= 8 - default case - - -## 函数 -函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下: - - func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { - //这里是处理逻辑代码 - //返回多个值 - return value1, value2 - } - -上面的代码我们看出 - -- 关键字`func`用来声明一个函数`funcName` -- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔 -- 函数可以返回多个值 -- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型 -- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号 -- 如果没有返回值,那么就直接省略最后的返回信息 -- 如果有返回值, 那么必须在函数的外层添加return语句 - -下面我们来看一个实际应用函数的例子(用来计算Max值) - - package main - import "fmt" - - // 返回a、b中最大值. - func max(a, b int) int { - if a > b { - return a - } - return b - } - - func main() { - x := 3 - y := 4 - z := 5 - - max_xy := max(x, y) //调用函数max(x, y) - max_xz := max(x, z) //调用函数max(x, z) - - fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) - fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) - fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 - } - -上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。 - -### 多个返回值 -Go语言比C更先进的特性,其中一点就是函数能够返回多个值。 - -我们直接上代码看例子 - - package main - import "fmt" - - //返回 A+B 和 A*B - func SumAndProduct(A, B int) (int, int) { - return A+B, A*B - } - - func main() { - x := 3 - y := 4 - - xPLUSy, xTIMESy := SumAndProduct(x, y) - - fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) - fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) - } - -上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。 - - func SumAndProduct(A, B int) (add int, Multiplied int) { - add = A+B - Multiplied = A*B - return - } - -### 变参 -Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: - - func myfunc(arg ...int) {} -`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`: - - for _, n := range arg { - fmt.Printf("And the number is: %d\n", n) - } - -### 传值与传指针 -当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。 - -为了验证我们上面的说法,我们来看一个例子 - - package main - import "fmt" - - //简单的一个函数,实现了参数+1的操作 - func add1(a int) int { - a = a+1 // 我们改变了a的值 - return a //返回一个新值 - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // 应该输出 "x = 3" - - x1 := add1(x) //调用add1(x) - - fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4" - fmt.Println("x = ", x) // 应该输出"x = 3" - } - -看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化 - -理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。 - -那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢? - -这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子 - - package main - import "fmt" - - //简单的一个函数,实现了参数+1的操作 - func add1(a *int) int { // 请注意, - *a = *a+1 // 修改了a的值 - return *a // 返回新值 - } - - func main() { - x := 3 - - fmt.Println("x = ", x) // 应该输出 "x = 3" - - x1 := add1(&x) // 调用 add1(&x) 传x的地址 - - fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" - fmt.Println("x = ", x) // 应该输出 "x = 4" - } - -这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢? - -- 传指针使得多个函数能操作同一个对象。 -- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。 -- Go语言中`string`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) - -### defer -Go里面有一个不错的设计,就是回调函数,有点类似面向对象语言里面的析构函数,当函数执行完之后再执行。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: - - func ReadWrite() bool { - file.Open("file") - // 做一些工作 - if failureX { - file.Close() - return false - } - - if failureY { - file.Close() - return false - } - - file.Close() - return true - } - -我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。 - - func ReadWrite() bool { - file.Open("file") - defer file.Close() - if failureX { - return false - } - if failureY { - return false - } - return true - } - -如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0` - - for i := 0; i < 5; i++ { - defer fmt.Printf("%d ", i) - } - -### 函数作为值、类型 - -在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 - - type typeName func(input1 inputType1 [, input2 inputType2 [, ...]) (result1 resultType1 [, ...]) - -函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子 - - package main - import "fmt" - - type testInt func(int) bool // 声明了一个函数类型 - - func isOdd(integer int) bool { - if integer%2 == 0 { - return false - } - return true - } - - func isEven(integer int) bool { - if integer%2 == 0 { - return true - } - return false - } - - // 声明的函数类型在这个地方当做了一个参数 - - func filter(slice []int, f testInt) []int { - var result []int - for _, value := range slice { - if f(value) { - result = append(result, value) - } - } - return result - } - - func main(){ - slice := []int {1, 2, 3, 4, 5, 7} - fmt.Println("slice = ", slice) - odd := filter(slice, isOdd) // 函数当做值来传递了 - fmt.Println("Odd elements of slice are: ", odd) - even := filter(slice, isEven) // 函数当做值来传递了 - fmt.Println("Even elements of slice are: ", even) - } - -函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。 - -### Panic和Recover - -Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢? - -Panic ->是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。恐慌可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。 - -Recover ->是一个内建的函数,可以让进入令人恐慌的流程中的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入恐慌,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。 - -下面这个函数演示了如何在过程中使用`panic` - - var user = os.Getenv("USER") - - func init() { - if user == "" { - panic("no value for $USER") - } - } - -下面这个函数检查作为其参数的函数在执行时是否会产生`panic`: - - func throwsPanic(f func()) (b bool) { - defer func() { - if x := recover(); x != nil { - b = true - } - }() - f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 - return - } - -### `main`函数和`init`函数 - -Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中只写一个`init`函数。 - -Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。 - -程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程: - -![](images/2.3.init.png?raw=true) - - -## links - * [目录]() - * 上一章: [Go基础](<2.2.md>) - * 下一节: [struct类型](<2.4.md>) +# 2.3 流程和函数 +这小节我们要介绍Go里面的流程控制以及函数操作 +## 流程控制 +流程控制在编程语言中是最伟大的发明了,因为有了它,你可以通过很简单的流程描述来表达很复杂的逻辑。流程控制包含分三大类:条件判断,循环控制和无条件跳转。 +### if +`if`也许是各种编程语言中最常见的了,它的语法概括起来就是:如果满足条件就做某事,否则做另一件事。 + +Go里面`if`条件判断语句中不需要括号,如下代码所示 + + if x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + +Go的`if`还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示 + + // 计算获取值x,然后根据x返回的大小,判断是否大于10。 + if x := computedValue(); x > 10 { + fmt.Println("x is greater than 10") + } else { + fmt.Println("x is less than 10") + } + + //这个地方如果这样调用就编译出错了,因为x是条件里面的变量 + fmt.Println(x) + +多个条件的时候如下所示: + + if integer == 3 { + fmt.Println("The integer is equal to 3") + } else if integer < 3 { + fmt.Println("The integer is less than 3") + } else { + fmt.Println("The integer is greater than 3") + } + +### goto + +Go有`goto`语句——请明智地使用它。用`goto`跳转到必须在当前函数内定义的标签。例如假设这样一个循环: + + func myFunc() { + i := 0 + Here: //这行的第一个词,以冒号结束作为标签 + println(i) + i++ + goto Here //跳转到Here去 + } + +>标签名是大小写敏感的。 + +### for +Go里面最强大的一个控制逻辑就是`for`,它即可以用来循环读取数据,又可以当作`while`来控制逻辑,还能迭代操作。它的语法如下: + + for expression1; expression2; expression3 { + //... + } + +`expression1`、`expression2`和`expression3`都是表达式,其中`expression1`和`expression3`是变量声明或者函数调用返回值之类的,`expression2`是用来条件判断,`expression1`在循环开始之前调用,`expression3`在每轮循环结束之时调用。 + +一个例子比上面讲那么多更有用,那么我们看看下面的例子吧: + + package main + import "fmt" + + func main(){ + sum := 0; + for index:=0; index < 10 ; index++ { + sum += index + } + fmt.Println("sum is equal to ", sum) + } + // 输出:sum is equal to 45 + +有些时候需要进行多个赋值操作,由于Go里面没有`,`操作,那么可以使用平行赋值`i, j = i+1, j-1` + + +有些时候如果我们忽略`expression1`和`expression3`: + + sum := 1 + for ; sum < 1000; { + sum += sum + } + +其中`;`也可以省略,那么就变成如下的代码了,是不是似曾相识?对,这就是`while`的功能。 + + sum := 1 + for sum < 1000 { + sum += sum + } + +在循环里面有两个关键操作`break`和`continue` ,`break`操作是跳出当前循环,`continue`是跳过本次循环。当嵌套过深的时候,`break`可以配合标签使用,即跳转至标签所指定的位置,详细参考如下例子: + + for index := 10; index>0; index-- { + if index == 5{ + break // 或者continue + } + fmt.Println(index) + } + // break打印出来10、9、8、7、6 + // continue打印出来10、9、8、7、6、4、3、2、1 + +`break`和`continue`还可以跟着标号,用来跳到多重循环中的外层循环 + +`for`配合`range`可以用于读取`slice`和`map`的数据: + + for k,v:=range map { + fmt.Println("map's key:",k) + fmt.Println("map's val:",v) + } + +由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用`_`来丢弃不需要的返回值 +例如 + + for _, v := range map{ + fmt.Println("map's val:", v) + } + + +### switch +有些时候你需要写很多的`if-else`来实现一些逻辑处理,这个时候代码看上去就很丑很冗长,而且也不易于以后的维护,这个时候`switch`就能很好的解决这个问题。它的语法如下 + + switch sExpr { + case expr1: + some instructions + case expr2: + some other instructions + case expr3: + some other instructions + default: + other code + } + +`sExpr`和`expr1`、`expr2`、`expr3`的类型必须一致。Go的`switch`非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果`switch`没有表达式,它会匹配`true`。 + + i := 10 + switch i { + case 1: + fmt.Println("i is equal to 1") + case 2, 3, 4: + fmt.Println("i is equal to 2, 3 or 4") + case 10: + fmt.Println("i is equal to 10") + default: + fmt.Println("All I know is that i is an integer") + } + +在第5行中,我们把很多值聚合在了一个`case`里面,同时,Go里面`switch`默认相当于每个`case`最后带有`break`,匹配成功后不会自动向下执行其他case,而是跳出整个`switch`, 但是可以使用`fallthrough`强制执行后面的case代码。 + + integer := 6 + switch integer { + case 4: + fmt.Println("The integer was <= 4") + fallthrough + case 5: + fmt.Println("The integer was <= 5") + fallthrough + case 6: + fmt.Println("The integer was <= 6") + fallthrough + case 7: + fmt.Println("The integer was <= 7") + fallthrough + case 8: + fmt.Println("The integer was <= 8") + fallthrough + default: + fmt.Println("default case") + } + +上面的程序将输出 + + The integer was <= 6 + The integer was <= 7 + The integer was <= 8 + default case + + +## 函数 +函数是Go里面的核心设计,它通过关键字`func`来声明,它的格式如下: + + func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { + //这里是处理逻辑代码 + //返回多个值 + return value1, value2 + } + +上面的代码我们看出 + +- 关键字`func`用来声明一个函数`funcName` +- 函数可以有一个或者多个参数,每个参数后面带有类型,通过`,`分隔 +- 函数可以返回多个值 +- 上面返回值声明了两个变量`output1`和`output2`,如果你不想声明也可以,直接就两个类型 +- 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号 +- 如果没有返回值,那么就直接省略最后的返回信息 +- 如果有返回值, 那么必须在函数的外层添加return语句 + +下面我们来看一个实际应用函数的例子(用来计算Max值) + + package main + import "fmt" + + // 返回a、b中最大值. + func max(a, b int) int { + if a > b { + return a + } + return b + } + + func main() { + x := 3 + y := 4 + z := 5 + + max_xy := max(x, y) //调用函数max(x, y) + max_xz := max(x, z) //调用函数max(x, z) + + fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) + fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz) + fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在这直接调用它 + } + +上面这个里面我们可以看到`max`函数有两个参数,它们的类型都是`int`,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型,同理多于2个同类型的变量或者返回值。同时我们注意到它的返回值就是一个类型,这个就是省略写法。 + +### 多个返回值 +Go语言比C更先进的特性,其中一点就是函数能够返回多个值。 + +我们直接上代码看例子 + + package main + import "fmt" + + //返回 A+B 和 A*B + func SumAndProduct(A, B int) (int, int) { + return A+B, A*B + } + + func main() { + x := 3 + y := 4 + + xPLUSy, xTIMESy := SumAndProduct(x, y) + + fmt.Printf("%d + %d = %d\n", x, y, xPLUSy) + fmt.Printf("%d * %d = %d\n", x, y, xTIMESy) + } + +上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。 + + func SumAndProduct(A, B int) (add int, Multiplied int) { + add = A+B + Multiplied = A*B + return + } + +### 变参 +Go函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参: + + func myfunc(arg ...int) {} +`arg ...int`告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是`int`。在函数体中,变量`arg`是一个`int`的`slice`: + + for _, n := range arg { + fmt.Printf("And the number is: %d\n", n) + } + +### 传值与传指针 +当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。 + +为了验证我们上面的说法,我们来看一个例子 + + package main + import "fmt" + + //简单的一个函数,实现了参数+1的操作 + func add1(a int) int { + a = a+1 // 我们改变了a的值 + return a //返回一个新值 + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // 应该输出 "x = 3" + + x1 := add1(x) //调用add1(x) + + fmt.Println("x+1 = ", x1) // 应该输出"x+1 = 4" + fmt.Println("x = ", x) // 应该输出"x = 3" + } + +看到了吗?虽然我们调用了`add1`函数,并且在`add1`中执行`a = a+1`操作,但是上面例子中`x`变量的值没有发生变化 + +理由很简单:因为当我们调用`add1`的时候,`add1`接收的参数其实是`x`的copy,而不是`x`本身。 + +那你也许会问了,如果真的需要传这个`x`本身,该怎么办呢? + +这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有`add1`函数知道`x`变量所在的地址,才能修改`x`变量的值。所以我们需要将`x`所在地址`&x`传入函数,并将函数的参数的类型由`int`改为`*int`,即改为指针类型,才能在函数中修改`x`变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。请看下面的例子 + + package main + import "fmt" + + //简单的一个函数,实现了参数+1的操作 + func add1(a *int) int { // 请注意, + *a = *a+1 // 修改了a的值 + return *a // 返回新值 + } + + func main() { + x := 3 + + fmt.Println("x = ", x) // 应该输出 "x = 3" + + x1 := add1(&x) // 调用 add1(&x) 传x的地址 + + fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4" + fmt.Println("x = ", x) // 应该输出 "x = 4" + } + +这样,我们就达到了修改`x`的目的。那么到底传指针有什么好处呢? + +- 传指针使得多个函数能操作同一个对象。 +- 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。 +- Go语言中`string`,`slice`,`map`这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变`slice`的长度,则仍需要取地址传递指针) + +### defer +Go里面有一个不错的设计,就是回调函数,有点类似面向对象语言里面的析构函数,当函数执行完之后再执行。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。如下代码所示,我们一般写打开一个资源是这样操作的: + + func ReadWrite() bool { + file.Open("file") + // 做一些工作 + if failureX { + file.Close() + return false + } + + if failureY { + file.Close() + return false + } + + file.Close() + return true + } + +我们看到上面有很多重复的代码,Go的`defer`有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在`defer`后指定的函数会在函数退出前调用。 + + func ReadWrite() bool { + file.Open("file") + defer file.Close() + if failureX { + return false + } + if failureY { + return false + } + return true + } + +如果有很多调用`defer`,那么`defer`是采用后进先出模式,所以如下代码会输出`4 3 2 1 0` + + for i := 0; i < 5; i++ { + defer fmt.Printf("%d ", i) + } + +### 函数作为值、类型 + +在Go中函数也是一种变量,我们可以通过`type`来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型 + + type typeName func(input1 inputType1 [, input2 inputType2 [, ...]) (result1 resultType1 [, ...]) + +函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递,请看下面的例子 + + package main + import "fmt" + + type testInt func(int) bool // 声明了一个函数类型 + + func isOdd(integer int) bool { + if integer%2 == 0 { + return false + } + return true + } + + func isEven(integer int) bool { + if integer%2 == 0 { + return true + } + return false + } + + // 声明的函数类型在这个地方当做了一个参数 + + func filter(slice []int, f testInt) []int { + var result []int + for _, value := range slice { + if f(value) { + result = append(result, value) + } + } + return result + } + + func main(){ + slice := []int {1, 2, 3, 4, 5, 7} + fmt.Println("slice = ", slice) + odd := filter(slice, isOdd) // 函数当做值来传递了 + fmt.Println("Odd elements of slice are: ", odd) + even := filter(slice, isEven) // 函数当做值来传递了 + fmt.Println("Even elements of slice are: ", even) + } + +函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到`testInt`这个类型是一个函数类型,然后两个`filter`函数的参数和返回值与`testInt`类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。 + +### Panic和Recover + +Go没有像Java那样的异常机制,它不能抛出异常,而是使用了`panic`和`recover`机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有`panic`的东西。这是个强大的工具,请明智地使用它。那么,我们应该如何使用它呢? + +Panic +>是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数`F`调用`panic`,函数F的执行被中断,但是`F`中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,`F`的行为就像调用了`panic`。这一过程继续向上,直到发生`panic`的`goroutine`中所有调用的函数返回,此时程序退出。恐慌可以直接调用`panic`产生。也可以由运行时错误产生,例如访问越界的数组。 + +Recover +>是一个内建的函数,可以让进入令人恐慌的流程中的`goroutine`恢复过来。`recover`仅在延迟函数中有效。在正常的执行过程中,调用`recover`会返回`nil`,并且没有其它任何效果。如果当前的`goroutine`陷入恐慌,调用`recover`可以捕获到`panic`的输入值,并且恢复正常的执行。 + +下面这个函数演示了如何在过程中使用`panic` + + var user = os.Getenv("USER") + + func init() { + if user == "" { + panic("no value for $USER") + } + } + +下面这个函数检查作为其参数的函数在执行时是否会产生`panic`: + + func throwsPanic(f func()) (b bool) { + defer func() { + if x := recover(); x != nil { + b = true + } + }() + f() //执行函数f,如果f中出现了panic,那么就可以恢复回来 + return + } + +### `main`函数和`init`函数 + +Go里面有两个保留的函数:`init`函数(能够应用于所有的`package`)和`main`函数(只能应用于`package main`)。这两个函数在定义时不能有任何的参数和返回值。虽然一个`package`里面可以写任意多个`init`函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个`package`中只写一个`init`函数。 + +Go程序会自动调用`init()`和`main()`,所以你不需要在任何地方调用这两个函数。每个`package`中的`init`函数都是可选的,但`package main`就必须包含一个`main`函数。 + +程序的初始化和执行都起始于`main`包。如果`main`包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到`fmt`包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行`init`函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对`main`包中的包级常量和变量进行初始化,然后执行`main`包中的`init`函数(如果存在的话),最后执行`main`函数。下图详细地解释了整个执行过程: + +![](images/2.3.init.png?raw=true) + +### import +我们在写Go代码的时候经常用到import这个命令用来导入包文件,而我们经常看到的方式参考如下: + + import( + "fmt" + ) + +然后我们代码里面可以通过如下的方式调用 + + fmt.Println("hello world") + +上面这个fmt是Go语言的标准库,其实是去goroot下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块: + +1. 相对路径 + + import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import + +2. 绝对路径 + + import “shorturl/model” //加载gopath/src/shorturl/model模块 + + +上面展示了一些import常用的几种方式,但是还有一些特殊的import,让很多新手很费解,下面我们来一一讲解一下到底是怎么一回事 + + +1. 点操作 + + 我们有时候会看到如下的方式导入包 + + import( + . "fmt" + ) + + 这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world") + +2. 别名操作 + + 别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字 + + import( + f "fmt" + ) + + 别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println("hello world") + +3. _操作 + + 这个操作经常是让很多人费解的一个操作符,请看下面这个import + + import ( + "database/sql" + _ "github.com/ziutek/mymysql/godrv" + ) + + _操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。 + + +## links + * [目录]() + * 上一章: [Go基础](<2.2.md>) + * 下一节: [struct类型](<2.4.md>) diff --git a/2.7.md b/2.7.md index 4f001cc0..4d89f8a5 100644 --- a/2.7.md +++ b/2.7.md @@ -1,190 +1,239 @@ -# 2.7 并发 - -有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而GO从语言层面就支持了并行。 - -## Goroutines - -Goroutines是Go并行设计的核心。Goroutines说到底其实就是线程,但是他比线程更小,十几个Goroutines可能体现在底层就是五六个线程,Go语言内部帮你实现了这些Goroutines之间的内存共享。Go语言的作者经常说着这样一句话,不要通过共享来通信,而要通过通信来共享。 - -Goroutines是通过Go的runtime管理的一个线程管理器。Goroutines通过`go`关键字实现了,其实就是一个普通的函数。 - - go hello(a, b, c) - -通过关键字go就启动了一个Goroutines。我们来看一个例子 - - package main - - import ( - "fmt" - "runtime" - ) - - func say(s string) { - for i := 0; i < 5; i++ { - runtime.Gosched() - fmt.Println(s) - } - } - - func main() { - go say("world") //开一个新的Goroutines执行 - say("hello") //当前Goroutines执行 - } - - 输出: - hello - world - hello - world - hello - world - hello - world - hello - -我们可以看到go关键字很方便的就实现了并发编程。 -runtime.Gosched()表示让CPU把时间片让给别人。 -## channels -Goroutines运行在相同的地址空间,因此访问共享内存必须做好同步。那么Goroutines之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel 时,也需要定义发送到channel 的值的类型。注意,必须使用make 创建channel: - - ci := make(chan int) - cs := make(chan string) - cf := make(chan interface{}) - -channel通过操作符`<-`来接收和发送数据 - - ch <- v // 发送v到channel ch. - v := <-ch // 从ch中接收数据,并赋值给v - -我们把这些应用到我们的例子中来: - - package main - - import "fmt" - - func sum(a []int, c chan int) { - sum := 0 - for _, v := range a { - sum += v - } - c <- sum // send sum to c - } - - func main() { - a := []int{7, 2, 8, -9, 4, 0} - - c := make(chan int) - go sum(a[:len(a)/2], c) - go sum(a[len(a)/2:], c) - x, y := <-c, <-c // receive from c - - fmt.Println(x, y, x + y) - } - -默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel 是在多个goroutine之间同步很棒的工具。 - -## Buffered Channels -上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。 - - ch := make(chan type, value) - - value == 0 ! 无缓冲(阻塞) - value > 0 ! 缓冲(非阻塞,直到value 个元素) - -我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的value值 - - - package main - - import "fmt" - - func main() { - c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 - c <- 1 - c <- 2 - fmt.Println(<-c) - fmt.Println(<-c) - } - -## Range和Close -上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子 - - package main - - import ( - "fmt" - ) - - func fibonacci(n int, c chan int) { - x, y := 1, 1 - for i := 0; i < n; i++ { - c <- x - x, y = y, x + y - } - close(c) - } - - func main() { - c := make(chan int, 10) - go fibonacci(cap(c), c) - for i := range c { - fmt.Println(i) - } - } - -`for i := range c`能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过关键字`close`函数关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法`v, ok := <-ch`测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。 - ->记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic - ->另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的 - -## Select -我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字`select`,通过`select`可以监听channel上的数据流动。 - -`select`默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。 - - package main - - import "fmt" - - func fibonacci(c, quit chan int) { - x, y := 1, 1 - for { - select { - case c <- x: - x, y = y, x + y - case <-quit: - fmt.Println("quit") - return - } - } - } - - func main() { - c := make(chan int) - quit := make(chan int) - go func() { - for i := 0; i < 10; i++ { - fmt.Println(<-c) - } - quit <- 0 - }() - fibonacci(c, quit) - } - -虽然goroutine是并发执行的,但是它们并不是并行运行的。如果不告诉Go额外的东西,同一时刻只会有一个goroutine执行逻辑代码。利用runtime.GOMAXPROCS(n)可以设置goroutine并行执行的数量。GOMAXPROCS 设置了同时运行逻辑代码的系统线程 的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。以后Go的新版本中调度得到改进后,这将被移除。 - -在`select`里面还有default语法,`select`其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。 - - select { - case i := <-c: - // use i - default: - // 当c阻塞的时候执行这里 - } - - -## links - * [目录]() - * 上一章: [interface](<2.6.md>) - * 下一节: [总结](<2.8.md>) +# 2.7 并发 + +有人把Go比作21世纪的C语言,第一是因为Go语言设计简单,第二,21世纪最重要的就是并行程序设计,而GO从语言层面就支持了并行。 + +## goroutine + +goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。 + +goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过`go`关键字实现了,其实就是一个普通的函数。 + + go hello(a, b, c) + +通过关键字go就启动了一个goroutine。我们来看一个例子 + + package main + + import ( + "fmt" + "runtime" + ) + + func say(s string) { + for i := 0; i < 5; i++ { + runtime.Gosched() + fmt.Println(s) + } + } + + func main() { + go say("world") //开一个新的Goroutines执行 + say("hello") //当前Goroutines执行 + } + + 输出: + hello + world + hello + world + hello + world + hello + world + hello + +我们可以看到go关键字很方便的就实现了并发编程。 +上面的多个goroutine运行在同一个进程里面,共享内存数据,不过设计上我们要遵循:不要通过共享来通信,而要通过通信来共享。 + +> runtime.Gosched()表示让CPU把时间片让给别人,结束当前的goroutine。 + +>默认情况下,调度器仅使用单线程,也就是说只实现了并发。想要发挥多核处理器的并行,需要在我们的程序中显示的调用 runtime.GOMAXPROCS(n) 告诉调度器同时使用多个线程。GOMAXPROCS 设置了同时运行逻辑代码的系统线程的最大数量,并返回之前的设置。如果n < 1,不会改变当前设置。以后Go的新版本中调度得到改进后,这将被移除。这里有一篇rob介绍的关于并发和并行的文章:http://concur.rspace.googlecode.com/hg/talk/concur.html#landing-slide + +## channels +goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。注意,必须使用make 创建channel: + + ci := make(chan int) + cs := make(chan string) + cf := make(chan interface{}) + +channel通过操作符`<-`来接收和发送数据 + + ch <- v // 发送v到channel ch. + v := <-ch // 从ch中接收数据,并赋值给v + +我们把这些应用到我们的例子中来: + + package main + + import "fmt" + + func sum(a []int, c chan int) { + sum := 0 + for _, v := range a { + sum += v + } + c <- sum // send sum to c + } + + func main() { + a := []int{7, 2, 8, -9, 4, 0} + + c := make(chan int) + go sum(a[:len(a)/2], c) + go sum(a[len(a)/2:], c) + x, y := <-c, <-c // receive from c + + fmt.Println(x, y, x + y) + } + +默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。 + +## Buffered Channels +上面我们介绍了默认的非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。 + + ch := make(chan type, value) + + value == 0 ! 无缓冲(阻塞) + value > 0 ! 缓冲(非阻塞,直到value 个元素) + +我们看一下下面这个例子,你可以在自己本机测试一下,修改相应的value值 + + + package main + + import "fmt" + + func main() { + c := make(chan int, 2)//修改2为1就报错,修改2为3可以正常运行 + c <- 1 + c <- 2 + fmt.Println(<-c) + fmt.Println(<-c) + } + +## Range和Close +上面这个例子中,我们需要读取两次c,这样不是很方便,Go考虑到了这一点,所以也可以通过range,像操作slice或者map一样操作缓存类型的channel,请看下面的例子 + + package main + + import ( + "fmt" + ) + + func fibonacci(n int, c chan int) { + x, y := 1, 1 + for i := 0; i < n; i++ { + c <- x + x, y = y, x + y + } + close(c) + } + + func main() { + c := make(chan int, 10) + go fibonacci(cap(c), c) + for i := range c { + fmt.Println(i) + } + } + +`for i := range c`能够不断的读取channel里面的数据,直到该channel被显式的关闭。上面代码我们看到可以显式的关闭channel,生产者通过关键字`close`函数关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法`v, ok := <-ch`测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。 + +>记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic + +>另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的 + +## Select +我们上面介绍的都是只有一个channel的情况,那么如果存在多个channel的时候,我们该如何操作呢,Go里面提供了一个关键字`select`,通过`select`可以监听channel上的数据流动。 + +`select`默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。 + + package main + + import "fmt" + + func fibonacci(c, quit chan int) { + x, y := 1, 1 + for { + select { + case c <- x: + x, y = y, x + y + case <-quit: + fmt.Println("quit") + return + } + } + } + + func main() { + c := make(chan int) + quit := make(chan int) + go func() { + for i := 0; i < 10; i++ { + fmt.Println(<-c) + } + quit <- 0 + }() + fibonacci(c, quit) + } + +在`select`里面还有default语法,`select`其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。 + + select { + case i := <-c: + // use i + default: + // 当c阻塞的时候执行这里 + } + +## 超时 +有时候会出现goroutine阻塞的情况,那么我们如何避免整个的程序进入阻塞的情况呢?我们可以利用select来设置超时,通过如下的方式实现: + + func main() { + c := make(chan int) + o := make(chan bool) + go func() { + for { + select { + case v := <- c: + println(v) + case <- time.After(5 * time.Second): + println("timeout") + o <- true + break + } + } + }() + <- o + } + + +## runtime goroutine +runtime包中有几个处理goroutine的函数: + +- Goexit + + 退出当前执行的goroutine,但是defer函数还会继续调用 + +- Gosched + + 让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。 + +- NumCPU + + 返回 CPU 核数量 + +- NumGoroutine + + 返回正在执⾏行和排队的任务总数 + +- GOMAXPROCS + + 用来设置可以运行的CPU核数 + + + +## links + * [目录]() + * 上一章: [interface](<2.6.md>) + * 下一节: [总结](<2.8.md>) diff --git a/7.4.md b/7.4.md index 1ac88990..ba08329b 100644 --- a/7.4.md +++ b/7.4.md @@ -345,4 +345,4 @@ Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的 ## links * [目录]() * 上一节: [正则处理](<7.3.md>) - * 下一节: [小结](<7.5.md>) + * 下一节: [文件操作](<7.5.md>) diff --git a/7.5.md b/7.5.md index f6818565..dba718e5 100644 --- a/7.5.md +++ b/7.5.md @@ -1,7 +1,153 @@ -# 7.5 小结 -这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。 - -## links - * [目录]() - * 上一节: [模板处理](<7.4.md>) - * 下一节: [Web服务](<8.md>) +# 7.5 文件操作 +在任何计算机设备中,文件是都是必须的对象,而在Web编程中,文件的操作一直是Web程序员经常遇到的问题,文件操作在Web应用中是必须的,非常有用的,我们经常遇到生成文件目录,文件(夹)编辑等操作,现在我把Go中的这些操作做一详细总结并实例示范如何使用。 +## 目录操作 +文件操作的大多数函数都是在os包里面,下面列举了几个目录操作的: + +- func Mkdir(name string, perm FileMode) error + + 创建名称为name的目录,权限设置是perm,例如0777 + +- func MkdirAll(path string, perm FileMode) error + + 根据path创建多级子目录,例如astaxie/test1/test2。 + +- func Remove(name string) error + + 删除名称为name的目录,当目录下有文件或者其他目录是会出错 + +- func RemoveAll(path string) error + + 根据path删除多级子目录,如果path是单个名称,那么该目录不删除。 + + +下面是演示代码: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + os.Mkdir("astaxie", 0777) + os.MkdirAll("astaxie/test1/test2", 0777) + err := os.Remove("astaxie") + if err != nil { + fmt.Println(err) + } + os.RemoveAll("astaxie") + } + + +## 文件操作 + +### 建立与打开文件 +新建文件可以通过如下两个方法 + +- func Create(name string) (file *File, err Error) + + 根据提供的文件名创建新的文件,返回一个文件对象,默认权限是0666的文件,返回的文件对象是可读写的。 + +- func NewFile(fd uintptr, name string) *File + + 根据文件描述符创建相应的文件,返回一个文件对象 + + +通过如下两个方法来打开文件: + +- func Open(name string) (file *File, err Error) + + 该方法打开一个名称为name的文件,但是是只读方式,内部实现其实调用了OpenFile。 + +- func OpenFile(name string, flag int, perm uint32) (file *File, err Error) + + 打开名称为name的文件,flag是打开的方式,只读、读写等,perm是权限 + +### 写文件 +写文件函数: + +- func (file *File) Write(b []byte) (n int, err Error) + + 写入byte类型的信息到文件 + +- func (file *File) WriteAt(b []byte, off int64) (n int, err Error) + + 在指定位置开始写入byte类型的信息 + +- func (file *File) WriteString(s string) (ret int, err Error) + + 写入string信息到文件 + +写文件的示例代码 + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "astaxie.txt" + fout, err := os.Create(userFile) + defer fout.Close() + if err != nil { + fmt.Println(userFile, err) + return + } + for i := 0; i < 10; i++ { + fout.WriteString("Just a test!\r\n") + fout.Write([]byte("Just a test!\r\n")) + } + } + +### 读文件 +读文件函数: + +- func (file *File) Read(b []byte) (n int, err Error) + + 读取数据到b中 + +- func (file *File) ReadAt(b []byte, off int64) (n int, err Error) + + 从off开始读取数据到b中 + +读文件的示例代码: + + package main + + import ( + "fmt" + "os" + ) + + func main() { + userFile := "asatxie.txt" + fl, err := os.Open(userFile) + defer fl.Close() + if err != nil { + fmt.Println(userFile, err) + return + } + buf := make([]byte, 1024) + for { + n, _ := fl.Read(buf) + if 0 == n { + break + } + os.Stdout.Write(buf[:n]) + } + } + +### 删除文件 +Go语言里面删除文件和删除文件夹是同一个函数 + +- func Remove(name string) Error + + 调用该函数就可以删除文件名为name的文件 + +## links + * [目录]() + * 上一节: [模板处理](<7.4.md>) + * 下一节: [小结](<7.6.md>) \ No newline at end of file diff --git a/7.6.md b/7.6.md new file mode 100644 index 00000000..170770c8 --- /dev/null +++ b/7.6.md @@ -0,0 +1,7 @@ +# 7.5 小结 +这一章给大家介绍了一些文本处理的工具,包括XML、JSON、正则和模板技术,XML和JSON是数据交互的工具,通过XML和JSON你可以表达各种含义,通过正则你可以处理文本(搜索、替换、截取),通过模板技术你可以展现这些数据给用户。这些都是你开发Web应用过程中需要用到的技术,通过这个小节的介绍你能够了解如何处理文本、展现文本。 + +## links + * [目录]() + * 上一节: [文件操作](<7.5.md>) + * 下一节: [Web服务](<8.md>) diff --git a/7.md b/7.md index dc77f24f..ef27c5a0 100644 --- a/7.md +++ b/7.md @@ -1,16 +1,16 @@ -# 7 文本处理 -Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。 - -XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。 - -## 目录 - * 1. [XML处理](7.1.md) - * 2. [JSON处理](7.2.md) - * 3. [正则处理](7.3.md) - * 4. [模板处理](7.4.md) - * 5. [小结](7.5.md) - -## links - * [目录]() - * 上一章: [第六章总结](<6.5.md>) - * 下一节: [XML处理](<7.1.md>) +# 7 文本处理 +Web开发中对于文本处理是非常重要的一部分,我们往往需要对输出或者输入的内容进行处理,这里的文本包括字符串、数字、Json、XMl等等。Go语言作为一门高性能的语言,对这些文本的处理都有官方的标准库来支持。而且在你使用中你会发现Go标准库的一些设计相当的巧妙,而且对于使用者来说也很方便就能处理这些文本。本章我们将通过四个小节的介绍,让用户对Go语言处理文本有一个很好的认识。 + +XML是目前很多标准接口的交互语言,很多时候和一些Java编写的webserver进行交互都是基于XML标准进行交互,7.1小节将介绍如何处理XML文本,我们使用XML之后发现它太复杂了,现在很多互联网企业对外的API大多数采用了JSON格式,这种格式描述简单,但是又能很好的表达意思,7.2小节我们将讲述如何来处理这样的JSON格式数据。正则是一个让人又爱又恨的工具,它处理文本的能力非常强大,我们在前面表单验证里面已经有所领略它的强大,7.3小节将详细的更深入的讲解如何利用好Go的正则。Web开发中一个很重要的部分就是MVC分离,在Go语言的Web开发中V有一个专门的包来支持`template`,7.4小节将详细的讲解如何使用模版来进行输出内容。7.5小节讲详细介绍如何进行文件和文件夹的操作。 + +## 目录 + * 1. [XML处理](7.1.md) + * 2. [JSON处理](7.2.md) + * 3. [正则处理](7.3.md) + * 4. [模板处理](7.4.md) + * 5. [小结](7.5.md) + +## links + * [目录]() + * 上一章: [第六章总结](<6.5.md>) + * 下一节: [XML处理](<7.1.md>) diff --git a/preface.md b/preface.md index 651b000a..11623a49 100644 --- a/preface.md +++ b/preface.md @@ -40,12 +40,13 @@ - 6.3 [session存储](6.3.md) - 6.4 [预防session劫持](6.4.md) - 6.5 [小结](6.5.md) -* 7.[文本处理](7.md) +* 7.[文本文件处理](7.md) - 7.1 [XML处理](7.1.md) - 7.2 [JSON处理](7.2.md) - 7.3 [正则处理](7.3.md) - 7.4 [模板处理](7.4.md) - - 7.5 [小结](7.5.md) + - 7.5 [文件操作](7.5.md) + - 7.6 [小结](7.6.md) * 8.[Web服务](8.md) - 8.1 [Socket编程](8.1.md) - 8.2 [WebSocket](8.2.md)