Go语言圣经读书笔记
2022-11-6
| 2023-2-22
0  |  0 分钟
password
Created
Feb 19, 2023 04:40 PM
type
Post
status
Published
date
Nov 6, 2022
slug
summary
Go语言圣经读书笔记
tags
Go
Go语言圣经
category
Go
icon

入门

前言

为了保证不破坏原有完整概念的前提下保持自适应, 可以实现小的修改就能实现新的需要.
但不断实现新的需要的过程中, 又会丢失相应的语言简洁度.
语言简洁的设计能够让一个系统保持稳定, 安全和持续进化
 
Go语言的特点: 简洁
  1. 标准库成熟且保证向后兼容
  1. 足够且简洁的类型系统
  1. 安全的内置数据类型和标准库的初始化
  1. 轻量, 代价极小的并发goroutine支持
 

hello world

Go语言的代码通过包(package)组织, 包类似于其他语言里的模块或库的概念.
一个包由位于单个目录下的一个或多.go源代码文件组成, 目录定义包的作用.
每个源文件都以一条package声明语句开始
Go语言的特色:
  1. 必须确定导入需要哪些包, 缺少包或多包, 程序都无法编译通过
  1. import 声明必须跟在文件的 package 声明之后。随后,则是组成程序的函数、变量、常量、类型的声明语句(分别由关键字 funcvarconsttype 定义)。这些内容的声明顺序并不重要.
  1. 一个函数的声明由 func 关键字、函数名、参数列表、返回值列表以及包含在大括号里的函数体组成。
  1. 不需要在语句或者声明的末尾添加分号
  1. Go 语言在代码格式上采取了很强硬的态度。gofmt工具把代码格式化为标准格式, 且此工具不会提供任何调整代码格式的参数.
 

命令行参数

可以通过os.Args[i]来获取命令行的参数数组
 
printf的转换参数
%d 十进制整数 %x, %o, %b 十六进制,八进制,二进制整数。 %f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00 %t 布尔:true或false %c 字符(rune) (Unicode码点) %s 字符串 %q 带双引号的字符串"abc"或带单引号的字符'c' %v 变量的自然形式(natural format) %T 变量的类型 %% 字面上的百分号标志(无操作数)

程序结构

命名

关键字
break default func interface select case defer go map struct chan else goto package switch const fallthrough if range type continue for import return var
预定义的名字
内建常量: true false iota nil 内建类型: int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr float32 float64 complex128 complex64 bool byte rune string error 内建函数: make len cap new append copy close delete complex real imag panic recover
 

变量

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。变量声明的一般语法如下:
var 变量名字 类型 = 表达式
零值初始化机制
var s string fmt.Println(s) // ""
同时声明一组变量
var i, j, k int // int, int, int var b, f, s = true, 2.3, "four" // bool, float64, string
调用一个函数,由函数返回的多个返回值初始化
var f, err = os.Open(name) // os.Open returns a file and an error
简短变量声明语句: 名字 := 表达式, 只能在函数内部使用
anim := gif.GIF{LoopCount: nframes} freq := rand.Float64() * 3.0 t := 0.0
将右边各个表达式的值赋值给左边对应位置的各个变量
i, j = j, i // 交换 i 和 j 的值
简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过
f, err := os.Open(infile) // ... f, err := os.Create(outfile) // compile error: no new variables
 

指针

一个指针的值是另一个变量的地址。一个指针对应变量在内存中的存储位置。
并不是每一个值都会有一个内存地址,但是对于每一个变量必然有对应的内存地址。
x := 1 p := &x // p, of type *int, points to x fmt.Println(*p) // "1" *p = 2 // equivalent to x = 2 fmt.Println(x) // "2"
任何类型的指针的零值都是nil。如果p指向某个有效变量,那么p != nil测试为真。
var x, y int fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
可以通过指针来更新变量的值
func incr(p *int) int { *p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!! return *p } v := 1 incr(&v) // side effect: v is now 2 fmt.Println(incr(&v)) // "3" (and v is 3)
 

new 函数

表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T
p := new(int) // p, *int 类型, 指向匿名的 int 变量 fmt.Println(*p) // "0" *p = 2 // 设置 int 匿名变量的值为 2 fmt.Println(*p) // "2"
 

变量的生命周期

包一级声明的变量: 和整个程序运行周期一致
局部变量: 从创建一个新变量的声明语句开始, 知道该变量不再被引用位置, 函数的参数变量和返回值变量都是局部变量
 
需要注意全局变量的引用关系, 可能会在后续程序执行时造成内存占用过多
var global *int func f() { var x int x = 1 global = &x } func g() { y := new(int) *y = 1 }
 

赋值

使用赋值语句可以更新一个变量的值
x = 1 // 命名变量的赋值 *p = true // 通过指针间接赋值 person.name = "bob" // 结构体字段赋值 count[x] = count[x] * scale // 数组、slice或map的元素赋值 count[x] *= scale // 上面也可以改写为这种
赋值支持自增和自减
v := 1 v++ // 等价方式 v = v + 1;v 变成 2 v-- // 等价方式 v = v - 1;v 变成 1
支持元组直接赋值
但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。
x, y = y, x a[i], a[j] = a[j], a[i]
有些函数会用额外的返回值来表达某种错误类型
同时, 和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值
v = m[key] // map查找,失败时返回零值 v = x.(T) // type断言,失败时panic异常 v = <-ch // 管道接收,失败时返回零值(阻塞不算是失败) _, ok = m[key] // map返回2个值 _, ok = mm[""], false // map返回1个值 _ = mm[""] // map返回1个值
 

可赋值性

显式赋值
medals := []string{"gold", "silver", "bronze"}
隐式赋值
medals[0] = "gold" medals[1] = "silver" medals[2] = "bronze"
 

类型

变量或表达式的类型定义了对应存储值的属性特征
type 类型名字 底层类型
类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。
对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出;
 

包和文件

通常一个包所在目录路径的后缀是包的导入路径
每个包都对应一个独立的名字空间。
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在Go语言中,一个简单的规则是:如果一个名字是大写字母开头的,那么该名字是导出的(译注:因为汉字不区分大小写,因此汉字开头的名字是没有导出的)。
 

包的初始化

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化
var a = b + c // a 第三个初始化, 为 3 var b = f() // b 第二个初始化, 为 2, 通过调用 f (依赖c) var c = 1 // c 第一个初始化, 为 1 func f() int { return c + 1 }
如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。
对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数
func init() { /* ... */ }
这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次
 

作用域

声明语句的作用域是指源代码中可以有效使用这个名字的范围。
句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。
对于内置的类型、函数和常量,比如int、len和true等是在全局作用域的,因此可以在整个程序中直接使用。
在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。

基础数据类型

Go语言的数值类型包括几种不同大小的整数、浮点数和复数。每种数值类型都决定了对应的大小范围和是否支持正负符号。

整型

Go语言同时提供了有符号和无符号类型的整数运算。
int8、int16、int32和int64
对应8、16、32、64bit大小的有符号整数
uint8、uint16、uint32和uint64四种无符号整数
一般对应特定CPU平台机器字大小的有符号和无符号整数int和uint
int是应用最广泛的数值类型。这两种类型都有同样的大小,32或64bit(不同的编译器即使在相同的硬件平台上可能产生不同的大小)
Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。
同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。
最后,还有一种无符号的整数类型uintptr,没有指定具体的bit大小但是足以容纳指针。uintptr类型只有在底层编程时才需要
日常编程中, 需要注意符号位溢出造成的问题和影响
var u uint8 = 255 fmt.Println(u, u+1, u*u) // "255 0 1" var i int8 = 127 fmt.Println(i, i+1, i*i) // "127 -128 1"
 

浮点数

Go语言提供了两种精度的浮点数,float32和float64。
一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;
 
函数math.IsNaN用于测试一个数是否是非数NaN,math.NaN则返回非数对应的值。
NaN和任何数都是不相等的
nan := math.NaN() fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"
 

复数

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。
内建的real和imag函数分别返回复数的实部和虚部:
var x complex128 = complex(1, 2) // 1+2i var y complex128 = complex(3, 4) // 3+4i fmt.Println(x*y) // "(-5+10i)" fmt.Println(real(x*y)) // "-5" fmt.Println(imag(x*y)) // "10"
如果一个浮点数面值或一个十进制整数面值后面跟着一个i,例如3.141592i或2i,它将构成一个复数的虚部,复数的实部是0:
fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1
在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像1+2i或与之等价的写法2i+1。上面x和y的声明语句还可以简化:
x := 1 + 2i y := 3 + 4i
复数也可以用==和!=进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的
 

布尔型

一个布尔类型的值只有两种:true和false。
if和for语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。一元操作符!对应逻辑非操作,因此!true的值为false &&的优先级比||高(助记:&&对应逻辑乘法,||对应逻辑加法,乘法比加法优先级要高)
布尔值并不会隐式转换为数字值0或1,反之亦然。必须使用一个显式的if语句辅助转换
i := 0 if b { i = 1 }
布尔到数字型(0/1)的转换
// btoi returns 1 if b is true and 0 if false. func btoi(b bool) int { if b { return 1 } return 0 }
0/1到布尔的转换
// itob reports whether i is non-zero. func itob(i int) bool { return i != 0 }
 

字符串

一个字符串是一个不可改变的字节序列。
内置的len函数可以返回一个字符串中的字节数目
s := "hello, world" fmt.Println(len(s)) // "12" fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')
第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。
子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。
fmt.Println(s[0:5]) // "hello"
不管i还是j都可能被忽略,当它们被忽略时将采用0作为开始位置,采用len(s)作为结束的位置。
fmt.Println(s[:5]) // "hello" fmt.Println(s[7:]) // "world" fmt.Println(s[:]) // "hello, world"
因为Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理,因此我们可以将Unicode码点也写到字符串面值中。
在一个双引号包含的字符串面值中,可以用以反斜杠\ 开头的转义序列插入任意的数据。下面的换行、回车和制表符等是常见的ASCII控制代码的转义方式:
\a 响铃 \b 退格 \f 换页 \n 换行 \r 回车 \t 制表符 \v 垂直制表符 \' 单引号(只用在 '\'' 形式的rune符号面值中) \" 双引号(只用在 "..." 形式的字符串面值中) \\ 反斜杠
一个原生的字符串面值形式是 `...`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;
const GoUsage = `Go is a tool for managing Go source code. Usage: go command [arguments] ...`
 

unicode

Unicode标准里收集了超过120,000个字符,涵盖超过100多种语言
Unicode码点对应Go语言中的rune整数类型
 

utf-8

UTF8是一个将Unicode码点编码为字节序列的变长编码。
如果字符串中包含了unicode的编码数据, len所返回的长度数据可能大于预期值
import "unicode/utf8" s := "Hello, 世界" fmt.Println(len(s)) // "13" fmt.Println(utf8.RuneCountInString(s)) // "9"
因此, 我们可以使用range循环处理字符串的时候会自动隐式解码utf8字符串的特性(utf8.RuneCountInString(s)), 可以以此来获取字符串的真实长度
n := 0 for range s { // 忽略了不需要的变量 n++ }
UTF8字符解码错误的时候, 将生成一个特别的Unicode字符\uFFFD ,在印刷中这个符号通常是一个黑色六角或钻石形状,里面包含一个白色的问号"?"。
 

字符串和Byte切片

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。
strings包中的六个函数:
func Contains(s, substr string) bool func Count(s, sep string) int func Fields(s string) []string func HasPrefix(s, prefix string) bool func Index(s, sep string) int func Join(a []string, sep string) string
bytes包中也对应的六个函数:
func Contains(b, subslice []byte) bool func Count(s, sep []byte) int func Fields(s []byte) [][]byte func HasPrefix(s, prefix []byte) bool func Index(s, sep []byte) int func Join(s [][]byte, sep []byte) []byte
 

常量

常量表达式的值在编译期计算,而不是在运行期。
因为它们的值是在编译期就确定的,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度
const IPv4Len = 4 // parseIPv4 parses an IPv4 address (d.d.d.d). func parseIPv4(s string) IP { var p [IPv4Len]byte // ... }
 

iota常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。
在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。
如下: 周日将会对应0 周一对应1 等等
type Weekday int const ( Sunday Weekday = iota Monday Tuesday Wednesday Thursday Friday Saturday )
 

无类型常量

 

复合数据类型

以不同的方式组合基本类型而构造出来的复合数据类型: 数组、slice、map和结构体
 

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。
因为数组的长度是固定的,因此在Go语言中很少直接使用数组。
和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列,slice功能也更灵活
var a [3]int // array of 3 integers fmt.Println(a[0]) // print the first element fmt.Println(a[len(a)-1]) // print the last element, a[2] // Print the indices and elements. for i, v := range a { fmt.Printf("%d %d\n", i, v) } // Print the elements only. for _, v := range a { fmt.Printf("%d\n", v) }
默认情况下,数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。
var q [3]int = [3]int{1, 2, 3} var r [3]int = [3]int{1, 2} fmt.Println(r[2]) // "0"
在数组字面值中,如果在数组的长度位置出现的是“...”省略号,则表示数组的长度是根据初始化值的个数来计算。
q := [...]int{1, 2, 3} fmt.Printf("%T\n", q) // "[3]int"
数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。
可以指定一个索引和对应值列表的方式初始化
type Currency int const ( USD Currency = iota // 美元 EUR // 欧元 GBP // 英镑 RMB // 人民币 ) symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"} fmt.Println(RMB, symbol[RMB]) // "3 ¥"
初始化索引的顺序是无关紧要的,而且没用到的索引可以省略
下面定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。
r := [...]int{99: -1}
如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的。不相等比较运算符!=遵循同样的规则。
a := [2]int{1, 2} b := [...]int{1, 2} c := [2]int{1, 3} fmt.Println(a == b, a == c, b == c) // "true false false" d := [3]int{1, 2} fmt.Println(a == d) // compile error: cannot compare [2]int == [3]int
 

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。
数组和slice之间有着紧密的联系。一个slice是一个轻量级的数据结构,提供了访问数组子序列(或者全部)元素的功能,而且slice的底层确实引用一个数组对象。
一个slice由三个部分构成:指针、长度和容量。
如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了slice,因为新slice的长度会变大:
fmt.Println(summer[:20]) // panic: out of range endlessSummer := summer[:5] // extend a slice (within capacity) fmt.Println(endlessSummer) // "[June July August September October]"
因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。
a := [...]int{0, 1, 2, 3, 4, 5} reverse(a[:]) fmt.Println(a) // "[5 4 3 2 1 0]"
和数组不同的是,slice之间不能比较,因此我们不能使用==操作符来判断两个slice是否含有全部相等元素。
不过标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较
func equal(x, y []string) bool { if len(x) != len(y) { return false } for i := range x { if x[i] != y[i] { return false } } return true }
如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。
内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。
make([]T, len) make([]T, len, cap) // same as make([]T, cap)[:len]
 

append函数

内置的append函数用于向slice追加元素
var runes []rune for _, r := range "Hello, 世界" { runes = append(runes, r) } fmt.Printf("%q\n", runes) // "['H' 'e' 'l' 'l' 'o' ',' ' ' '世' '界']"
 

Map

一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。
内置的make函数可以创建一个map:
ages := make(map[string]int) // mapping from strings to ints
也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
ages := map[string]int{ "alice": 31, "charlie": 34, }
相当于
ages := make(map[string]int) ages["alice"] = 31 ages["charlie"] = 34
使用内置的delete函数可以删除元素:
delete(ages, "alice") // remove element ages["alice"]
map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
_ = &ages["bob"] // compile error: cannot take address of map element
要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。
for name, age := range ages { fmt.Printf("%s\t%d\n", name, age) }
Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。
如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。
import "sort" var names []string for name := range ages { names = append(names, name) } sort.Strings(names) for _, name := range names { fmt.Printf("%s\t%d\n", name, ages[name]) }
我们使用key作为索引来获取map的value的时候, 有时候可能因为没有赋值而返回零值, 可以通过返回的第二个参数来进行确定
if age, ok := ages["bob"]; !ok { /* ... */ }
和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较。要判断两个map是否包含相同的key和value,我们必须通过一个循环实现:
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true }
Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。
 

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。每个值称为结构体的成员。
通常一行对应一个结构体成员,成员的名字在前类型在后,不过如果相邻的成员类型如果相同的话可以被合并到一行,就像下面的Name和Address成员那样:
type Employee struct { ID int Name, Address string DoB time.Time Position string Salary int ManagerID int }
一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组。)但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。
结构体可以作为函数的参数和返回值。
func Scale(p Point, factor int) Point { return Point{p.X * factor, p.Y * factor} } fmt.Println(Scale(Point{1, 2}, 5)) // "{5 10}"
如果考虑效率的话,较大的结构体通常会用指针的方式传入和返回,
func Bonus(e *Employee, percent int) int { return e.Salary * percent / 100 }
如果要在函数内部修改结构体成员的话,用指针传入是必须的;因为在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。
因为结构体通常通过指针处理,可以用下面的写法来创建并初始化一个结构体变量,并返回结构体的地址:
pp := &Point{1, 2}
等价于
pp := new(Point) *pp = Point{1, 2}
不过&Point{1, 2}写法可以直接在表达式中使用,比如一个函数调用
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用==或!=运算符进行比较。
type Point struct{ X, Y int } p := Point{1, 2} q := Point{1, 2} fmt.Println(p.X == q.X && p.Y == q.Y) // "true" fmt.Println(p == q) // "true"
Go语言有一个特性让我们只声明一个成员对应的数据类型而不指名成员的名字;这类成员就叫匿名成员。匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。
得益于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:
type Point struct{ X, Y int } type Circle struct { Point Radius int } type Wheel struct { Circle Spokes int } var w Wheel w.X = 8 // equivalent to w.Circle.Point.X = 8 w.Y = 8 // equivalent to w.Circle.Point.Y = 8 w.Radius = 5 // equivalent to w.Circle.Radius = 5 w.Spokes = 20
但是,结构体字面值并没有简短表示匿名成员的语法, 因此下面的语句都不能编译通过:
w = Wheel{8, 8, 5, 20} // compile error: unknown fields w = Wheel{X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields
因为匿名成员也有一个隐式的名字,因此不能同时包含两个类型相同的匿名成员,这会导致名字冲突。
即使不导出,我们依然可以用简短形式访问匿名成员嵌套的成员
但是在包外部,因为circle和point没有导出,不能访问它们的成员,因此简短的匿名成员访问语法也是禁止的。
 

JSON

将一个Go语言中类似movies的结构体slice转为JSON的过程叫编组(marshaling)。编组通过调用json.Marshal函数完成:
Marshal函数返回一个编码后的字节slice,包含很长的字符串,并且没有空白缩进
data, err := json.Marshal(movies) if err != nil { log.Fatalf("JSON marshaling failed: %s", err) } fmt.Printf("%s\n", data)
为了生成便于阅读的格式,另一个json.MarshalIndent函数将产生整齐缩进的输出。该函数有两个额外的字符串参数用于表示每一行输出的前缀和每一个层级的缩进:
data, err := json.MarshalIndent(movies, "", " ") if err != nil { log.Fatalf("JSON marshaling failed: %s", err) } fmt.Printf("%s\n", data)
编码的逆操作是解码,对应将JSON数据解码为Go语言的数据结构,Go语言中一般叫unmarshaling,通过json.Unmarshal函数完成。
var titles []struct{ Title string } if err := json.Unmarshal(data, &titles); err != nil { log.Fatalf("JSON unmarshaling failed: %s", err) } fmt.Println(titles) // "[{Casablanca} {Cool Hand Luke} {Bullitt}]"
 

文本和HTML模板

一个模板是一个字符串或一个文件,里面包含了一个或多个由双花括号包含的{{action}}对象。
每个actions都包含了一个用模板语言书写的表达式,一个action虽然简短但是可以输出复杂的打印值,模板语言包含通过选择结构体的成员、调用函数或方法、表达式控制流if-else语句和range循环语句,还有其它实例化模板等诸多特性。
template.New先创建并返回一个模板;Funcs方法将daysAgo等自定义函数注册到模板中,并返回模板;最后调用Parse函数分析模板。
report, err := template.New("report"). Funcs(template.FuncMap{"daysAgo": daysAgo}). Parse(templ) if err != nil { log.Fatal(err) }
 

函数

函数可以让我们将一个语句序列打包为一个单元,然后可以从程序中其它地方多次调用。
 
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func name(parameter-list) (result-list) { body }
形参: 定义的函数入参
实参: 传入的函数实际入参
每一次函数调用都必须按照声明顺序为所有参数提供实参(参数值)。在函数调用时,Go语言没有默认参数值,也没有任何方法可以通过参数名指定形参,因此形参和返回值的变量名对于函数调用者而言没有意义。
在函数体中,函数的形参作为局部变量,被初始化为调用者提供的值。函数的形参和有名返回值作为函数最外层的局部变量,被存储在相同的词法块中。
实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。(引用类型例外)
你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。这样的声明定义了函数签名。
package math func Sin(x float64) float //implemented in assembly language
 

递归

函数可以是递归的,这意味着函数可以直接或间接的调用自身。
 

多返回值

在Go中,一个函数可以返回多个值。
准确的变量名可以传达函数返回值的含义。尤其在返回值的类型都相同时,就像下面这样:
func Size(rect image.Rectangle) (width, height int) func Split(path string) (dir, file string) func HourMinSec(t time.Time) (hour, minute, second int)
虽然良好的命名很重要,但你也不必为每一个返回值都取一个适当的名字。(bare return)
// CountWordsAndImages does an HTTP GET request for the HTML // document url and returns the number of words and images in it. func CountWordsAndImages(url string) (words, images int, err error) { resp, err := http.Get(url) if err != nil { return } doc, err := html.Parse(resp.Body) resp.Body.Close() if err != nil { err = fmt.Errorf("parsing HTML: %s", err) return } words, images = countWordsAndImages(doc) return // equal to return words, images, err } func countWordsAndImages(n *html.Node) (words, images int) { /* ... */ }
 

错误

panic是来自被调用函数的信号,表示发生了某个已知的bug。一个良好的程序永远不应该发生panic异常。
在Go的错误处理中,错误是软件包API和应用程序用户界面的一个重要组成部分,程序运行失败仅被认为是几个预期的结果之一。
对于那些将运行失败看作是预期结果的函数,它们会返回一个额外的返回值,通常是最后一个,来传递错误信息。
通常,导致失败的原因不止一种, 因此,额外的返回值不再是简单的布尔类型,而是error类型。
error类型可能是nil或者non-nil。nil意味着函数运行成功,non-nil表示失败。
 

函数值

在Go中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。
func square(n int) int { return n * n } func negative(n int) int { return -n } func product(m, n int) int { return m * n } f := square fmt.Println(f(3)) // "9" f = negative fmt.Println(f(3)) // "-3" fmt.Printf("%T\n", f) // "func(int) int" f = product // compile error: can't assign func(int, int) int to func(int) int
函数类型的零值是nil。调用值为nil的函数值会引起panic错误
函数值可以与nil比较
但是函数值之间是不可比较的,也不能用函数值作为map的key
函数值使得我们不仅仅可以通过数据来参数化函数,亦可通过行为。
 

匿名函数

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。
函数字面量的语法和函数声明相似,区别在于func关键字后没有函数名。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function)。
函数字面量允许我们在使用函数时,再定义它。
strings.Map(func(r rune) rune { return r + 1 }, "HAL-9000")
通过这种方式定义的函数可以访问完整的词法环境(lexical environment),这意味着在函数中定义的内部函数可以引用该函数的变量
 

可变参数

参数数量可变的函数称为可变参数函数。
在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数。
func sum(vals ...int) int { total := 0 for _, val := range vals { total += val } return total }
 

Deferred函数

你只需要在调用普通函数或方法前加上关键字defer,就完成了defer所需要的语法
当执行到该条语句时,函数和参数表达式得到计算,但直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。
package ioutil func ReadFile(filename string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() return ReadAll(f) }
调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。
 

panic异常

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine(可以先理解成线程,在第8章会详细介绍)中被延迟的函数(defer 机制)。
随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。
 

Recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复
如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。
func Parse(input string) (s *Syntax, err error) { defer func() { if p := recover(); p != nil { err = fmt.Errorf("internal error: %v", p) } }() // ...parser... }
通常来讲, 我们应该只恢复应该被恢复的panic异常
有些情况下,我们无法恢复。某些致命错误会导致Go在运行时终止程序,如内存不足。

方法

OOP: 封装与组合
在函数声明时,在其名字之前放上一个变量,即是一个方法。
这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。
package geometry import "math" type Point struct{ X, Y float64 } // traditional function func Distance(p, q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) } // same thing, but as a method of the Point type func (p Point) Distance(q Point) float64 { return math.Hypot(q.X-p.X, q.Y-p.Y) }
 

基于指针对象的方法

当这个接受者变量本身比较大时,我们就可以用其指针而不是对象来声明方法,如下:
func (p *Point) ScaleBy(factor float64) { p.X *= factor p.Y *= factor }
这个方法的名字是(*Point).ScaleBy。这里的括号是必须的;没有括号的话这个表达式可能会被理解为*(Point.ScaleBy)
只有类型(Point)和指向他们的指针(*Point),才可能是出现在接收器声明里的两种接收器。
在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的,比如下面这个例子:
type P *int func (P) f() { /* ... */ } // compile error: invalid receiver type
想要调用指针类型方法(*Point).ScaleBy,只要提供一个Point类型的指针即可
r := &Point{1, 2} r.ScaleBy(2) fmt.Println(*r) // "{2, 4}" // 或者 p := Point{1, 2} pptr := &p pptr.ScaleBy(2) fmt.Println(p) // "{2, 4}" // 或者 p := Point{1, 2} (&p).ScaleBy(2) fmt.Println(p) // "{2, 4}"
如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:
编译器会隐式地帮我们用&p去调用ScaleBy这个方法。这种简写方法只适用于“变量”
p.ScaleBy(2)
  1. 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。
  1. 在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是一块内存地址,就算你对其进行了拷贝。
就像一些函数允许nil指针作为参数一样,方法理论上也可以用nil指针作为其接收器,尤其当nil对于对象来说是合法的零值时,比如map或者slice。
// An IntList is a linked list of integers. // A nil *IntList represents the empty list. type IntList struct { Value int Tail *IntList } // Sum returns the sum of the list elements. func (list *IntList) Sum() int { if list == nil { return 0 } return list.Value + list.Tail.Sum() }
换言之: 如果只定义了数据的结构, 那么依然可以将该数据结构定义为方法的接收者

通过嵌入结构体来扩展类型

在类型中内嵌的匿名字段也可能是一个命名类型的指针,这种情况下字段和方法会被间接地引入到当前的类型中(译注:访问需要通过该指针指向的对象去取)。添加这一层间接关系让我们可以共享通用的结构并动态地改变对象之间的关系。
type ColoredPoint struct { *Point Color color.RGBA } p := ColoredPoint{&Point{1, 1}, red} q := ColoredPoint{&Point{5, 4}, blue} fmt.Println(p.Distance(*q.Point)) // "5" q.Point = p.Point // p and q now share the same Point p.ScaleBy(2) fmt.Println(*p.Point, *q.Point) // "{2 2} {2 2}"
下面是一个小trick。这个例子展示了简单的cache,其使用两个包级别的变量来实现,一个mutex互斥锁(§9.2)和它所操作的cache:
var ( mu sync.Mutex // guards mapping mapping = make(map[string]string) ) func Lookup(key string) string { mu.Lock() v := mapping[key] mu.Unlock() return v }
下面这个版本在功能上是一致的,但将两个包级别的变量放在了cache这个struct一组内:
var cache = struct { sync.Mutex mapping map[string]string }{ mapping: make(map[string]string), } func Lookup(key string) string { cache.Lock() v := cache.mapping[key] cache.Unlock() return v }
我们给新的变量起了一个更具表达性的名字:cache。因为sync.Mutex字段也被嵌入到了这个struct里,其Lock和Unlock方法也就都被引入到了这个匿名结构中了,这让我们能够以一个简单明了的语法来对其进行加锁解锁操作。
 

方法值和方法表达式

函数可以不通过指定其接收器即可被调用;即调用时不需要指定接收器(译注:因为已经在前文中指定过了),只要传入函数的参数即可:
p := Point{1, 2} q := Point{4, 6} distanceFromP := p.Distance // method value fmt.Println(distanceFromP(q)) // "5" var origin Point // {0, 0} fmt.Println(distanceFromP(origin)) // "2.23606797749979", sqrt(5) scaleP := p.ScaleBy // method value scaleP(2) // p becomes (2, 4) scaleP(3) // then (6, 12) scaleP(10) // then (60, 120)
在一个包的API需要一个函数值、且调用方希望操作的是某一个绑定了对象的方法的话,方法“值”会非常实用
和方法“值”相关的还有方法表达式。当调用一个方法时,与调用一个普通的函数相比,我们必须要用选择器(p.Distance)语法来指定方法的接收器。
当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数“值”,这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
当你根据一个变量来决定调用同一个类型的哪个函数时,方法表达式就显得很有用了。你可以根据选择来调用接收器各不相同的方法。
type Point struct{ X, Y float64 } func (p Point) Add(q Point) Point { return Point{p.X + q.X, p.Y + q.Y} } func (p Point) Sub(q Point) Point { return Point{p.X - q.X, p.Y - q.Y} } type Path []Point func (path Path) TranslateBy(offset Point, add bool) { var op func(p, q Point) Point if add { op = Point.Add } else { op = Point.Sub } for i := range path { // Call either path[i].Add(offset) or path[i].Sub(offset). path[i] = op(path[i], offset) } }
 

封装

一个对象的变量或者方法如果对调用方是不可见的话,一般就被定义为“封装”。
Go语言只有一种控制可见性的手段:大写首字母的标识符会从定义它们的包中被导出,小写字母的则不会。
这种限制包内成员的方式同样适用于struct或者一个类型的方法。因而如果我们想要封装一个对象,我们必须将其定义为一个struct。
一个struct类型的字段对同一个包的所有代码都有可见性,无论你的代码是写在一个函数还是一个方法里。
但只有大写的内容, 才能够被其他包获取到

接口

接口类型是对其它类型行为的抽象和概括;
接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合;它们只会表现出它们自己的方法。
也就是说当你有看到一个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的方法来做什么。
package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error }
有些新的接口类型通过组合已有的接口来定义
type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer }
接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现这个接口。
 

flag.Value接口

为我们自己的数据类型定义新的标记符号
package flag // Value is the interface to the value stored in a flag. type Value interface { String() string Set(string) error }
String方法格式化标记的值用在命令行帮助消息中;这样每一个flag.Value也是一个fmt.Stringer。Set方法解析它的字符串参数并且更新标记变量的值。实际上,Set方法和String是两个相反的操作,所以最好的办法就是对他们使用相同的注解方式。
 

接口值

接口值,由两个部分组成,一个具体的类型和那个类型的值。
称为接口的动态类型和动态值
 

sort.Interface接口

package sort type Interface interface { Len() int Less(i, j int) bool // i, j are indices of sequence elements Swap(i, j int) }
为了对序列进行排序,我们需要定义一个实现了这三个方法的类型,然后对这个类型的一个实例应用sort.Sort函数。
type StringSlice []string func (p StringSlice) Len() int { return len(p) } func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] } func (p StringSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
 

http.Handler接口

package http type Handler interface { ServeHTTP(w ResponseWriter, r *Request) } func ListenAndServe(address string, h Handler) error
ListenAndServe函数需要一个例如“localhost:8000”的服务器地址,和一个所有请求都可以分派的Handler接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) { switch req.URL.Path { case "/list": for item, price := range db { fmt.Fprintf(w, "%s: %s\n", item, price) } case "/price": item := req.URL.Query().Get("item") price, ok := db[item] if !ok { w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w, "no such item: %q\n", item) return } fmt.Fprintf(w, "%s\n", price) default: w.WriteHeader(http.StatusNotFound) // 404 fmt.Fprintf(w, "no such page: %s\n", req.URL) } }
 

error接口

type error interface { Error() string }
创建一个error最简单的方法就是调用errors.New函数,它会根据传入的错误信息返回一个新的error。
package errors func New(text string) error { return &errorString{text} } type errorString struct { text string } func (e *errorString) Error() string { return e.text }
调用errors.New函数是非常稀少的,因为有一个方便的封装函数fmt.Errorf,它还会处理字符串格式化。
package fmt import "errors" func Errorf(format string, args ...interface{}) error { return errors.New(Sprintf(format, args...)) }
在Unix平台上,Errno的Error方法会从一个字符串表中查找错误消息
package syscall type Errno uintptr // operating system error code var errors = [...]string{ 1: "operation not permitted", // EPERM 2: "no such file or directory", // ENOENT 3: "no such process", // ESRCH // ... } func (e Errno) Error() string { if 0 <= int(e) && int(e) < len(errors) { return errors[e] } return fmt.Sprintf("errno %d", e) }
下面的语句创建了一个持有Errno值为2的接口值,表示POSIX ENOENT状况:
var err error = syscall.Errno(2) fmt.Println(err.Error()) // "no such file or directory" fmt.Println(err) // "no such file or directory"
 

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。
一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。
如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。
具体类型的类型断言从它的操作对象中获得具体的值
var w io.Writer w = os.Stdout f := w.(*os.File) // success: f == os.Stdout c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer
对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分。
var w io.Writer w = os.Stdout rw := w.(io.ReadWriter) // success: *os.File has both Read and Write w = new(ByteCounter) rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
如果断言操作的对象是一个nil接口值,那么不论被断言的类型是什么这个类型断言都会失败。
w = rw // io.ReadWriter is assignable to io.Writer w = rw.(io.Writer) // fails only if rw == nil
 

基于类型断言区别错误类型

有三种经常的错误必须进行不同的处理:文件已经存在(对于创建操作),找不到文件(对于读取操作),和权限拒绝。
package os func IsExist(err error) bool func IsNotExist(err error) bool func IsPermission(err error) bool
使用
_, err := os.Open("/no/such/file") fmt.Println(os.IsNotExist(err)) // "true"
 

通过类型断言询问行为

 

类型分支

一个类型分支基于这个接口值的动态类型使一个多路分支有效。
switch x.(type) { case nil: // ... case int, uint: // ... case bool: // ... case string: // ... default: // ... }

Goroutines和Channels

Goroutines

在Go语言中,每一个并发的执行单元叫作一个goroutine
f() // call f(); wait for it to return go f() // create a new goroutine that calls f(); don't wait
主函数返回时,所有的goroutine都会被直接打断,程序退出。
除了从主函数退出或者直接终止程序之外,没有其它的编程方法能够让一个goroutine来打断另一个的执行
在我们使用go关键词的同时,需要慎重地考虑net.Conn中的方法在并发地调用时是否安全,事实上对于大多数类型来说也确实不安全
 

Channels

如果说goroutine是Go语言程序的并发体的话,那么channels则是它们之间的通信机制。
使用内置的make函数,我们可以创建一个channel:
ch := make(chan int) // ch has type 'chan int'
两个相同类型的channel可以使用==运算符比较。如果两个channel引用的是相同的对象,那么比较的结果为真。
一个channel有发送和接受两个主要操作,都是通信行为。一个发送语句将一个值从一个goroutine通过channel发送到另一个执行接收操作的goroutine。发送和接收两个操作都使用<-运算符。
ch <- x // a send statement x = <-ch // a receive expression in an assignment statement <-ch // a receive statement; result is discarded
Channel还支持close操作,用于关闭channel,随后对基于该channel的任何发送操作都将导致panic异常。
使用内置的close函数就可以关闭一个channel:
close(ch)
以最简单方式调用make函数创建的是一个无缓存的channel,但是我们也可以指定第二个整型参数,对应channel的容量。如果channel的容量大于零,那么该channel就是带缓存的channel。
ch = make(chan int) // unbuffered channel ch = make(chan int, 0) // unbuffered channel ch = make(chan int, 3) // buffered channel with capacity 3
 

无缓存Channel

一个基于无缓存Channels的发送操作将导致发送者goroutine阻塞,直到另一个goroutine在相同的Channels上执行接收操作
基于无缓存Channels的发送和接收操作将导致两个goroutine做一次同步操作。
 

串联的Channels

Channels也可以用于将多个goroutine连接在一起,一个Channel的输出作为下一个Channel的输入。这种串联的Channels就是所谓的管道(pipeline)。
func main() { naturals := make(chan int) squares := make(chan int) // Counter go func() { for x := 0; ; x++ { naturals <- x } }() // Squarer go func() { for { x := <-naturals squares <- x * x } }() // Printer (in main goroutine) for { fmt.Println(<-squares) } }
其实你并不需要关闭每一个channel。只有当需要告诉接收者goroutine,所有的数据已经全部发送时才需要关闭channel。

单方向的Channel

当一个channel作为一个函数参数时,它一般总是被专门用于只发送或者只接收。
类型chan<- int表示一个只发送int的channel,只能发送不能接收。相反,类型<-chan int表示一个只接收int的channel,只能接收不能发送。
func counter(out chan<- int) { for x := 0; x < 100; x++ { out <- x } close(out) } func squarer(out chan<- int, in <-chan int) { for v := range in { out <- v * v } close(out) } func printer(in <-chan int) { for v := range in { fmt.Println(v) } } func main() { naturals := make(chan int) squares := make(chan int) go counter(naturals) go squarer(squares, naturals) printer(squares) }
 

带缓存的Channels

向缓存Channel的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。
我们可以在无阻塞的情况下连续向新创建的channel发送三个值:
ch <- "A" ch <- "B" ch <- "C"
我们接收一个值,
fmt.Println(<-ch) // "A"
某些特殊情况下,程序可能需要知道channel内部缓存的容量,可以用内置的cap函数获取:
fmt.Println(cap(ch)) // "3" // 同样的, 可以使用len来返回有效元素的个数 fmt.Println(len(ch)) // "2"

并发的循环

并发循环时, 需要注意返回的error判断逻辑, 当我们定义了return err的时候, 会导致filenames终止循环, 而导致goroutine在向这个channel发送值时一直得不到消费. 从而导致channel的阻塞, 且永远不会退出. 这种情况叫做goroutine泄露
// makeThumbnails4 makes thumbnails for the specified files in parallel. // It returns an error if any step failed. func makeThumbnails4(filenames []string) error { errors := make(chan error) for _, f := range filenames { go func(f string) { _, err := thumbnail.ImageFile(f) errors <- err }(f) } for range filenames { if err := <-errors; err != nil { return err // NOTE: incorrect: goroutine leak! } } return nil }
最简单的解决办法就是用一个具有合适大小的buffered channel,这样这些worker goroutine向channel中发送错误时就不会被阻塞。
 

基于select的多路复用

select { case <-ch1: // ... case x := <-ch2: // ...use x... case ch3 <- y: // ... default: // ... }
和switch语句稍微有点相似,也会有几个case和最后的default选择分支。每一个case代表一个通信操作(在某个channel上进行发送或者接收),并且会包含一些语句组成的一个语句块。
一个接收表达式可能只包含接收表达式自身
select会等待case中有能够执行的case时去执行。当条件满足时,select才会去通信并执行case之后的语句;
 

并发的退出

 

包和工具

包的声明

在每个Go语言源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。
package main import ( "fmt" "math/rand" ) func main() { fmt.Println(rand.Int()) }
通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。
关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。
  1. 包对应一个可执行程序, 也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给go build(§10.7.3)构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。
  1. 包所在的目录中可能有一些文件名是以_test.go为后缀的Go源文件. 所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。测试的外部扩展包一般用来避免测试代码中的循环导入依赖
  1. 一些依赖版本号的管理工具会在导入路径后追加版本号信息, 例如: xxx/yaml.v2 这种情况下包的名字不包含版本号后缀, 而是yaml
 

导入声明

如果我们想同时导入两个有着名字相同的包,例如math/rand包和crypto/rand包,那么导入声明必须至少为一个同名包指定一个新的包名以避免冲突。这叫做导入包的重命名。
import ( "crypto/rand" mrand "math/rand" // alternative name mrand avoids conflict )
导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。
 

包的匿名导入

如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的init初始化函数
可以用下划线_来重命名导入的包
import _ "image/png" // register PNG decoder
 

包和命名

尽可能让命名有描述性且无歧义。
包名一般采用单数的形式。
要避免包名有其它的含义。
 

工具

$ go ... build compile packages and dependencies clean remove object files doc show documentation for package or symbol env print Go environment information fmt run gofmt on package sources get download and install packages and dependencies install compile and install packages and dependencies list list packages run compile and run Go program test test packages version print Go version vet run go tool vet on packages Use "go help [command]" for more information about a command. ...
为了达到零配置的设计目标,Go语言的工具箱很多地方都依赖各种约定。

工作区结构

GOPATH: 工作区
GOROOT: 指定Go的安装目录,还有它自带的标准库包的位置
 

下载包

使用Go语言工具箱的go命令,不仅可以根据包导入路径找到本地工作区的包,甚至可以从互联网上找到和更新包。
使用命令go get可以下载一个单一的包或者用...下载整个子目录里面的每个包。
一旦go get命令下载了包,然后就是安装包或包对应的可执行的程序。
go get命令支持当前流行的托管网站GitHub、Bitbucket和Launchpad,可以直接向它们的版本控制系统请求代码。
go get命令获取的代码是真实的本地存储仓库,而不仅仅只是复制源文件,因此你依然可以使用版本管理工具比较本地代码的变更或者切换到其它的版本。
 

构建包

go build命令编译命令行参数指定的每个包。
如果包是一个库,则忽略输出结果;这可以用于检测包是可以正确编译的。如果包的名字是main,go build 将调用链接器在当前目录创建一个可执行程序;以导入路径的最后一段作为可执行程序的名字。
每个包可以由它们的导入路径指定,就像前面看到的那样,或者用一个相对目录的路径名指定,相对路径必须以...开头。如果没有指定参数,那么默认指定为当前目录对应的包。
$ cd $GOPATH/src/gopl.io/ch1/helloworld $ go build // 或者 $ cd anywhere $ go build gopl.io/ch1/helloworld // 或者 $ cd $GOPATH $ go build ./src/gopl.io/ch1/helloworld
如果是main包,将会以第一个Go源文件的基础文件名作为最终的可执行程序的名字。
go run命令实际上是结合了构建和运行的两个步骤:
默认情况下,go build命令构建指定的包和它依赖的包,然后丢弃除了最后的可执行文件之外所有的中间编译结果。
go install命令和go build命令很相似,但是它会保存每个包的编译成果,而不是将它们都丢弃。
被编译的包会被保存到$GOPATH/pkg目录下,目录路径和 src目录路径对应,可执行程序被保存到$GOPATH/bin目录。
go install命令和go build命令都不会重新编译没有发生变化的包,这可以使后续构建更快捷。
 

包文档

go doc命令打印其后所指定的实体的声明与文档注释
godoc提供可以相互交叉引用的HTML页面,但是包含和go doc 命令相同以及更多的信息。
你也可以在自己的工作区目录运行godoc服务。运行下面的命令,然后在浏览器查看 http://localhost:8000/pkg 页面:
$ godoc -http :8000
 

内部包

有时候,一个中间的状态可能也是有用的,标识符对于一小部分信任的包是可见的,但并不是对所有调用者都可见。
为了满足这些需求,Go语言的构建工具对包含internal名字的路径段的包导入路径做了特殊处理。这种包叫internal包,一个internal包只能被和internal目录有同一个父目录的包所导入。
例如,net/http/internal/chunked内部包只能被net/http/httputil或net/http包导入,但是不能被net/url包导入。不过net/url包却可以导入net/http/httputil包。
 

查询包

go list命令可以查询可用包的信息。
go list命令的参数还可以用"..."表示匹配任意的包的导入路径。我们可以用它来列出工作区中的所有包:
$ go list gopl.io/ch3/... gopl.io/ch3/basename1 gopl.io/ch3/basename2 gopl.io/ch3/comma gopl.io/ch3/mandelbrot gopl.io/ch3/netflag gopl.io/ch3/printints gopl.io/ch3/surface
或者是和某个主题相关的所有包:
$ go list ...xml... encoding/xml gopl.io/ch7/xmlselect
 

反射

Go语言提供了一种机制,能够在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。这种机制被称为反射。
 

为何需要反射

没有办法来检查未知类型的表示方式,我们被卡住了。这就是我们需要反射的原因。
func Sprint(x interface{}) string { type stringer interface { String() string } switch x := x.(type) { case stringer: return x.String() case string: return x case int: return strconv.Itoa(x) // ...similar cases for int16, uint32, and so on... case bool: if x { return "true" } return "false" default: // array, chan, func, map, pointer, slice, struct return "???" } }
 

reflect.Type和reflect.Value

反射是由 reflect 包提供的。它定义了两个重要的类型,Type 和 Value。
一个 Type 表示一个Go类型。它是一个接口, 唯一能反应reflect.Type实现的是接口的类型描述信息, 也正是这个实体标识了接口值的动态类型
函数 reflect.TypeOf 接受任意的 interface{} 类型,并以 reflect.Type 形式返回其动态类型:
t := reflect.TypeOf(3) // a reflect.Type fmt.Println(t.String()) // "int" fmt.Println(t) // "int"
fmt.Printf 提供了一个缩写 %T 参数,内部使用 reflect.TypeOf 来输出:
fmt.Printf("%T\n", 3) // "int"
一个 reflect.Value 可以装载任意类型的值。函数 reflect.ValueOf 接受任意的 interface{} 类型,并返回一个装载着其动态值的 reflect.Value。
和 reflect.TypeOf 类似,reflect.ValueOf 返回的结果也是具体的类型,但是 reflect.Value 也可以持有一个接口值。
v := reflect.ValueOf(3) // a reflect.Value fmt.Println(v) // "3" fmt.Printf("%v\n", v) // "3" fmt.Println(v.String()) // NOTE: "<int Value>"
reflect.ValueOf 的逆操作是 reflect.Value.Interface 方法。它返回一个 interface{} 类型,装载着与 reflect.Value 相同的具体值:
v := reflect.ValueOf(3) // a reflect.Value x := v.Interface() // an interface{} i := x.(int) // an int fmt.Printf("%d\n", i) // "3"
 

Display递归打印

 

通过reflect.Value修改值

一个变量就是一个可寻址的内存空间,里面存储了一个值,并且存储的值可以通过内存地址来更新。
有一些reflect.Values是可取地址的;其它一些则不可以。
x := 2 // value type variable? a := reflect.ValueOf(2) // 2 int no b := reflect.ValueOf(x) // 2 int no c := reflect.ValueOf(&x) // &x *int no d := c.Elem() // 2 int yes (x)
对于d,它是c的解引用方式生成的,指向另一个变量,因此是可取地址的。我们可以通过调用reflect.ValueOf(&x).Elem(),来获取任意变量x对应的可取地址的Value。
我们可以通过调用reflect.Value的CanAddr方法来判断其是否可以被取地址:
fmt.Println(a.CanAddr()) // "false" fmt.Println(b.CanAddr()) // "false" fmt.Println(c.CanAddr()) // "false" fmt.Println(d.CanAddr()) // "true"
通过反射修改变量值的四个步骤
  1. 调用Addr()方法, 它返回一个Value, 里面保存了指向变量的指针
  1. 在Value上调用Interface方法, 也就是返回一个interface
  1. 如果我们知道变量的类型, 可以使用类型断言来将得到的interface{}类型转为普通的类型指针
  1. 通过类型指针, 即可修改类型的值
x := 2 d := reflect.ValueOf(&x).Elem() // d refers to the variable x px := d.Addr().Interface().(*int) // px := &x *px = 3 // x = 3 fmt.Println(x) // "3"
也可以通过reflect.Value.Set方法来更新对应的值
x := 2 d := reflect.ValueOf(&x).Elem() // d refers to the variable x d.Set(reflect.ValueOf(4)) fmt.Println(x) // "4"
Set方法将在运行时执行和编译时进行类似的可赋值性约束的检查。以上代码,变量和值都是int类型,但是如果变量是int64类型,那么程序将抛出一个panic异常,所以关键问题是要确保改类型的变量可以接受对应的值:
d.Set(reflect.ValueOf(int64(5))) // panic: int64 is not assignable to int
同样,对一个不可取地址的reflect.Value调用Set方法也会导致panic异常:
x := 2 b := reflect.ValueOf(x) b.Set(reflect.ValueOf(3)) // panic: Set using unaddressable value
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Go
  • Go
  • Go语言圣经
  • Typescript中的逆变与协变Go HelloWorld
    目录