GO语言基础 一
1.可见性规则
在Go语言中,标识符必须以一个大写字母开头,这样才可以被外部包的代码所使用,这被称为导出。标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的。但是包名不管在什么情况下都必须小写。
2.命名规范以及语法惯例
当某个函数需要被外部包调用的时候需要使用大写字母开头,并遵循 Pascal 命名法(“大驼峰式命名法”);否则就遵循“小驼峰式命名法”,即第一个单词的首字母小写,其余单词的首字母大写。
单词之间不以空格断开或连接号(-)、底线(_)连结,第一个单词首字母采用大写字母;后续单词的首字母亦用大写字母,例如:FirstName、LastName。每一个单词的首字母都采用大写字母的命名格式,被称为“Pascal命名法”,源自于Pascal语言的命名惯例,也有人称之为“大驼峰式命名法”(Upper Camel Case),为驼峰式大小写的子集。
Go 语言追求简洁的代码风格,并通过 gofmt 强制实现风格统一。
Go 语言也使用分号作为语句的结束,但一般会省略分号。像在标识符后面;整数、浮点、复数、Rune或字符串等字面量后面;关键字break、continue、fallthrough、或者return后面;操作符或标点符号++、–、)、]或}之后等等都可以使用分号,但是往往会省略掉,像LiteIDE编辑器会在保存.go文件时自动过滤掉这些分号,所以在Go语言开发中一般不用过多关注分号的使用。
左大括号 { 不能单独一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示。右大括号 } 需要单独一行。
在定义接口名时也有惯例,一般单方法接口由方法名称加上-er后缀来命名。
3.注释
在Go语言中,注释有两种形式:
1.行注释:使用双斜线//开始,一般后面紧跟一个空格。行注释是Go语言中最常见的注释形式,在标准包中,一般都采用行注释,建议采用这种方式。
2.块注释:使用 /* */,块注释不能嵌套。块注释一般用于包描述或注释成块的代码片段。
一般而言,注释文字尽量每行长度接近一致,过长的行应该换行以方便在编辑器阅读。注释可以是单行,多行,甚至可以使用doc.go文件来专门保存包注释。每个包只需要在一个go文件的package关键字上面注释,两者之间没有空行。对于变量,函数,结构体,接口等的注释直接加在声明前,注释与声明之间没有空行。例如:
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run genzfunc.go
// Package sort provides primitives for sorting slices and user-defined
// collections.
package sort
// A type, typically a collection, that satisfies sort.Interface can be
// sorted by the routines in this package. The methods require that the
// elements of the collection be enumerated by an integer index.
type Interface interface {
// Len is the number of elements in the collection.
Len() int
// Less reports whether the element with
// index i should sort before the element with index j.
Less(i, j int) bool
// Swap swaps the elements with indexes i and j.
Swap(i, j int)
}
// Insertion sort
func insertionSort(data Interface, a, b int) {
for i := a + 1; i < b; i++ {
for j := i; j > a && data.Less(j, j-1); j-- {
data.Swap(j, j-1)
}
}
}
函数或方法的注释需要以函数名开始,且两者之间没有空行,示例如下:
// ContainsRune reports whether the rune is contained in the UTF-8-encoded byte slice b.
func ContainsRune(b []byte, r rune) bool {
return IndexRune(b, r) >= 0
}
需要预格式化的部分,直接加空格缩进即可,示例如下:
// For example, flags Ldate | Ltime (or LstdFlags) produce,
// 2009/01/23 01:23:23 message
// while flags Ldate | Ltime | Lmicroseconds | Llongfile produce,
// 2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
在方法,结构体或者包注释前面加上“Deprecated:”表示不建议使用,示例如下:
// Deprecated: Old 老旧方法,不建议使用
func Old(a int)(int){
return a
}
在注释中,还可以插入空行,示例如下:
// Search calls f(i) only for i in the range [0, n).
//
// A common use of Search is to find the index i for a value x in
// a sorted, indexable data structure such as an array or slice.
// In this case, the argument f, typically a closure, captures the value
// to be searched for, and how the data structure is indexed and
// ordered.
//
// For instance, given a slice data sorted in ascending order,
// the call Search(len(data), func(i int) bool { return data[i] >= 23 })
// returns the smallest index i such that data[i] >= 23. If the caller
// wants to find whether 23 is in the slice, it must test data[i] == 23
// separately.
4.包的概念
Go语言使用包(package)的概念来组织管理代码,包是结构化代码的一种方式。和其他语言如JAVA类似,Go语言中包的主要作用是把功能相似或相关的代码组织在同一个包中,以方便查找和使用。在Go语言中,每个.go文件都必须归属于某一个包,每个文件都可有init()函数。包名在源文件中第一行通过关键字package指定,包名要小写。如下所示:
package fmt
每个目录下面可以有多个.go文件,这些文件只能属于同一个包,否则编译时会报错。同一个包下的不同.go文件相互之间可以直接引用变量和函数,所以这些文件中定义的全局变量和函数不能重名。
Go语言的可执行应用程序必须有main包,而且在main包中必须且只能有一个main()函数,main()函数是应用程序运行开始入口。在main包中也可以使用init()函数。
Go语言不强制要求包的名称和文件所在目录名称相同,但是这两者最好保持相同,否则很容易引起歧义。因为导入包的时候,会使用目录名作为包的路径,而在代码中使用时,却要使用包的名称。
5.包的导入
一个Go程序通过import关键字将一组包链接在一起。import其实是导入目录,而不是定义的包名称,实际应用中我们一般都会保持一致。
例如标准包中定义的big包:package big,import “math/big” ,源代码其实是在GOROOT下src中的src/math/big目录。在代码中使用big.Int时,big指的才是.go文件中定义的包名称。
当导入多个包时,一般按照字母顺序排列包名称,像LiteIDE会在保存文件时自动完成这个动作。所谓导入包即等同于包含了这个包的所有的代码对象。
为避免名称冲突,同一包中所有对象的标识符必须要求唯一。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
import语句一般放在包名定义的下一行,导入包示例如下:
package main
import "context" //加载context包
导入多个包的常见的方式是:
import (
"fmt"
"net/http"
)
调用导入的包函数的一般方式:
fmt.Println("Hello World!")
6.标准包:
unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。
syscall-os-os/exec:
os: 提供给我们一个平台无关性的操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
os/exec: 提供我们运行外部操作系统命令和程序的方式。
syscall: 底层的外部包,提供了操作系统底层调用的基本接口。
archive/tar 和 /zip-compress:压缩(解压缩)文件功能。
fmt-io-bufio-path/filepath-flag:
fmt: 提供了格式化输入输出功能。
io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
bufio: 缓冲输入输出功能的封装。
path/filepath: 用来操作在当前系统中的目标文件名路径。
flag: 对命令行参数的操作。
strings-strconv-unicode-regexp-bytes:
strings: 提供对字符串的操作。
strconv: 提供将字符串转换为基础类型的功能。
unicode: 为 unicode 型的字符串提供特殊的功能。
regexp: 正则表达式功能。
bytes: 提供对字符型分片的操作。
math-math/cmath-math/big-math/rand-sort:
math: 基本的数学函数。
math/cmath: 对复数的操作。
math/rand: 伪随机数生成。
sort: 为数组排序和自定义集合。
math/big: 大数的实现和计算。
container-/list-ring-heap: 实现对集合的操作。
list: 双链表。
ring: 环形链表。
time-log:
time: 日期和时间的基本操作。
log: 记录程序运行时产生的日志。
encoding/Json-encoding/xml-text/template:
encoding/Json: 读取并解码和写入并编码 Json 数据。
encoding/xml:简单的 XML1.0 解析器。
text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板。
net-net/http-html:
net: 网络数据的基本操作。
http: 提供了一个可扩展的 HTTP 服务器和客户端,解析 HTTP 请求和回复。
html: HTML5 解析器。
runtime: Go 程序运行时的交互操作,例如垃圾回收和协程创建。
reflect: 实现通过程序运行时反射,让程序操作任意类型的变量。
可执行应用程序的初始化和执行都起始于main包。如果main包的源代码中没有包含main()函数,则会引发构建错误 undefined: main.main。main()函数既没有参数,也没有返回类型,init()函数和main()函数在这一点上两者一样。
7.包的初始化
如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时某个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。
当某个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init()函数(如果有的话),依次类推。
等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init()函数,最后执行main()函数。
Go语言中init()函数常用于包的初始化,该函数是Go语言的一个重要特性,有下面的特征:
- nit函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
- 每个包可以拥有多个init函数
- 包的每个源文件也可以拥有多个init函数
- 同一个包中多个init()函数的执行顺序不定
- 不同包的init()函数按照包导入的依赖关系决定该函数的执行顺序
- init()函数不能被其他函数调用,其在main函数执行之前,自动被调用
8.项目目录
Go 项目目录下一般有三个子目录:
- src存放源代码
- pkg编译后生成的文件
- bin编译后生成的可执行文件
9. Go程序的编译
在Go语言中,和编译有关的命令主要是go run、go build、go install这三个命令。
go run只能作用于main包文件,先运行compile 命令编译生成.a文件,然后 link 生成最终可执行文件并运行程序,这过程的产生的是临时文件,在go run 退出前会删除这些临时文件(含.a文件和可执行文件)。最后直接在命令行输出程序执行结果。go run 命令在第二次执行的时候,如果发现导入的代码包没有发生变化,那么 go run 不会再次编译这个导入的代码包,直接进行链接生成最终可执行文件并运行程序。
go install用于编译并安装指定的代码包及它们的依赖包,并且将编译后生成的可执行文件放到 bin 目录下($GOPATH/bin),编译后的包文件放到当前工作区的 pkg 的平台相关目录下。
go build用于编译指定的代码包以及它们的依赖包。如果用来编译非main包的源码,则只做检查性的编译,而不会输出任何结果文件。如果是一个可执行程序的源码(即是 main 包),这个过程与go run 大体相同,除了会在当前目录生成一个可执行文件外。
使用go build时有一个地方需要注意,对外发布编译文件如果不希望被人看到源代码,请使用go build -ldflags 命令,设置编译参数-ldflags “-w -s” 再编译后发布。避免使用gdb来调试而清楚看到源代码。
10.字符串介绍
Go 语言中可以使用反引号或者双引号来定义字符串。反引号表示原生的字符串,即不进行转义。
2.双引号:字符串使用双引号括起来,其中的相关的转义字符将被替换。例如:
str := "Hello World! \n Hello Gopher! \n"
输出:
Hello World!
Hello Gopher!
2.反引号:字符串使用反引号括起来,其中的相关的转义字符不会被替换。例如:
str := `Hello World! \n Hello Gopher! \n`
输出:
Hello World! \nHello Gopher! \n
双引号中的转义字符被替换,而反引号中原生字符串中的 \n 会被原样输出。
Go 语言中的string类型是一种值类型,存储的字符串是不可变的,如果要修改string内容需要将string转换为[]byte或[]rune,并且修改后的string内容是重新分配的。
需要注意的是,在Go语言代码使用 UTF-8 编码,同时标识符也支持 Unicode 字符。在标准库 unicode 包中,提供了对 Unicode 相关编码、解码的支持。而UTF8编码由Go语言之父Ken Thompson和Rob Pike共同发明的,现在已经是Unicode的标准。
Go语言默认使用UTF-8编码,对Unicode的支持非常好。但这也带来一个问题,也就是很多资料中提到的“获取字符串长度”的问题。内置的len()函数获取的是每个字符的UTF-8编码的长度和,而不是直接的字符数量。
package main
import (
"fmt"
"unicode/utf8"
)
func main() {
s := "其实就是rune"
fmt.Println(len(s)) // "16"
fmt.Println(utf8.RuneCountInString(s)) // "8"
}
如字符串含有中文等字符,我们可以看到每个中文字符的索引值相差3。下面代码同时说明了在for range循环处理字符时,不是按照字节的方式来处理的。v其实际上是一个rune类型值。实际上,Go语言的range循环在处理字符串的时候,会自动隐式解码UTF8字符串。
package main
import (
"fmt"
)
func main() {
s := "Go语言四十二章经"
for k, v := range s {
fmt.Printf("k:%d,v:%c == %d\n", k, v, v)
}
}
输出:
k:0,v:G == 71
k:1,v:o == 111
k:2,v:语 == 35821
k:5,v:言 == 35328
k:8,v:四 == 22235
k:11,v:十 == 21313
k:14,v:二 == 20108
k:17,v:章 == 31456
k:20,v:经 == 32463
注意事项:获取字符串中某个字节的地址的行为是非法的,例如:&str[i]。
字符串拼接
可以通过以下方式来对代码中多行的字符串进行拼接。
直接使用运算符
str := "Beginning of the string " +
“second part of the string”
由于编译器行尾自动补全分号的缘故,加号 + 必须放在第一行。 拼接的简写形式 += 也可以用于字符串:
s := “hel” + "lo, "
s += “world!”
fmt.Println(s) // 输出 “hello, world!”
里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给 gc 带来额外的负担,所以性能比较差。
fmt.Sprintf()
内部使用 []byte 实现,不像直接运算符这种会产生很多临时的字符串,但是内部的逻辑比较复杂,有很多额外的判断,还用到了 interface,所以性能一般。
package main
import (
"fmt"
)
func main() {
sprintf := fmt.Sprintf("%d:%s", 2018, "年")
fmt.Println(sprintf)
}
strings.Join()
Join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是本来没有,去构造这个数据的代价也不小。
package main
import (
"fmt"
"strings"
)
func main() {
sprintf := strings.Join([]string{"hello", "world"}, ", ")
fmt.Println(sprintf) //hello, world
}
bytes.Buffer
strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。strings.Builder 同样也提供了 Grow() 来支持预定义容量。当我们可以预定义我们需要使用的容量时,strings.Builder 就能避免扩容而创建新的 slice 了。strings.Builder是非线程安全,性能上和 bytes.Buffer 相差无几。
package main
import (
"bytes"
"fmt"
)
func main() {
var buffer bytes.Buffer
buffer.WriteString("hello")
buffer.WriteString(", ")
buffer.WriteString("world")
fmt.Print(buffer.String())
}
有关string处理
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。
strings 包提供了很多操作字符串的简单函数,通常一般的字符串操作需求都可以在这个包中找到。下面简单举几个例子:
判断是否以某字符串打头/结尾 strings.HasPrefix(s, prefix string) bool strings.HasSuffix(s, suffix string) bool
字符串分割 strings.Split(s, sep string) []string
返回子串索引 strings.Index(s, substr string) int strings.LastIndex 最后一个匹配索引
字符串连接 strings.Join(a []string, sep string) string 另外可以直接使用“+”来连接两个字符串
字符串替换 strings.Replace(s, old, new string, n int) string
字符串转化为大小写 strings.ToUpper(s string) string strings.ToLower(s string) string
统计某个字符在字符串出现的次数 strings.Count(s, substr string) int
判断字符串的包含关系 strings.Contains(s, substr string) bool
11.数组(Array)
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。
数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的。
注意事项:如果我们想让数组元素类型为任意类型的话可以使用空接口interface{}作为类型。当使用值时我们必须先做一个类型判断。
在Go语言中,可以定义一维数组或者多维数组。
一维数组声明以及初始化常见方式如下:
var arrAge = [5]int{18, 20, 15, 22, 16}
var arrName = [5]string{3: "Chris", 4: "Ron"} //指定索引位置初始化
// {"","","","Chris","Ron"}
var arrCount = [4]int{500, 2: 100} //指定索引位置初始化 {500,0,100,0}
var arrLazy = [...]int{5, 6, 7, 8, 22} //数组长度初始化时根据元素多少确定
var arrPack = [...]int{10, 5: 100} //指定索引位置初始化,数组长度与此有关 {10,0,0,0,0,100}
var arrRoom [20]int
var arrBed = new([20]int)
输出:
[18 20 15 22 16]
[ Chris Ron]
[500 0 100 0]
[5 6 7 8 22]
[10 0 0 0 0 100]
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
&[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
数组在声明时需要确定长度,但是也可以采用上面不定长数组的方式声明,在初始化时会自动确定好数组的长度。上面 arrPack 声明中 len(arrPack) 结果为6 ,表明初始化时已经确定了数组长度。而arrRoom和arrBed这两个数组的所有元素这时都为0,这是因为每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0。
Go 语言中的数组是一种值类型(不像 C/C++ 中是指向首元素的指针),所以可以通过 new() 来创建:var arr1 = new([5]int)
那么这种方式和 var arr2 [5]int 的区别是什么呢?arr1 的类型是 *[5]int,而 arr2的类型是 [5]int。在Go语言中,数组的长度都算在类型里。
package main
import (
"fmt"
)
func main() {
var arr1 = new([5]int)
arr := arr1
arr1[2] = 100
fmt.Println(arr1[2], arr[2])
var arr2 [5]int
newarr := arr2
arr2[2] = 100
fmt.Println(arr2[2], newarr[2])
}
输出:
100 100
100 0
从上面代码结果可以看到,new([5]int)创建的是数组指针,arr其实和arr1指向同一地址,故而修改arr1时arr同样也生效。而newarr是由arr2值传递(拷贝),故而修改任何一个都不会改变另一个的值。在写函数或方法时,如果参数是数组,需要注意参数长度不能过大。
由于把一个大数组传递给函数会消耗很多内存(值传递),在实际中我们通常有两种方法可以避免这种现象:传递数组的指针、使用切片
,而通常使用切片是第一选择。
多维数组在Go语言中也是支持的,例如:
[...][5]int{ {10, 20}, {30, 40} } // len() 长度根据实际初始化时数据的长度来定,这里为2
[3][5]int // len() 长度为3
[2][2][2]float64 // 可以这样理解 [2]([2]([2]float64))
在定义多维数组时,仅第一维允许使用“…”,而内置函数len和cap也都返回第一维度长度。定义数组时使用“…”表示长度,表示初始化时的实际长度来确定数组的长度。
b := [...][5]int{ { 10, 20 }, { 30, 40, 50, 60 } }
fmt.Println(b[1][3], len(b)) //60 2
数组元素可以通过索引(下标)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。(数组以 0 开始在所有类 C 语言中是相似的)。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组大小最大为 2Gb。
遍历数组的方法既可以for 条件循环,也可以使用 for-range。这两种 for 结构对于切片(slices)来说也同样适用。
package main
import (
"fmt"
)
func main() {
var arrAge = [5]int{18, 20, 15, 22, 16}
for i, v := range arrAge {
fmt.Printf("%d 的年龄: %d\n", i, v)
}
}
输出:
0 的年龄: 18
1 的年龄: 20
2 的年龄: 15
3 的年龄: 22
4 的年龄: 16
多维数组的遍历需要使用多层的循环嵌套
另外,如数组元素类型支持”==,!=”操作符,那么数组也支持此操作,但如果数组类型不一样则不支持(需要长度和数据类型一致,否则编译不通过)。如:
var arrRoom [20]int
var arrBed [20]int
println(arrRoom == arrBed) //true
12.切片(slice)
切片(slice) 是对底层数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型(和数组不一样)。切片提供对该数组中编号的元素序列的访问。 切片类型表示其元素类型的所有数组切片的集合。未初始化切片的值为nil。
与数组一样,切片是可索引的并且具有长度。切片s的长度可以通过内置函数len() 获取;与数组不同,切片的长度可能在执行期间发生变化。我们可以把切片看成是一个长度可变的数组。
切片提供了计算容量的函数 cap() ,可以测量切片最大长度。切片的长度永远不会超过它的容量,所以对于切片 s 来说,这个不等式永远成立:0 <= len(s) <= cap(s)。
一旦初始化,切片始终与保存其元素的基础数组相关联。因此,切片会和与其拥有同一基础数组的其他切片共享存储;相比之下,不同的数组总是代表不同的存储。
切片下面的数组可以延伸超过切片的末端。容量是切片长度与切片之外的数组长度的总和。
使用内置函数make()可以给切片初始化,该函数指定切片类型和指定长度和可选容量的参数。
切片与数组相比较:
优点
因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中切片比数组更常用。
声明切片的格式是: var identifier []type(不需要说明长度)。一个切片在未初始化之前默认为 nil,长度为 0。切片的初始化格式是:var slice1 []type = arr1[start:end]
这表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。
切片也可以用类似数组的方式初始化:var x = []int{2, 3, 5, 7, 11}
,这样就创建了一个长度为 5 的数组并且创建了一个相关切片。
当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片,同时创建好相关数组:var slice1 []type = make([]type, len,cap)
也可以简写为 slice1 := make([]type, len),这里 len 是数组的长度并且也是 slice 的初始长度。cap是容量,其中 cap 是可选参数。v := make([]int, 10, 50)
这样分配一个有 50 个 int 值的数组,并且创建了一个长度为 10,容量为 50 的 切片 v,该切片指向数组的前 10 个元素。
以上我们列举了三种切片初始化方式,这三种方式都比较常用。
如果从数组或者切片中生成一个新的切片,我们可以使用下面的表达式:
a[low : high : max] max-low的结果表示容量,high-low的结果表示长度。
package main
import "fmt"
func main() {
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Println(t)//[2 3]
}
这里t的容量(capacity)是5-1=4 ,长度是2。
如果切片取值时索引值大于长度会导致panic错误发生,即使容量远远大于长度也没有用,如下面代码所示:
package main
import "fmt"
func main() {
sli := make([]int, 5, 10)
fmt.Printf("切片sli长度和容量:%d, %d\n", len(sli), cap(sli)) //切片sli长度和容量:5, 10
fmt.Println(sli) //[0 0 0 0 0]
newsli := sli[:cap(sli)]
fmt.Println(newsli) //[0 0 0 0 0 0 0 0 0 0]
var x = []int{2, 3, 5, 7, 11}
fmt.Printf("切片x长度和容量:%d, %d\n", len(x), cap(x)) //切片x长度和容量:5, 5
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5] // a[low : high : max] max-low的结果表示容量 high-low为长度
fmt.Printf("切片t长度和容量:%d, %d\n", len(t), cap(t)) //切片t长度和容量:2, 4
// fmt.Println(t[2]) // panic ,索引不能超过切片的长度
}
切片重组(reslice)
slice1 := make([]type, start_length, capacity)
通过改变切片长度得到新切片的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end],其中 end 是新的末尾索引(即长度)。
当我们在一个slice基础上重新划分一个slice时,新的slice会继续引用原有slice的数组。如果你忘了这个行为的话,在你的应用分配大量临时的slice用于创建新的slice来引用原有数据的一小部分时,会导致难以预期的内存使用。
package main
import "fmt"
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 10000 10000 0xc000060000
return raw[:3] // 10000个字节实际只需要引用3个,其他空间浪费
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 3 10000 0xc000060000
}
为了避免这个陷阱,我们需要从临时的slice中使用内置函数copy(),拷贝数据(而不是重新划分slice)到新切片。
package main
import "fmt"
func get() []byte {
raw := make([]byte, 10000)
fmt.Println(len(raw), cap(raw), &raw[0]) // 显示: 10000 10000 数组首字节地址
res := make([]byte, 3)
copy(res, raw[:3]) // 利用copy 函数复制,raw 可被GC释放
return res
}
func main() {
data := get()
fmt.Println(len(data), cap(data), &data[0]) // 显示: 3 3 数组首字节地址
}
Append()函数将 0 个或多个具有相同类型 S 的元素追加到切片s后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。
因此,append()函数返回的切片可能已经指向一个不同的相关数组了。append()函数总是返回成功,除非系统内存耗尽了。
s0 := []int{0, 0}
s1 := append(s0, 2) // append 单个元素 s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append 多个元素 s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append 一个切片 s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append 切片片段 s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
append()函数操作如果导致分配新的切片来保证已有切片元素和新增元素的存储,也就是返回的切片可能已经指向一个不同的相关数组了,那么新的slice已经和原来slice没有任何关系,即使修改了数据也不会同步。
append()函数操作后,有没有生成新的slice需要看原有slice的容量是否足够。
陈旧的切片(Stale Slices)
多个slice可以引用同一个底层数组。在某些情况下,在一个slice中添加新的数据,在原有数组无法保持更多新的数据时,将导致分配一个新的数组。而现在其他的slice还指向老的数组(和老的数据)。
上一节我们也说了:append()函数操作后,有没有生成新的slice需要看原有slice的容量是否足够。
下面,我们看看这个过程是怎么产生的:
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
fmt.Println(len(s1), cap(s1), s1) // 输出 3 3 [1 2 3]
s2 := s1[1:]
fmt.Println(len(s2), cap(s2), s2) // 输出 2 2 [2 3]
for i := range s2 {
s2[i] += 20
}
// s2的修改会影响到数组数据,s1输出新数据
fmt.Println(s1) // 输出 [1 22 23]
fmt.Println(s2) // 输出 [22 23]
s2 = append(s2, 4) // append s2容量为2,这个操作导致了slice s2扩容,会生成新的底层数组。
for i := range s2 {
s2[i] += 10
}
// s1 的数据现在是老数据,而s2扩容了,复制数据到了新数组,他们的底层数组已经不是同一个了。
fmt.Println(len(s1), cap(s1), s1) // 输出3 3 [1 22 23]
fmt.Println(len(s2), cap(s2), s2) // 输出3 4 [32 33 14]
}
13.字典(Map)
map是一种元素对的无序集合,一组称为元素value,另一组为唯一键索引key。 未初始化map的值为nil。map 是引用类型,可以使用如下声明:var map1 map[keytype]valuetype
注意:[keytype] 和 valuetype 之间允许有空格,但是 Gofmt 移除了空格
在声明的时候不需要知道 map 的长度,map 是可以动态增长的。
key 可以是任意可以用 == 或者 != 操作符比较的类型,比如 string、int、float。所以数组、函数、字典、切片和结构体不能作为 key (含有数组切片的结构体不能作为 key,只包含内建类型的 struct 是可以作为 key 的),但是指针和接口类型可以。
value 可以是任意类型的;通过使用空接口类型,我们可以存储任意值,但是使用这种类型作为值时需要先做一次类型断言。map 也可以用函数作为自己的值,这样就可以用来做分支结构:key 用来选择要执行的函数。
map 传递给函数的代价很小:在 32 位机器上占 4 个字节,64 位机器上占 8 个字节,无论实际上存储了多少数据。通过 key 在 map 中寻找值是很快的,比线性查找快得多,但是仍然比从数组和切片的索引中直接读取要慢 100 倍;所以如果你很在乎性能的话还是建议用切片来解决问题。
map 可以用 {key1: val1, key2: val2} 的描述方法来初始化,就像数组和结构体一样。
map 是引用类型的,内存用 make 方法来分配。map 的初始化:var map1 = make(map[keytype]valuetype)
map 容量: 和数组不同,map 可以根据新增的 key-value 对动态的伸缩,因此它不存在固定长度或者最大限制。但是你也可以选择标明 map 的初始容量 capacity,就像这样:make(map[keytype]valuetype,cap)。
例如:map2 := make(map[string]float32, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value 对,map 的大小会自动加 1。所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
在一个 nil 的slice中添加元素是没问题的,但对一个map做同样的事将会生成一个运行时的panic。
可正常运行:
package main
import "fmt"
func main() {
var s []int
s = append(s, 1)
fmt.Println(s) //[1]
var m = make(map[string]int)
m["one"] = 1
fmt.Println(m)//map[one:1]
}
会发生错误:
package main
import "fmt"
func main() {
var m map[string]int
m["one"] = 1 // 错误
fmt.Println(m)
}
14.流程控制
Switch 语句
switch var1 {
case val1:
...
case val2:
...
default:
...
}
switch {
case condition1:
...
case condition2:
...
default:
...
}
switch 语句的第二种形式是不提供任何被判断的值(实际上默认为判断是否为 true),然后在每个 case 分支中进行测试不同的条件。当任一分支的测试结果为 true 时,该分支的代码会被执行。
switch 语句的第三种形式是包含一个初始化语句:
switch initialization {
case val1:
...
case val2:
...
default:
...
}
switch result := calculate(); {
case result < 0:
...
case result > 0:
...
default:
// 0
}
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。前花括号 { 必须和 switch 关键字在同一行。
您可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1,val2,val3。 一旦成功地匹配到某个分支,在执行完相应代码后就会退出整个 switch 代码块,也就是说您不需要特别使用 break 语句来表示结束。
如果在执行完每个分支的代码后,还希望继续执行后续分支的代码,可以使用 fallthrough 关键字来达到目的。
fallthrough强制执行后面的case代码,fallthrough不会判断下一条case的expr结果是否为true。
package main
import "fmt"
func main() {
switch a := 1; {
case a == 1:
fmt.Println("The integer was == 1")
fallthrough
case a == 2:
fmt.Println("The integer was == 2")
case a == 3:
fmt.Println("The integer was == 3")
fallthrough
case a == 4:
fmt.Println("The integer was == 4")
case a == 5:
fmt.Println("The integer was == 5")
fallthrough
default:
fmt.Println("default case")
}
}
Select控制
select是Go中的一个控制结构,类似于switch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。
select中的case语句必须是一个channel操作
select中的default子句总是可运行的。
- 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
- 如果没有可运行的case语句,且有default语句,那么就会执行default的动作。
- 如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行。
package main
import (
"fmt"
"time"
)
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3):
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
case <-time.After(time.Second * 3): //超时退出
fmt.Println("request time out")
}
}
// 输出:request time out
For循环
最简单的基于计数器的迭代,基本形式为:
for 初始化语句; 条件语句; 修饰语句 {}
这三部分组成的循环的头部,它们之间使用分号 ; 相隔,但并不需要括号 () 将它们括起来。您还可以在循环中同时使用多个计数器:
for i, j := 0, N; i < j; i, j = i+1, j-1 {}
这得益于 Go 语言具有的平行赋值的特性,for 结构的第二种形式是没有头部的条件判断迭代(类似其它语言中的 while 循环),基本形式为:for 条件语句 {}。
您也可以认为这是没有初始化语句和修饰语句的 for 结构,因此 ;; 便是多余的了
条件语句是可以被省略的,如 i:=0; ; i++ 或 for { } 或 for ;; { }(;; 会在使用 Gofmt 时被移除):这些循环的本质就是无限循环。 最后一个形式也可以被改写为 for true { },但一般情况下都会直接写 for { }。
如果 for 循环的头部没有条件语句,那么就会认为条件永远为 true,因此循环体内必须有相关的条件判断以确保会在某个时刻退出循环。
package main
import (
"fmt"
)
func main() {
a := []int{1, 2, 3, 4, 5, 6}
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
for j := 0; j < 5; j++ {
for i := 0; i < 10; i++ {
if i > 5 {
break
}
fmt.Println(i)
}
}
}
for-range 结构
这是 Go 特有的一种的迭代结构,您会发现它在许多情况下都非常有用。它可以迭代任何一个集合(包括数组和 map),同时可以获得每次迭代所对应的索引。一般形式为:for ix, val := range coll { }
要注意的是,val 始终为集合中对应索引的值拷贝,因此它一般只具有只读性质,对它所做的任何修改都不会影响到集合中原有的值(注:如果 val 为指针,则会产生指针的拷贝,依旧可以修改集合中的原值)。
package main
import (
"fmt"
"time"
)
type field struct {
name string
}
func (p *field) print() {
fmt.Println(p.name)
}
func main() {
data := []field{ {"one"}, {"two"}, {"three"} }
for _, v := range data {
go v.print()
}
time.Sleep(3 * time.Second)
// goroutines (可能)显示: three, three, three
}
当前的迭代变量作为匿名goroutine的参数。
package main
import (
"fmt"
"time"
)
func main() {
data := []string{"one", "two", "three"}
for _, v := range data {
go func(in string) {
fmt.Println(in)
}(v)
}
time.Sleep(3 * time.Second)
// goroutines输出: one, two, three
}
一个字符串是 Unicode 编码的字符(或称之为 rune)集合,因此您也可以用它迭代字符串:
package main
import (
"fmt"
)
func main() {
str := "abcdefghijk"
for pos, char := range str {
fmt.Println(pos, string(char))
}
}
if
If语句由布尔表达式后紧跟一个或多个语句组成,注意布尔表达式不用()
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
break
一个 break 的作用范围为该语句出现后的最内部的结构,它可以被用于任何形式的 for 循环(计数器、条件判断等)。 但在 switch 或 select 语句中,break 语句的作用结果是跳过整个代码块,执行后续的代码。
continue
关键字 continue 忽略剩余的循环体而直接进入下一次循环的过程,但不是无条件执行下一次循环,执行之前依旧需要满足循环的判断条件。 关键字 continue 只能被用于 for 循环中。
label
or、switch 或 select 语句都可以配合标签(label)形式的标识符使用,即某一行第一个以冒号(:)结尾的单词(Gofmt 会将后续代码自动移至下一行) (标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母) continue 语句指向 LABEL1,当执行到该语句的时候,就会跳转到 LABEL1 标签的位置。
使用标签和 Goto 语句是不被鼓励的:它们会很快导致非常糟糕的程序设计,而且总有更加可读的替代方案来实现相同的需求。
15.错误类型
任何时候当你需要一个新的错误类型,都可以用 errors(必须先 import)包的 errors.New 函数接收合适的错误信息来创建,像下面这样:
err := errors.New("math - square root of negative number")
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New ("math - square root of negative number")
}
}
用 fmt 创建错误对象:
通常你想要返回包含错误参数的更有信息量的字符串,例如:可以用 fmt.Errorf() 来实现:它和 fmt.Printf() 完全一样,接收有一个或多个格式占位符的格式化字符串和相应数量的占位变量。和打印信息不同的是它用信息生成错误对象。 比如在前面的平方根例子中使用:
if f < 0 {
return 0, fmt.Errorf("square root of negative number %g", f)
}
Panic
在Go语言中 panic 是一个内置函数,用来表示非常严重的不可恢复的错误。必须要先声明defer,否则不能捕获到panic异常。普通函数在执行的时候发生panic了,则开始运行defer(如有),defer处理完再返回。
在多层嵌套的函数调用中调用 panic,可以马上中止当前函数的执行,所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行(每层的) defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况:这个终止过程就是 panicking。
标准库中有许多包含 Must 前缀的函数,像 regexp.MustComplie 和 template.Must;当正则表达式或模板中转入的转换字符串导致错误时,这些函数会 panic。
不能随意地用 panic 中止程序,必须尽力补救错误让程序能继续执行。
自定义包中的错误处理和 panicking,这是所有自定义包实现者应该遵守的最佳实践:
1)在包内部,总是应该从 panic 中 recover:不允许显式的超出包范围的 panic()
2)向包的调用者返回错误值(而不是 panic)。
recover() 的调用仅当它在 defer 函数中被直接调用时才有效。
下面主函数recover 了panic:
package main
import (
"fmt"
)
func div(a, b int) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("捕获到错误:%s\n", r)
}
}()
if b < 0 {
panic("除数需要大于0")
}
fmt.Println("余数为:", a/b)
}
func main() {
// 捕捉内部的Panic错误
div(10, 0)
// 捕捉主动Panic的错误
div(10, -1)
}
程序输出:
捕获到错误:runtime error: integer divide by zero
捕获到错误:除数需要大于0
Recover:从 panic 中恢复
正如名字一样,这个(recover)内建函数被用于从 panic 或 错误场景中恢复:让程序可以从 panicking 重新获得控制权,停止终止过程进而恢复正常执行。 recover 只能在 defer 修饰的函数中使用:用于取得 panic 调用中传递过来的错误值,如果是正常执行,调用 recover 会返回 nil,且没有其它效果。 总结:panic 会导致栈被展开直到 defer 修饰的 recover() 被调用或者程序中止。
package main
import (
"fmt"
"log"
)
func protect(g func()) {
defer func() {
log.Println("done")
// 即使有panic,Println也正常执行。
if err := recover(); err != nil {
log.Printf("run time panic: %v", err)
}
}()
log.Println("start")
g() // 可能发生运行时错误的地方
panic("测试")
}
func main() {
protect(func() {
fmt.Println(1)
})
}
有关于defer
说到错误处理,就不得不提defer。先说说它的规则:
规则一 当defer被声明时,其参数就会被实时解析
规则二 defer执行顺序为先进后出
规则三 defer可以读取有名返回值,也就是可以改变有名返回参数的值。
这三个规则用起来需要注意下,避免出现代码陷阱,下面是具体代码:
// 规则一,当defer被声明时,其参数就会被实时解析
package main
import "fmt"
func main() {
var i = 1
defer fmt.Println("result =>", func() int {
return i * 2
}()) // 输出: result => 2 (而不是 4)
i++
fmt.Println(i) //2
}
// 规则二 defer执行顺序为先进后出
package main
import "fmt"
func main() {
defer fmt.Print(" !!! ")
defer fmt.Print(" world ")
fmt.Print(" hello ")
//输出: hello world !!!++++
}
上面讲了两条规则,第三条规则其实也不难理解,只要记住是可以改变有名返回值:
这是由于在Go语言中,return 语句不是原子操作,最先是所有结果值在进入函数时都会初始化为其类型的零值(姑且称为ret赋值),然后执行defer命令,最后才是return操作。如果是有名返回值,返回值变量其实可视为是引用赋值,可以能被defer修改。而在匿名返回值时,给ret的值相当于拷贝赋值,defer命令时不能直接修改。
func func1 (i int) {
.....
return
}
上面函数签名中的 i 就是有名返回值,如果fun1()中定义了 defer 代码块,是可以改变返回值 i 的,函数返回语句return i 可以简写为 return 。
这里综合了一下,在下面这个例子里列举了几种情况,可以好好琢磨下:
package main
import "fmt"
func main() {
fmt.Println("=========================")
fmt.Println("return:", fun1())
fmt.Println("=========================")
fmt.Println("return:", fun2())
fmt.Println("=========================")
fmt.Println("return:", fun3())
fmt.Println("=========================")
fmt.Println("return:", fun4())
}
func fun1() (i int) {
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer2: 2
}()
// 规则二 defer执行顺序为先进后出
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer1: 1
}()
// 规则三 defer可以读取有名返回值(函数指定了返回参数名)
return //这里实际结果为2。
}
func fun2() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer2: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer1: 1
}()
return i
}
func fun3() (r int) {
t := 5
defer func() {
t = t + 5
fmt.Println(t)
}()
return t
}
func fun4() int {
i := 8
// 规则一 当defer被声明时,其参数就会被实时解析
defer func(i int) {
i = 99
fmt.Println(i)
}(i)
i = 19
return i
}
在上面fun1() (i int)有名返回值情况下,return最终返回的实际值和期望的return 0有较大出入。因为在上面fun1() (i int) 中,如果return 100或return 0 ,这样的区别在于i的值实际上分别是100或0。而在上面中,如果return 100,则因为改变了有名返回值i,而defer可以读取有名返回值,所以返回值最终为102,而defer1打印101,defer2打印102。因此我们一般直接写为return。
这点要注意,有时函数可能返回非我们希望的值,所以改为匿名返回也是一种办法。具体请看下面输出。
=========================
defer1: 1
defer2: 2
return: 2
=========================
defer1: 1
defer2: 2
return: 0
=========================
10
return: 5
=========================
99
return: 19
使用defer计算函数执行时间
package main
import (
"fmt"
"time"
)
func main() {
defer func(start time.Time) {
terminal := time.Since(start)
fmt.Println(terminal)
}(time.Now())
fmt.Println("start program")
time.Sleep(5 * time.Second)
fmt.Println("finish program")
}
在对比和基准测试中,我们需要知道一个计算执行消耗的时间。最简单的一个办法就是在计算开始之前设置一个起始时候,再由计算结束时的结束时间,最后取出它们的差值,就是这个计算所消耗的时间。想要实现这样的做法,可以使用 time 包中的 Now() 和 Sub 函数:
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
time.Sleep(5 * time.Second)
end := time.Now()
delta := end.Sub(start)
fmt.Printf("longCalculation took this amount of time: %s\n", delta)
}
16.函数介绍
Go语言函数基本组成:关键字func、函数名、参数列表、返回值、函数体和返回语句。语法如下:
func 函数名(参数列表) (返回值列表) {
// 函数体
return
}
除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。
对于函数,一般也可以这么认为:“func” FunctionName Signature [ FunctionBody ] .
“func” 为定义函数的关键字,FunctionName 为函数名,Signature 为函数签名,FunctionBody 为函数体。以下面定义的函数为例:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
函数签名由函数参数、返回值以及它们的类型组成,被统称为函数签名。如:
(a typea, b typeb) (t1 type1, t2 type2)
如果两个函数的参数列表和返回值列表的变量类型能一一对应,那么这两个函数就有相同的签名,下面testa与testb具有相同的函数签名。
func testa (a, b int, z float32) bool
func testb (a, b int, z float32) (bool)
函数调用传入的参数必须按照参数声明的顺序。而且Go语言没有默认参数值的说法。函数签名中的最后传入参数可以具有前缀为…的类型(…int),这样的参数称为可变参数,并且可以使用零个或多个参数来调用该函数,这样的函数称为变参函数。
func doFix (prefix string, values ...int)
函数的参数和返回值列表始终带括号,但如果只有一个未命名的返回值(且只有此种情况),则可以将其写为未加括号的类型;一个函数也可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来。
func testa (a, b int, z float32) bool
func swap (a int, b int) (t1 int, t2 int)
在函数体中,参数是局部变量,被初始化为调用者传入的值。函数的参数和具名返回值是函数最外层的局部变量,它们的作用域就是整个函数。如果函数的签名声明了返回值,则函数体的语句列表必须以终止语句结束。
func IndexRune(s string, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
return // 必须要有终止语句,如果这里没有return,则会编译错误:missing return at end of function
}
函数重载(function overloading)指的是可以编写多个同名函数,只要它们拥有不同的形参或者不同的返回值,在 Go 语言里面函数重载是不被允许的。
函数也可以作为函数类型被使用。函数类型也就是函数签名,函数类型表示具有相同参数和结果类型的所有函数的集合。函数类型的未初始化变量的值为nil。就像下面:
type funcType func (int, int) int
上面通过type关键字,定义了一个新类型,函数类型 funcType 。
函数也可以在表达式中赋值给变量,这样作为表达式中右值出现,我们称之为函数值字面量(function literal),函数值字面量是一种表达式,它的值被称为匿名函数,就像下面一样:
f := func() int { return 7 }
下面代码对以上2种情况都做了定义和调用:
package main
import (
"fmt"
"time"
)
type funcType func(time.Time) // 定义函数类型funcType
func main() {
f := func(t time.Time) time.Time {
return t
} // 方式一:直接赋值给变量
fmt.Println(f(time.Now()))
var timer funcType = CurrentTime // 方式二:定义函数类型funcType变量timer
timer(time.Now())
funcType(CurrentTime)(time.Now()) // 先把CurrentTime函数转为funcType类型,然后传入参数调用
// 这种处理方式在Go 中比较常见
}
func CurrentTime(start time.Time) {
fmt.Println(start)
}
函数调用
Go 语言中函数默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能对副本的值进行更改,但不会影响到原来的变量。
如果我们希望函数可以直接修改参数的值,而不是对参数的副本进行操作,则需要将参数的地址传递给函数,这就是按引用传递,比如 Function(&arg1),此时传递给函数的是一个指针。如果传递给函数的是一个指针,我们可以通过这个指针来修改对应地址上的变量值。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、通道(channel)等这样的引用类型都是默认使用引用传递。
命名返回值被初始化为相应类型的零值,当需要返回的时候,我们只需要一条简单的不带参数的return语句。需要注意的是,即使只有一个命名返回值,也需要使用 () 括起来
前面说过,函数签名中的最后传入参数可以具有前缀为…的类型(…int),这样的函数称为变参函数。
变参函数可以接受某种类型的切片 slice 为参数:
package main
import (
"fmt"
)
// 变参函数,参数不定长
func list(nums ...int) {
fmt.Println(nums)
}
func main() {
// 常规调用,参数可以多个
list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
// 在参数同类型时,可以组成slice使用 parms... 进行参数传递
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
list(numbers...) // slice时使用
}
内置函数
Go 语言拥有一些内置函数,内置函数是预先声明的,它们像任何其他函数一样被调用,内置函数没有标准的类型,因此它们只能出现在调用表达式中,它们不能用作函数值。它们有时可以针对不同的类型进行操作:
内置函数 | 说明 |
---|---|
close | 用于通道,对于通道c,内置函数close©将不再在通道c上发送值。 如果c是仅接收通道,则会出错。 发送或关闭已关闭的通道会导致运行时错误。 关闭nil通道也会导致运行时错误。 |
new、make | new 和 make 均是用于分配内存:new用于值类型的内存分配,并且置为零值。make只用于slice、map以及channel这三种引用数据类型的内存分配和初始化。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针。make(T) 它返回类型T的值(不是* T)。 |
make()内置函数声明不同类型时的参数以及具体作用请见下面说明:
调用 | T的类型 | 结果 |
---|---|---|
make(T, n) | slice | T为切片类型,长度和容量都为n |
make(T, n, m) | slice | T为切片类型,长度为n,容量为m (n<=m ,否则错误) |
make(T) | map | T为字典类型 |
make(T, n) | map | T为字典类型,初始化n个元素的空间 |
make(T) | channel | T为通道类型,无缓冲区 |
make(T, n) | channel | T为通道类型,缓冲区长度为n |
make()内置函数的实际使用举例见下面代码以及注释:
s := make([]int, 10, 100) // slice with len(s) == 10, cap(s) == 100
s := make([]int, 1e3) // slice with len(s) == cap(s) == 1000
s := make([]int, 1<<63) // illegal: len(s) is not representable by a value of type int
s := make([]int, 10, 0) // illegal: len(s) > cap(s)
c := make(chan int, 10) // channel with a buffer size of 10
m := make(map[string]int, 100) // map with initial space for approximately 100 elements
new(T)内置函数在运行时为该类型的变量分配内存,并返回指向它的类型* T的值。 并对变量初始化。
package main
import "fmt"
type S struct {
a int
b float64
}
func main() {
s := new(S)
s.a = 1
s.b = 2
fmt.Println(s)
}
new(S)为S类型的变量分配内存,并初始化(a = 0,b = 0.0),返回包含该位置地址的类型* S的值。
内置函数 | 参数类型 | 结果 |
---|---|---|
len(s) | string type ,[n]T, *[n]T ,[]T ,map[K]T ,chan T | string的长度(按照字节计算),数组长度 ,切片长度 ,字典长度 ,通道缓冲区中排队的元素数 |
cap(s) | [n]T, *[n]T ,[]T ,chan T | 数组长度 ,切片容量 ,通道缓冲区容量 |
对于len(s)和cap(s),如果s为nil值,则两个函数的取值都是0,我们还需要记住一个规则:
0 <= len(s) <= cap(s)
在Go语言中,常量在某些计算条件下也可以通过表达式计算得到。比如:如果s是字符串常量,则表达式len(s)是常量。 如果s的类型是数组或指向数组的指针而表达式不包含通道接收或(非常量)函数调用,则表达式len(s)和cap(s)是常量;否则len和cap的调用不是常量。
内置函数 | 说明 |
---|---|
append | 用于附加连接切片 |
copy | 用于复制切片 |
delete | 从字典删除元素 |
append(s S, x …T) S // T 是类型S的元素
append内置函数是变参函数,常常用来附加切片元素,将零或多个值x附加到S类型的切片s,它的可变参数必须是切片类型,并返回结果切片,也就是是S类型。值x传递给类型为…的参数T,其中T 是S的元素类型,并且适用相应的参数传递规则:
s0 := []int{0, 0}
s1 := append(s0, 2) // append 附加连接单个元素 s1 == []int{0, 0, 2}
s2 := append(s1, 3, 5, 7) // append 附加连接多个元素 s2 == []int{0, 0, 2, 3, 5, 7}
s3 := append(s2, s0...) // append 附加连接切片s0 s3 == []int{0, 0, 2, 3, 5, 7, 0, 0}
s4 := append(s3[3:6], s3[2:]...) // append 附加切片指定值 s4 == []int{3, 5, 7, 2, 3, 5, 7, 0, 0}
var t []interface{}
t = append(t, 42, 3.1415, "foo") // t == []interface{}{42, 3.1415, "foo"}
var b []byte
b = append(b, "bar"...) // append 附加连接字符串内容 b == []byte{'b', 'a', 'r' }
copy(dst, src []T) int
copy(dst []byte, src string) int
copy内置函数常常将切片元素从源src复制到目标dst,并返回复制的元素数。 两个参数必须具有相同的元素类型T,并且必须可以分配给类型为[] T的切片。 复制的元素数量是len(src)和len(dst)的最小值。
作为特殊情况,copy函数还接受可分配给[] byte类型的目标参数,其中source参数为字符串类型。 此种情况将字符串中的字节复制到字节切片中。
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int, 6)
var b = make([]byte, 5)
n1 := copy(s, a[0:]) // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
n3 := copy(b, "Hello, World!") // n3 == 5, b == []byte("Hello")
delete(m, k) //从字典m中删除元素 m[k]
内置函数delete从字典m中删除带有键k的元素。
内置函数 | 说明 |
---|---|
complex | 从浮点实部和虚部构造复数值 |
real | 提取复数值的实部 |
imag | 提取复数值的虚部 |
complex(realPart, imaginaryPart floatT) complexT
real(complexT) floatT
imag(complexT) floatT
内置函数complex用浮点实部和虚部构造复数值,而real和imag则提取复数值的实部和虚部。
对于complex,两个参数必须是相同的浮点类型,返回类型是具有相应浮点组成的复数类型。float32用于complex64参数,float64用于complex128参数。如果其中一个参数求值为无类型常量,则首先将其转换为另一个参数的类型。如果两个参数都计算为无类型常量,则它们必须是非复数或其虚部必须为零,并且函数的返回值是无类型复数常量。
对于real和imag,参数必须是复数类型,返回类型是相应的浮点类型:float32一般为complex64返回类型,float64一般为complex128返回类型。如果参数求值为无类型常量,则它必须是数字,并且函数的返回值是无类型浮点常量。
real和imag函数一起形成复数的逆,因此对于复数类型Z的值z,z == Z(complex(real(z),imag(z)))。
如果这些函数的操作数都是常量,则返回值是常量。
var a = complex(2, -2) // complex128
const b = complex(1.0, -1.4) // 无类型complex 常量 1 - 1.4i
x := float32(math.Cos(math.Pi/2)) // float32
var c64 = complex(5, -x) // complex64
var s uint = complex(1, 0) // 无类型 complex 常量 1 + 0i 可以转为uint
var rl = real(c64) // float32
var im = imag(a) // float64
const c = imag(b) // 无类型常量 -1.4
内置函数 | 说明 |
---|---|
panic | 用来表示非常严重的不可恢复的异常错误 |
recover | 用于从 panic 或 错误场景中恢复 |
func panic(interface{})
func recover() interface{}
panic和recover两个内置函数,协助报告和处理运行时异常和程序定义的错误。
在执行函数F时,显式调用panic或者运行时发生panic都会终止F的执行。然后,由F延迟(defer)的任何函数都照常执行。 依此类推,直到执行goroutine中的顶级函数延迟。 此时,程序终止并报告错误条件,包括panic参数的值。
panic(42)
panic("unreachable")
panic(Error("cannot parse"))
recover函数允许程序管理发生panic的goroutine的行为。
另外,Go语言中提供了几个在引导期间有用的内置函数。 这些函数不保证会保留在Go语言中,一般不建议使用。
print 打印所有参数
println 打印所有参数并换行
递归与回调
函数直接或间接调用函数本身,则该函数称为递归函数。使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的内存分配耗尽。有时我们可以通过循环来解决:
package main
import "fmt"
// Factorial函数递归调用
func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
// Fac2函数循环计算
func Fac2(n uint64) (result uint64) {
result = 1
var un uint64 = 1
for i := un; i <= n; i++ {
result *= i
}
return
}
func main() {
var i uint64 = 10
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(i))
fmt.Printf("%d 的阶乘是 %d\n", i, Fac2(i))
}
10 的阶乘是 3628800
10 的阶乘是 3628800
Go 语言中也可以使用相互调用的递归函数:多个函数之间相互调用形成闭环。因为 Go 语言编译器的特殊性,这些函数的声明顺序可以是任意的。
Go语言中函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。
package main
import (
"fmt"
)
func main() {
callback(1, Add)
}
func Add(a, b int) {
fmt.Printf("%d 与 %d 相加的和是: %d\n", a, b, a+b)
}
func callback(y int, f func(int, int)) {
f(y, 2) // 回调函数f
}
1 与 2 相加的和是: 3
匿名函数
函数值字面量是一种表达式,它的值被称为匿名函数。从形式上看当我们不给函数起名字的时候,可以使用匿名函数,例如:
func(x, y int) int { return x + y }
这样的函数不能够独立存在,但可以被赋值于某个变量,即保存函数的地址到变量中:
fplus := func(x, y int) int { return x + y }
然后通过变量名对函数进行调用:
fplus(3, 4)
当然,也可以直接对匿名函数进行调用,注意匿名函数的最后面加上了括号并填入了参数值,如果没有参数,也需要加上空括号,代表直接调用:
func(x, y int) int { return x + y } (3, 4)
参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。
下面代码演示了上面的几种情况:
package main
import (
"fmt"
)
func main() {
fn := func() {
fmt.Println("hello")
}
fn()
fmt.Println("匿名函数加法求和:", func(x, y int) int { return x + y }(3, 4))
func() {
sum := 0
for i := 1; i <= 1e6; i++ {
sum += i
}
fmt.Println("匿名函数加法循环求和:", sum)
}()
}
hello
匿名函数加法求和: 7
匿名函数加法循环求和: 500000500000
闭包函数
匿名函数同样也被称之为闭包。
闭包可被允许调用定义在其环境下的变量,可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。闭包继承了函数所声明时的作用域,作用域内的变量都被共享到闭包的环境中,因此这些变量可以在闭包中被操作,直到被销毁。也可以理解为内层函数引用了外层函数中的变量或称为引用了自由变量。
实质上看,闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。由闭包的实质含义,我们可以推论:闭包获取捕获变量相当于引用传递,而非值传递;对于闭包函数捕获的常量和变量,无论闭包何时何处被调用,闭包都可以使用这些常量和变量,而不用关心它们表面上的作用域。
我们通过下面代码来看看闭包的使用:
package main
import "fmt"
var G = 7
func main() {
// 影响全局变量G,代码块状态持续
y := func() int {
fmt.Printf("G: %d, G的地址:%p\n", G, &G)
G += 1
return G
}
fmt.Println(y(), y)
fmt.Println(y(), y)
fmt.Println(y(), y) //y的地址
// 影响全局变量G,注意z的匿名函数是直接执行,所以结果不变
z := func() int {
G += 1
return G
}()
fmt.Println(z, &z)
fmt.Println(z, &z)
fmt.Println(z, &z)
// 影响外层(自由)变量i,代码块状态持续
var f = N()
fmt.Println(f(1), &f)
fmt.Println(f(1), &f)
fmt.Println(f(1), &f)
var f1 = N()
fmt.Println(f1(1), &f1)
}
func N() func(int) int {
var i int
return func(d int) int {
fmt.Printf("i: %d, i的地址:%p\n", i, &i)
i += d
return i
}
}
输出:
G: 7, G的地址:0x10782f8
8 0xfd5ce0
G: 8, G的地址:0x10782f8
9 0xfd5ce0
G: 9, G的地址:0x10782f8
10 0xfd5ce0
11 0xc00000a0c0
11 0xc00000a0c0
11 0xc00000a0c0
i: 0, i的地址:0xc00000a0c8
1 0xc00003e028
i: 1, i的地址:0xc00000a0c8
2 0xc00003e028
i: 2, i的地址:0xc00000a0c8
3 0xc00003e028
i: 0, i的地址:0xc00000a0d0
1 0xc00003e030
首先强调一点,G是闭包中被捕获的全局变量,因此,对于每一次引用,G的地址都是固定的,i是函数内部局部变量,地址也是固定的,他们都可以被闭包保持状态并修改。还要注意,f和f1是不同的实例,它们的地址是不一样的。
变参函数
可变参数也就是不定长参数,支持可变参数列表的函数可以支持任意个传入参数,比如fmt.Println函数就是一个支持可变长参数列表的函数。
package main
import "fmt"
func Greeting(who ...string) {
for k, v := range who {
fmt.Println(k, v)
}
}
func main() {
s := []string{"James", "Jasmine", "Jack"}
Greeting(s...) // 注意这里切片s... ,把切片打散传入,与s具有相同底层数组的值。
}
0 James
1 Jasmine
2 Jack
高阶函数
有时在定义所需功能时我们可以利用函数作为(其它函数的)参数的事实来使用高阶函数。
定义一个通用的 Process() 函数,它接收一个作用于每一辆 car 的 f 函数作参数:
// Process all cars with the given function f:
func (cs Cars) Process(f func(car *Car)) {
for _, c := range cs {
f(c)
}
}
17.Type关键字
Type关键字在Go语言中作用很重要,比如定义结构体,接口,还可以自定义类型,定义类型别名等。自定义类型由一组值以及作用于这些值的方法组成,类型一般有类型名称,往往从现有类型组合通过Type关键字构造出一个新的类型。
Type 自定义类型
在Go 语言中,基础类型有下面几种:
bool error
complex64 complex128
float32 float64
int int8 int16 int32 int64 rune
string byte
uint uint8 uint16 uint32 uint64 uintptr
使用 type 关键字可以定义我们自己的类型,如我们可以使用type定义一个新的结构体,但也可以把一个已经存在的类型作为基础类型而定义新类型,然后就可以在我们的代码中使用新的类型名字,这称为自定义类型,如:type IZ int
这里IZ就是完全是一种新类型,然后我们可以使用下面的方式声明变量:var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能。
如果我们有多个类型需要定义,可以使用因式分解关键字的方式,例如:
type (
IZ int
FZ float64
STR string
)
在 type IZ int 中,IZ 就是在 int 类型基础构建的新名称,这称为自定义类型。然后就可以使用 IZ 来操作 int 类型的数据。使用这种方法定义之后的类型可以拥有更多的特性,但是在类型转换时必须显式转换。
每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于 Go 语言不存在隐式类型转换,因此所有的转换都必须显式说明,就像调用一个函数一样(类型在这里的作用可以看作是一种函数):valueOfTypeB = typeB(valueOfTypeA)
type TZ int 中,新类型不会拥有原基础类型所附带的方法,如下面代码所示:
package main
import (
"fmt"
)
type A struct {
Face int
}
type Aa A // 自定义新类型Aa,没有基础类型A的方法
func (a A) f() {
fmt.Println("hi ", a.Face)
}
func main() {
var s A = A{ Face: 9 }
s.f()
var sa = Aa{ Face: 9 }
sa.f()
}
输出:
sa.f undefined (type Aa has no field or method f)
Type 定义类型别名
type IZ = int
这种写法其实是定义了int类型的别名,类型别名在1.9中实现,可将别名类型和原类型这两个类型视为完全一致使用。type IZ int 其实是定义了新类型,这和类型别名完全不是一个含义。自定义类型不会拥有原类型附带的方法,而别名是拥有原类型附带的。下面举2个例子说明:
如果是类型别名,完整拥有其方法:
package main
import (
"fmt"
)
type A struct {
Face int
}
type Aa = A // 类型别名
func (a A) f() {
fmt.Println("hi ", a.Face)
}
func main() {
var s A = A{Face: 9}
s.f()
var sa Aa = Aa{Face: 9}
sa.f()
}
结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是NULL或 0)。值得注意的是,Go 语言中不存在类型继承。
函数也是一个确定的类型,就是以函数签名作为类型。这种类型的定义例如:type typeFunc func ( int, int) int
我们可以在函数体中的某处返回使用类型为 typeFunc 的变量 varfunc:return varfunc
自定义类型不会继承原有类型的方法,但接口方法或组合类型的内嵌元素则保留原有的方法。
// Mutex 用两种方法,Lock and Unlock。
type Mutex struct { /* Mutex fields */ }
func (m *Mutex) Lock() { /* Lock implementation */ }
func (m *Mutex) Unlock() { /* Unlock implementation */ }
// NewMutex和 Mutex 一样的数据结构,但是其方法是空的。
type NewMutex Mutex
// PtrMutex 的方法也是空的
type PtrMutex *Mutex
// *PrintableMutex 拥有Lock and Unlock 方法
type PrintableMutex struct {
Mutex
}
18.结构体(struct)
Go 通过结构体的形式支持用户自定义类型,或者叫定制类型。
- Go 语言结构体是实现自定义类型的一种重要数据类型。
- 结构体是复合类型(composite types),它由一系列属性组成,每个属性都有自己的类型和值的,结构体通过属性把数据聚集在一起。
- 结构体类型和字段的命名遵循可见性规则。
- 方法(Method)可以访问这些数据,就好像它们是这个独立实体的一部分。
- 结构体是值类型,因此可以通过 new 函数来创建。
结构体是由一系列称为字段(fields)的命名元素组成,每个元素都有一个名称和一个类型。 字段名称可以显式指定(IdentifierList)或隐式指定(EmbeddedField),没有显式字段名称的字段称为匿名(内嵌)字段。在结构体中,非空字段名称必须是唯一的。
结构体定义的一般方式如下:
type identifier struct {
field1 type1
field2 type2
...
}
结构体里的字段一般都有名字,像 field1、field2 等,如果字段在代码中从来也不会被用到,那么可以命名它为 _。
空结构体如下所示:struct {}
具有6个字段的结构体:
struct {
x, y int
u float32
_ float32 // 填充
A *[]int
F func()
}
对于匿名字段,必须将匿名字段指定为类型名称T或指向非接口类型名称* T的指针,并且T本身可能不是指针类型。
struct {
T1 // 字段名 T1
*T2 // 字段名 T2
P.T3 // 字段名 T3
*P.T4 // f字段名T4
x, y int // 字段名 x 和 y
}
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:
type S struct { a int; b float64 }
new(S)
new(S)为S类型的变量分配内存,并初始化(a = 0,b = 0.0),返回包含该位置地址的类型* S的值。
我们一般的惯用方法是:t := new(T),变量 t 是一个指向 T的指针,此时结构体字段的值是它们所属类型的零值。
也可以这样写:var t T ,也会给 t 分配内存,并零值化内存,但是这个时候 t 是类型T。
在这两种方式中,t 通常被称做类型 T 的一个实例(instance)或对象(object)。
使用点号符“.”可以获取结构体字段的值:structname.fieldname。在 Go 语言中“.”叫选择器(selector)。无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的选择表示法来引用结构体的字段:
type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i
type Interval struct {
start int
end int
}
结构体变量有下面几种初始化方式,前面一种按照字段顺序,后面两种则对应字段名来初始化赋值:
- intr := Interval{0, 3}
- intr := Interval{end:5, start:1}
- intr := Interval{end:5}
复合字面量是构造结构体,数组,切片和字典的值,并每次都创建新值。声明和初始化一个结构体实例(一个结构体字面量:struct-literal)方式如下:
定义结构体类型Point3D和Line:
type Point3D struct { x, y, z float64 }
type Line struct { p, q Point3D }
声明并初始化:
origin := Point3D{} // Point3D 是零值
line := Line{origin, Point3D{y: -4, z: 12.3}} // line.q.x 是零值
这里 Point3D{}以及 Line{origin, Point3D{y: -4, z: 12.3}}都是结构体字面量。
表达式 new(Type) 和 &Type{} 是等价的。&struct1{a, b, c} 是一种简写,底层仍然会调用 new (),这里值的顺序必须按照字段顺序来写。也可以通过在值的前面放上字段名来初始化字段的方式,这种方式就不必按照顺序来写了。
结构体类型和字段的命名遵循可见性规则,一个导出的结构体类型中有些字段是导出的,也即首字母大写字段会导出;另一些不可见,也即首字母小写为未导出,对外不可见。
结构体特性
- 结构体的内存布局 Go 语言中,结构体和它所包含的数据在内存中是以连续块的形式存在的,即使结构体中嵌套有其他的结构体,这在性能上带来了很大的优势。
- 递归结构体 结构体类型可以通过引用自身(指针类型)来定义。这在定义链表或二叉树的节点时特别有用,此时节点包含指向临近节点的链接。
- 使用工厂方法 通过参考应用可见性规则,我们可以设定结构体名不能导出,就可以达到使用 new 函数,强制使用工厂方法的目的。
type H struct {
int
*H
}
type matrix struct {
...
}
func NewMatrix(params) *matrix {
m := new(matrix) // 初始化 m
return m
}
在包外,只有通过NewMatrix函数才可以初始化matrix 结构。
带标签的结构体
结构体中的字段除了有名字和类型外,还可以有一个可选的标签(tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有 reflect 包能获取它。
reflect包可以在运行时自省类型、属性和方法,如变量是结构体类型,可以通过 Field 来索引结构体的字段,然后就可以使用 Tag 属性。
package main
import (
"fmt"
"reflect"
)
type Student struct {
name string "学生名字" // 结构体标签
Age int "学生年龄" // 结构体标签
Room int `json:"Roomid"` // 结构体标签
}
func main() {
st := Student{"Titan", 14, 102}
fmt.Println(reflect.TypeOf(st).Field(0).Tag) //学生名字
fmt.Println(reflect.TypeOf(st).Field(1).Tag) //学生年龄
fmt.Println(reflect.TypeOf(st).Field(2).Tag) //json:"Roomid"
fmt.Println(st) //{Titan 14 102}
}
匿名成员
Go语言结构体中可以包含一个或多个匿名(内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的,此时类型就是字段的名字(这一特征决定了在一个结构体中,每种数据类型只能有一个匿名字段)。
匿名(内嵌)字段本身也可以是一个结构体类型,即结构体可以包含内嵌结构体。
type Human struct {
name string
}
type Student struct { // 含内嵌结构体Human
Human // 匿名(内嵌)字段
int // 匿名(内嵌)字段
}
Go语言结构体中这种含匿名(内嵌)字段和内嵌结构体的结构,可近似地理解为面向对象语言中的继承概念。
Go 语言中的继承是通过内嵌或者说组合来实现的,所以可以说,在 Go 语言中,相比较于继承,组合更受青睐。
嵌入与聚合
结构体中包含匿名(内嵌)字段叫嵌入或者内嵌;而如果结构体中字段包含了类型名,还有字段名,则是聚合。聚合的在JAVA和C++都是常见的方式,而内嵌则是Go 的特有方式。
type Human struct {
name string
}
type Person1 struct { // 内嵌
Human
}
type Person2 struct { // 内嵌, 这种内嵌与上面内嵌有差异
*Human
}
type Person3 struct{ // 聚合
human Human
}
嵌入在结构体中广泛使用,在Go语言中如果只考虑结构体和接口的嵌入组合方式,一共有下面四种:
1.在接口中嵌入接口:
这里指的是在接口中定义中嵌入接口类型,而不是接口的一个实例,相当于合并了两个接口类型定义的全部函数。下面只有同时实现了Writer和 Reader 的接口,才可以说是实现了Teacher接口,即可以作为Teacher的实例。Teacher接口嵌入了Writer和 Reader 两个接口,在Teacher接口中,Writer和 Reader是两个匿名(内嵌)字段。
type Writer interface{
Write()
}
type Reader interface{
Read()
}
type Teacher interface{
Reader
Writer
}
2.在接口中嵌入结构体:
这种方式在Go语言中是不合法的,不能通过编译
type Human struct {
name string
}
type Writer interface {
Write()
}
type Reader interface {
Read()
}
type Teacher interface {
Reader
Writer
Human
}
存在语法错误,并不具有实际的含义,编译报错: interface contains embedded non-interface Base(Interface 不能嵌入非interface的类型)
3.在结构体中内嵌接口:
初始化的时候,内嵌接口要用一个实现此接口的结构体赋值;或者定义一个新结构体,可以把新结构体作为receiver,实现接口的方法就实现了接口(先记住这句话,后面在讲述方法时会解释),这个新结构体可作为初始化时实现了内嵌接口的结构体来赋值。
package main
import (
"fmt"
)
type Writer interface {
Write()
}
type Author struct {
name string
Writer
}
// 定义新结构体,重点是实现接口方法Write()
type Other struct {
i int
}
func (a Author) Write() {
fmt.Println(a.name, " Write.")
}
// 新结构体Other实现接口方法Write(),也就可以初始化时赋值给Writer 接口
func (o Other) Write() {
fmt.Println("Other Write.")
}
func main() {
// 方法一:Other{99}作为Writer 接口赋值
Ao := Author{"Other", Other{99}}
Ao.Write()
// 方法二:简易做法,对接口使用零值,可以完成初始化
Au := Author{name: "Hawking"}
Au.Write()
}
4.在结构体中嵌入结构体:
在结构体嵌入结构体很好理解,但不能嵌入自身值类型,可以嵌入自身的指针类型即递归嵌套。
在初始化时,内嵌结构体也进行赋值;外层结构自动获得内嵌结构体所有定义的字段和实现的方法。
下面代码完整演示了结构体中嵌入结构体,初始化以及字段的选择调用:
package main
import (
"fmt"
)
type Human struct {
name string // 姓名
Gender string // 性别
Age int // 年龄
string // 匿名字段
}
type Student struct {
Human // 匿名字段
Room int // 教室
int // 匿名字段
}
func main() {
//使用new方式
stu := new(Student)
stu.Room = 102
stu.Human.name = "Titan"
stu.Gender = "男"
stu.Human.Age = 14
stu.Human.string = "Student"
fmt.Println("stu is:", stu)
fmt.Printf("Student.Room is: %d\n", stu.Room)
fmt.Printf("Student.int is: %d\n", stu.int) // 初始化时已自动给予零值:0
fmt.Printf("Student.Human.name is: %s\n", stu.name) // (*stu).name
fmt.Printf("Student.Human.Gender is: %s\n", stu.Gender)
fmt.Printf("Student.Human.Age is: %d\n", stu.Age)
fmt.Printf("Student.Human.string is: %s\n", stu.string)
// 使用结构体字面量赋值
stud := Student{Room: 102, Human: Human{"Hawking", "男", 14, "Monitor"}}
fmt.Println("stud is:", stud)
fmt.Printf("Student.Room is: %d\n", stud.Room)
fmt.Printf("Student.int is: %d\n", stud.int) // 初始化时已自动给予零值:0
fmt.Printf("Student.Human.name is: %s\n", stud.Human.name)
fmt.Printf("Student.Human.Gender is: %s\n", stud.Human.Gender)
fmt.Printf("Student.Human.Age is: %d\n", stud.Human.Age)
fmt.Printf("Student.Human.string is: %s\n", stud.Human.string)
}
输出:
Student is: &{{Titan 男 14 Student} 102 25}
Student.Room is: 102
Student.int is: 25
Student.Human.name is: Titan
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Student
+++++++++++++++++++++++++++++
stud is: {{Hawking 男 14 Monitor} 102 101}
Student.Room is: 102
Student.int is: 101
Student.Human.name is: Hawking
Student.Human.Gender is: 男
Student.Human.Age is: 14
Student.Human.string is: Monitor
内嵌结构体的字段,例如我们即可以stu.Human.name这样来选择使用,而如果外层结构体中没有同名的name字段,也可以stu.name直接来选择使用。对于嵌入和聚合结构体而言,我们在选择调用内部字段时,可以不用多层选择调用,在不同名情况下可直接调用。比如stu.name这样效果实际上与stu.Human.name一样。
我们通过对结构体使用new(T),struct{filed:value}两种方式来声明初始化,这两种方式分别得到*T,和T。
我们从输出stu is: &{ {Titan 男 14 Student} 102 0} 可以得知,stu 是个指针,但是我们在随后调用字段时并没有使用指针,这是在Go语言中这里的 stu.name 相当于(*stu).name,这是一个语法糖,一般我们都使用stu.name方式来调用,但我们要知道有这个语法糖存在。
命名冲突
当两个字段拥有相同的名字(可能是继承来的名字)时该怎么办呢?外层名字会覆盖内层名字(但是两者的内存空间都保留)。
如果相同的名字在同一级别出现了两次,如果这个名字被程序使用了,将会引发一个错误,但不使用没关系。没有好办法来解决这种问题引起的二义性,一般由程序员完整写出来避免错误。
下面代码中如果写成 c.a 是错误的,因为我们不知道到底是要调用 c.A.a 还是 c.B.a。其实只要我们完整写出来(如:c.B.a)就不存在这个问题。
package main
import "fmt"
type A struct{ a int }
type B struct{ a, b int }
type C struct {
A
B
}
var c C
func main() {
c.A.a = 1
c.B.a = 3
c.b = 2
fmt.Println(c) //{{1} {3 2}}
}
19.接口是什么
Go 语言接口定义了一组方法集合,但是这些方法集合仅仅只是被定义,它们没有在接口中实现。接口(interface)类型是Go语言的一种数据类型。而因为所有的类型包括自定义类型都实现了空接口interface{},所以空接口interface{}可以被当做任意类型的数值。
Go 语言中的所有类型包括自定义类型都实现了interface{}接口,这意味着所有的类型如string、 int、 int64甚至是自定义的struct类型都拥有interface{}的接口,这一点interface{}和Java中的Object类比较相似。
接口类型的未初始化变量的值为nil。
var i interface{} = 99 // i可以是任何类型
i = 44.09
i = "All" // i 可接受任意类型的赋值
接口(interface)就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现。Go 语言通过它可以实现很多面向对象的特性。
通过如下格式定义接口:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
上面的 Namer 是一个接口类型,按照惯例,单方法接口由方法名称加上-er后缀或类似修改来命名,以构造代理名词:Reader,Writer,Formatter,CloseNotifier等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头等。
Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。如标准包io中定义了下面2个接口:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
在Go语言中,如果接口的所有方法在某个类型方法集中被实现,则认为该类型实现了这个接口。
类型不用显式声明实现了接口,只需要实现接口所有方法,这样的隐式实现解藕了实现接口的包和定义接口的包。
同一个接口可被多个类型可以实现,一个类型也可以实现多个接口。实现了某个接口的类型,还可以有其它的方法。有时我们甚至都不知道某个类型定义的方法集巧合地实现了某个接口。这种灵活性使我们不用像JAVA语言那样需要显式implement,一旦类型不需要实现某个接口,我们甚至可以不改动任何代码。
类型需要实现接口方法集中的所有方法,一定是接口方法集中所有方法。类型实现了这个接口,那么接口类型的变量也就可以存放该类型的值。
如下代码所示,结构体A和类型I都实现了接口B的方法f(),所有这两种类型也具有了接口B的一切特性,可以将该类型的值存储在接口B类型的变量中:
package main
import (
"fmt"
)
type A struct {
Books int
}
type B interface {
f()
}
func (a A) f() {
fmt.Println("A.f() ", a.Books)
}
type I int
func (i I) f() {
fmt.Println("I.f() ", i)
}
func main() {
var a = A{Books: 9}
a.f() //A.f() 9
var b B = A{Books: 99} // 接口类型可接受结构体A的值,因为结构体A实现了接口
b.f() //A.f() 99
var i I = 199 // I是int类型引申出来的新类型
i.f() //I.f() 199
var b2 = I(299) // 接口类型可接受新类型I的值,因为新类型I实现了接口
b2.f() //I.f() 299
}
如果接口在类型之后才定义,或者二者处于不同的包中。但只要类型实现了接口中的所有方法,这个类型就实现了此接口。
因此Go语言中接口具有强大的灵活性。
注意:接口中的方法必须要全部实现,才能实现接口。
接口嵌入
一个接口可以包含一个或多个其他的接口,但是在接口内不能嵌入结构体,也不能嵌入接口自身,否则编译会出错。
下面这两种嵌入接口自身的方式都不能编译通过:
// 编译错误:invalid recursive type Bad
type Bad interface {
Bad
}
// 编译错误:invalid recursive type Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}
比如下面的接口 File 包含了 ReadWrite 和 Lock 的所有方法,它还额外有一个 Close() 方法。接口的嵌入方式和结构体的嵌入方式语法上差不多,直接写接口名即可。
type ReadWrite interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type Lock interface {
Lock()
Unlock()
}
type File interface {
ReadWrite
Lock
Close()
}
类型断言
前面我们可以把实现了某个接口的类型值保存在接口变量中,但反过来某个接口变量属于哪个类型呢?如何检测接口变量的类型呢?这就是类型断言(Type Assertion)的作用。
接口类型I的变量 varI 中可以包含任何实现了这个接口的类型的值,如果多个类型都实现了这个接口,所以有时我们需要用一种动态方式来检测它的真实类型,即在运行时确定变量的实际类型。
通常我们可以使用类型断言(value, ok := element.(T))来测试在某个时刻接口变量 varI 是否包含类型 T 的值:value, ok := varI.(T) // 类型断言
varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of I) on left) 。
package main
import "fmt"
type I interface {
f()
}
type T string
func (t T) f() {
fmt.Println(t)
}
func main() {
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
// varI已经转为T类型
fmt.Println("varI类型断言结果为:", v) //varI类型断言结果为: Tstring
varI.f() //Tstring
}
}
如果断言成功,v 是 varI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,ok 是 false,也没有运行时错误发生。
接口类型向普通类型转换有两种方式:Comma-ok断言和Type-switch测试。
通过Type-switch做类型判断
接口变量的类型可以使用一种特殊形式的 switch 做类型断言:
package main
import "fmt"
type Stringer interface {
}
var value interface{}
func main() {
// Type-switch做类型判断
switch str := value.(type) {
case string:
fmt.Println("value类型断言结果为string:", str)
case Stringer:
fmt.Println("value类型断言结果为Stringer:", str)
case nil:
fmt.Println("value类型断言结果为nil:", str)
default:
fmt.Println("value类型不在上述类型之中")
}
}
可以用 Type-switch 进行运行时类型分析,但是在 type-switch 时不允许有 fallthrough 。Type-switch让我们在处理未知类型的数据时,比如解析 json 等编码的数据,会非常方便。
测试一个值是否实现了某个接口(Comma-ok断言)
我们想测试它是否实现了 I 接口,可以这样做类型断言:
package main
import (
"fmt"
)
type I interface {
f()
}
type T string
func (t T) f() {
fmt.Println("T Method")
}
type Stringer interface {
String() string
}
func main() {
// 类型断言
var varI I
varI = T("Tstring")
if v, ok := varI.(T); ok { // 类型断言
fmt.Println("varI类型断言结果为:", v) // varI已经转为T类型
varI.f()
}
// Type-switch做类型判断
var value interface{} // 默认为零值
switch str := value.(type) {
case string:
fmt.Println("value类型断言结果为string:", str)
case Stringer:
fmt.Println("value类型断言结果为Stringer:", str)
default:
fmt.Println("value类型不在上述类型之中")
}
// Comma-ok断言
value = "类型断言检查"
str, ok := value.(string)
if ok {
fmt.Printf("value类型断言结果为:%T\n", str) // str已经转为string类型
} else {
fmt.Printf("value不是string类型 \n")
}
}
程序输出:
varI类型断言结果为: Tstring
T Method
value类型不在上述类型之中
value类型断言结果为:string
接口描述了一系列的行为,规定可以做什么行为,“当一个东西,走起来像鸭子,叫起来也像鸭子,游泳也像鸭子,那么我们可以认为他就是一只鸭子”。类型实现不同的接口将拥有不同的行为方法集合,这就是多态的本质。
使用接口使代码更具有普适性,例如函数的参数为接口变量。标准库中遵循了这个原则,但如果对接口概念没有良好的把握,是不能很好理解它是如何构建的。
那么为什么在Go语言中我们可以进行类型断言呢?我们可以在上面代码中看到,断言后的值 v, ok := varI.(T),v值对应的是一个类型名:Tstring 。 因为在Go语言中,一个接口值(Interface Value)其实是由两部分组成:type :value 。所以在做类型断言时,变量只能是接口类型变量,断言得到的值其实是接口值中对应的类型名。这在后面讨论reflect反射包时将会有更深入的说明。
接口与动态类型
在经典的面向对象语言(像 C++,Java 和 C#)中,往往将数据和方法被封装为类的概念:类中包含它们两者,并且不能剥离。
Go 语言中没有类,数据(结构体或更一般的类型)和方法是一种松耦合的正交关系。Go 语言中的接口必须提供一个指定方法集的实现,但是更加灵活通用:任何提供了接口方法实现代码的类型都隐式地实现了该接口,而不用显式地声明。该特性允许我们在不改变已有的代码的情况下定义和使用新接口。
接收一个(或多个)接口类型作为参数的函数,其实参可以是任何实现了该接口的类型。 实现了某个接口的类型可以被传给任何以此接口为参数的函数 。
Go 语言动态类型的实现通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有方法。我们也可以通过类型断言来检查接口变量是否实现了相应类型。
因此 Go 语言提供了动态语言的优点,却没有其他动态语言在运行时可能发生错误的缺点。Go 语言的接口提高了代码的分离度,改善了代码的复用性,使得代码开发过程中的设计模式更容易实现。
接口的提取
接口的提取,是非常有用的设计模式,良好的提取可以减少需要的类型和方法数量。而且在Go语言中不需要像传统的基于类的面向对象语言那样维护整个的类层次结构。
假设有一些拥有共同行为的对象,并且开发者想要抽象出这些行为,这时就可以创建一个接口来使用。在Go语言中这样操作甚至不会影响到前面开发的代码,所以我们不用提前设计出所有的接口,接口的设计可以不断演进,并且不用废弃之前的决定。而且类型要实现某个接口,类型本身不用改变,只需要在这个类型上实现新的接口方法集。
接口的继承
当一个类型包含(内嵌)另一个类型(实现了一个或多个接口)时,这个类型就可以使用(另一个类型)所有的接口方法。
类型可以通过继承多个接口来提供像多重继承一样的特性:
type ReaderWriter struct {
io.Reader
io.Writer
}
多态用得越多,代码就相对越少。这被认为是 Go 编程中的重要的最佳实践。
20.方法
方法的定义
在 Go 语言中,结构体就像是类的一种简化形式,那么面向对象程序员可能会问:类的方法在哪里呢?在 Go 语言中有一个概念,它和方法有着同样的名字,并且大体上意思相近。
Go 语言中方法和函数在形式上很像,它是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量。因此方法是一种特殊类型的函数,方法只是比函数多了一个接收器(receiver),当然在接口中定义的函数我们也称为方法(因为最终还是要通过绑定到类型来实现)。
正是因为有了接收器,方法才可以作用于接收器的类型(变量)上,类似于面向对象中类的方法可以作用于类属性上。
定义方法的一般格式如下:func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }
在方法名之前,func 关键字之后的括号中指定接收器 receiver。
type A struct {
Face int
}
func (a A) f() {
fmt.Println("hi ", a.Face)
}
上面代码中,我们定义了结构体 A ,注意f()就是 A 的方法,(a A)表示接收器。a 是 A的实例,f()是它的方法名,方法调用遵循传统的 object.name 即选择器符号:a.f()。
接收器(receiver)
接收器类型除了不能是指针类型或接口类型外,可以是其他任何类型,不仅仅是结构体类型,也可以是函数类型,还可以是 int、bool、string 等等为基础的自定义类型。
package main
import (
"fmt"
)
type Human struct {
name string // 姓名
Gender string // 性别
Age int // 年龄
string // 匿名字段
}
func (h Human) print() { // 值方法
fmt.Println("Human:", h)
}
type MyInt int
func (m MyInt) print() { // 值方法
fmt.Println("MyInt:", m)
}
func main() {
//使用new方式
hu := new(Human)
hu.name = "Titan"
hu.Gender = "男"
hu.Age = 14
hu.string = "Student"
hu.print()
// 指针变量
mi := new(MyInt)
mi.print()
// 使用结构体字面量赋值
hum := Human{"Hawking", "男", 14, "Monitor"}
hum.print()
// 值变量
myi := MyInt(99)
myi.print()
}
输出:
Human: {Titan 男 14 Student}
MyInt: 0
Human: {Hawking 男 14 Monitor}
MyInt: 99
接收器不能是一个接口类型,因为接口是一个抽象定义,但是方法却是具体实现;如果这样做会引发一个编译错误:invalid receiver type printer (pointer or interface type)
package main
import (
"fmt"
)
type printer interface {
print()
}
func (p printer) print() { // invalid receiver type printer (printer is an interface type)
fmt.Println("printer:", p)
}
func main() {}
接收器不能是一个指针类型,但是它可以是任何其他允许类型的指针。
package main
import (
"fmt"
)
type MyInt int
type Q *MyInt
func (q Q) print() {
// invalid receiver type Q (Q is a pointer type)
fmt.Println("Q:", q)
}
func main() {}
接收器不能是指针类型,但可以是类型的指针,有点绕口。下面我们看个例子:
package main
import (
"fmt"
)
type MyInt int
func (mi *MyInt) print() { // 指针接收器,指针方法
fmt.Println("MyInt:", *mi)
}
func (mi MyInt) echo() { // 值接收器,值方法
fmt.Println("MyInt:", mi)
}
func main() {
i := MyInt(9)
i.print()
i.echo()
}
如果有类型T,方法的接收器为(t T)时我们称为值接收器,该方法称为值方法;方法的接收器为(t *T)时我们称为指针接收器,该方法称为指针方法。
类型 T(或 T)上的所有方法的集合叫做类型 T(或 T)的方法集。
关于接收器的命名
社区约定的接收器命名是类型的一个或两个字母的缩写(像 c 或者 cl 对于 Client)。不要使用泛指的名字像是 me,this 或者 self,也不要使用过度描述的名字,简短即可。
方法表达式与方法值
在Go语言中,方法调用的方式如下:如有类型X的变量x,m()是其方法,则方法有效调用方式是x.m(),而x如果是指针变量,则 x.m() 是 (&x).m()的简写。所以我们看到指针方法的调用往往也写成 x.m(),其实是一种语法糖。
这里我们了解下Go语言的选择器(selector),如:x.f
package main
import (
"fmt"
)
type MyInt int
func (mi *MyInt) print() { // 指针接收器,指针方法
fmt.Println("MyInt:", *mi)
}
func (mi *MyInt) echo() { // 值接收器,值方法
fmt.Println("MyInt:", mi)
}
func main() {
i := MyInt(9)
i.print()
i.echo()
}
如果x不是包名,则表示是x(或* x)的f(字段或方法)。标识符f(字段或方法)称为选择器(selector),选择器(selector)不能是空白标识符。选择器(selector)表达式的类型是f的类型。
选择器f可以表示类型T的字段或方法,或者指嵌入字段T的字段或方法f。遍历到f的嵌入字段的层数被称为其在T中的深度。在T中声明的字段或方法f的深度为零。在T中的嵌入字段A中声明的字段或方法f的深度是A中的f的深度加1。
在Go语言中,我们认为方法的显式接收器(explicit receiver)x是方法x.m()的等效函数X.m()的第一个参数,所以x.m()和X.m(x)是等价的,下面我们看看具体例子:
package main
import (
"fmt"
)
type T struct {
a int
}
func (tv T) Mv(a int) int {
fmt.Printf("Mv的值是: %d\n", a)
return a
} // 值方法
func (tp *T) Mp(f float32) float32 {
fmt.Printf("Mp: %f\n", f)
return f
} // 指针方法
func main() {
var t T
// 下面几种调用方法是等价的
t.Mv(1) // 一般调用
T.Mv(t, 1) // 显式接收器t可以当做为函数的第一个参数
f0 := t.Mv // 通过选择器(selector)t.Mv将方法值赋值给一个变量 f0
f0(2)
T.Mv(t, 3)
(T).Mv(t, 4)
f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f1(t, 5)
f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f2(t, 6)
}
t.Mv(1)和T.Mv(t, 1)效果是一致的,这里显式接收器t可以当做为等效函数T.Mv()的第一个参数。而在Go语言中,我们可以利用选择器,将方法值(Method Value)取到,并可以将其赋值给其它变量。使用 t.Mv,就可以得到 Mv 方法的方法值,而且这个方法值绑定到了显式接收器(实参)t。
f0 := t.Mv // 通过选择器将方法值t.Mv赋值给一个变量 f0
除了使用选择器(selector)取到方法值外,还可以使用方法表达式(Method Expression) 取到函数值(Function Value)。方法表达式(Method Expression)产生的是一个函数值(Function Value)而不是方法值(Method Value)。
f1 := T.Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f1(t, 5)
f2 := (T).Mv // 利用方法表达式(Method Expression) T.Mv 取到函数值
f2(t, 6)
这个函数值的第一个参数必须是一个接收器:
f1(t, 5)
f2(t, 6)
上面有关选择器(selector),方法表达式(Method Expression) ,函数值(Function Value),方法值(Method Value)等概念可以帮助我们更好理解方法,掌握他们可以更好地使用好方法。
在Go语言中不允许方法重载,因为方法是函数,所以对于一个类型只能有唯一一个特定名称的方法。但是如果基于接收器类型,我们可以通过一种变通的方法,达到这个目的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的:
type MyInt1 int
type MyInt2 int
func (a *MyInt1) Add(b int) int { return 0 }
func (a *MyInt2) Add(b int) int { return 0 }
自定义类型方法与匿名嵌入
Go语言中类型加上它的方法集等价于面向对象中的类。但在 Go 语言中,类型的代码和绑定在它上面的方法集的代码可以不放置在同一个文件中,它们可以保存在同一个包下的其他源文件中。
下面是在非结构体类型上定义方法的例子:
type MyInt int
func (m MyInt) print() { // 值方法
fmt.Println("MyInt:", m)
}
注意:类型和作用在它上面定义的方法必须在同一个包里定义,所以基础类型int、float 等上不能直接定义。
类型在其他的,或是非本地的包里定义,在它上面定义方法都会发生错误。
package main
import (
"fmt"
)
func (i int) print() {
// cannot define new methods on non-local type int
fmt.Println("Int:", i)
}
func main() {
}
程序编译不通过,错误如下:
cannot define new methods on non-local type int
虽然我们不能直接为非同一包下的类型直接定义方法,但我们可以以这个类型(比如:int 或 float)为基础来自定义新类型,然后再为新类型定义方法。
package main
import (
"fmt"
)
type MyInt int
func (m MyInt) print() { // 值方法
fmt.Println("MyInt:", m)
}
func main() {
myi := MyInt(99)
myi.print() //MyInt: 99
}
MyInt类型由int 为基础自定义的,MyInt定义了一个方法print()。
下面我们再以这个代码为例看看在类型别名下的方法情况,类型别名情况下方法是保留的,但自定义的新类型方法是需要重新定义的,原方法不保留。
如果我们采用类型别名下面程序可正常运行,Go 1.9及以上版本编译通过:
package main
import (
"fmt"
)
type MyInt int
type NewInt = MyInt
func (m MyInt) print() { // 值方法
fmt.Println("MyInt:", m)
}
func main() {
myi := MyInt(99)
myi.print()
Ni := NewInt(myi)
Ni.print()
}
输出:
MyInt: 99
MyInt: 99
但上面代码我们稍微修改,把type NewInt = MyInt 改为type NewInt MyInt 。一个符号“=”去掉使得NewInt 变为新类型,会报程序错误:
Ni.print undefined (type NewInt has no field or method print)
因为Ni 属于新的自定义类型 NewInt, 它没有定义print()方法,需要另外定义这个方法。
我们也可以像下面这样将定义好的类型作为匿名类型嵌入在一个新的结构体中。当然新方法只在这个自定义类型上有效。
package main
import (
"fmt"
)
type Human struct {
name string // 姓名
Gender string // 性别
Age int // 年龄
string // 匿名字段
}
type Student struct {
Human // 匿名字段
Room int // 教室
int // 匿名字段
}
func (h Human) String() { // 值方法
fmt.Println("Human")
}
func (s Student) String() { // 值方法
fmt.Println("Student")
}
func (s Student) Print() { // 值方法
fmt.Println("Print")
}
func main() {
stud := Student{
Room: 102,
Human: Human{"Hawking", "男", 14, "Monitor"},
}
stud.String()
stud.Human.String()
}
Student
Human
函数和方法的区别
方法相对于函数多了接收器,这是他们之间最大的区别。
函数是直接调用,而方法是作用在接收器上,方法需要类型的实例来调用。方法接收器必须有一个显式的名字,这个名字必须在方法中被使用。
在接收器是指针时,方法可以改变接收器的值(或状态),这点函数也可以做到(当参数作为指针传递,即通过引用调用时,函数也可以改变参数的状态)。
在 Go 语言中,(接收器)类型关联的方法不写在类型结构里面,就像类那样;耦合更加宽松;类型和方法之间的关联由接收器来建立。
方法没有和定义的数据类型(结构体)混在一起,方法和数据是正交,而且数据和行为(方法)是相对独立的。
指针方法与值方法
有类型T,方法的接收器为(t T)时我们称为值接收器,该方法称为值方法;方法的接收器为(t *T)时我们称为指针接收器,该方法称为指针方法。
如果想要方法改变接收器的数据,就在接收器的指针上定义该方法;否则,就在普通的值类型上定义方法。这是指针方法和值方法最大的区别。
下面声明一个 T 类型的变量,并调用其方法 M1() 和 M2() 。
package main
import (
"fmt"
)
type T struct {
Name string
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
t1 := T{"t1"}
fmt.Println("M1调用前:", t1.Name)
t1.M1()
fmt.Println("M1调用后:", t1.Name)
fmt.Println("+++++++++++++++++++")
fmt.Println("Name设置前:", t1.Name)
t1.Name = "name3"
fmt.Println("Name设置后:", t1.Name)
fmt.Println("+++++++++++++++++++")
fmt.Println("M2调用前:", t1.Name)
t1.M2()
fmt.Println("M2调用后:", t1.Name)
}
输出:
M1调用前: t1
M1调用后: t1
+++++++++++++++++++
Name设置前: t1
Name设置后: name3
+++++++++++++++++++
M2调用前: name3
M2调用后: name2
分析:
t2.M1() => M1(t2), t2 是指针类型, 取 t2 的值并拷贝一份传给 M1。
t2.M2() => M2(t2),都是指针类型,不需要转换。
*T 类型的变量也可以调用M1()和M2()这两个方法。
从上面调用我们可以得知:无论你声明方法的接收器是指针接收器还是值接收器,Go都可以帮你隐式转换为正确的方法使用。
但我们需要记住,值变量只拥有值方法集,而指针变量则同时拥有值方法集和指针方法集。
接口变量上的指针方法与值方法
无论是T类型变量还是*T类型变量,都可调用值方法或指针方法。但如果是接口变量呢,那么这两个方法都可以调用吗?
我们添加一个接口看看:
package main
type T struct {
Name string
}
type Intf interface {
M1()
M2()
}
func (t T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
var t1 T = T{"t1"}
t1.M1()
t1.M2()
var t2 Intf = t1
t2.M1()
t2.M2()
}
输出:
cannot use t1 (variable of type T) as Intf value in variable declaration: T does not implement Intf (method M2 has pointer receiver)
上面代码中我们看到,var t2 Intf 中,t2是Intf接口类型变量,t1是T类型值变量。上面错误信息中已经明确了T没有实现接口Intf,所以不能直接赋值。这是为什么呢?
首先这是Go语言的一种规则,具体如下:
规则一:如果使用指针方法来实现一个接口,那么只有指向那个类型的指针才能够实现对应的接口。
规则二:如果使用值方法来实现一个接口,那么那个类型的值和指针都能够实现对应的接口。
按照上面两条规则的规则一,我们稍微修改下代码:
package main
type T struct {
Name string
}
type Interface interface {
M1()
M2()
}
func (t *T) M1() {
t.Name = "name1"
}
func (t *T) M2() {
t.Name = "name2"
}
func main() {
var t1 = T{"t1"}
t1.M1()
t1.M2()
var t2 Interface = &t1
t2.M1()
t2.M2()
}
程序编译通过。
程序编译通过。综合起来看,接口类型的变量(实现了该接口的类型变量)调用方法时,我们需要注意方法的接收器,是不是真正实现了接口。结合接口类型断言,我们做下测试:
package main
import (
"fmt"
)
type T struct {
Name string
}
type Intf interface {
M1()
M2()
}
func (t T) M1() {
t.Name = "name1"
fmt.Println("M1")
}
func (t *T) M2() {
t.Name = "name2"
fmt.Println("M2")
}
func main() {
var t1 = T{"t1"}
// interface{}(t1) 先转为空接口,再使用接口断言
_, ok1 := interface{}(t1).(Intf)
fmt.Println("t1 => Intf", ok1)
_, ok2 := interface{}(t1).(T)
fmt.Println("t1 => T", ok2)
t1.M1()
t1.M2()
_, ok3 := interface{}(t1).(*T)
fmt.Println("t1 => *T", ok3)
t1.M1()
t1.M2()
_, ok4 := interface{}(&t1).(Intf)
fmt.Println("&t1 => Intf", ok4)
t1.M1()
t1.M2()
_, ok5 := interface{}(&t1).(T)
fmt.Println("&t1 => T", ok5)
_, ok6 := interface{}(&t1).(*T)
fmt.Println("&t1 => *T", ok6)
t1.M1()
t1.M2()
}
输出:
t1 => Intf false
t1 => T true
M1
M2
t1 => *T false
M1
M2
&t1 => Intf true
M1
M2
&t1 => T false
&t1 => *T true
M1
M2
执行结果表明,t1 没有实现Intf方法集,不是Intf接口类型;而&t1 则实现了Intf方法集,是Intf接口类型,可以调用相应方法。t1 这个结构体值变量本身则调用值方法或者指针方法都是可以的,这是因为语法糖存在的原因。
按照上面的两条规则,那究竟怎么选择是指针接收器还是值接收器呢?
何时使用值类型
(1)如果接收器是一个 map,func 或者 chan,使用值类型(因为它们本身就是引用类型)。
(2)如果接收器是一个 slice,并且方法不执行 reslice 操作,也不重新分配内存给 slice,使用值类型。
(3)如果接收器是一个小的数组或者原生的值类型结构体类型(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接收器是一个简单地基本类型像是 int 和 string,使用值类型就好了。
值类型的接收器可以减少一定数量的内存垃圾生成,值类型接收器一般会在栈上分配到内存(但也不一定),在没搞明白代码想干什么之前,别为这个原因而选择值类型接收器。
何时使用指针类型
(1)如果方法需要修改接收器里的数据,则接收器必须是指针类型。
(2)如果接收器是一个包含了 sync.Mutex 或者类似同步字段的结构体,接收器必须是指针,这样可以避免拷贝。
(3)如果接收器是一个大的结构体或者数组,那么指针类型接收器更有效率。
(4)如果接收器是一个结构体,数组或者 slice,它们中任意一个元素是指针类型而且可能被修改,建议使用指针类型接收器,这样会增加程序的可读性。
最后如果实在还是不知道该使用哪种接收器,那么记住使用指针接收器是最靠谱的。
匿名类型的方法提升
当一个匿名类型被嵌入在结构体中时,匿名类型的可见方法也同样被内嵌,这在效果上等同于外层类型继承了这些方法:将父类型放在子类型中来实现亚型。这个机制提供了一种简单的方式来模拟经典面向对象语言中的子类和继承相关的效果。
当我们嵌入一个匿名类型,这个类型的方法就变成了外部类型的方法,但是当它的方法被调用时,方法的接收器是内部类型(嵌入的匿名类型),而非外部类型。
type People struct {
Age int
gender string
Name string
}
type OtherPeople struct {
People
}
func (p People) PeInfo() {
fmt.Println("People ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
因此嵌入类型的名字充当着字段名,同时嵌入类型作为内部类型存在,我们可以使用下面的调用方法:OtherPeople.People.PeInfo()
这儿我们可以通过类型名称来访问内部类型的字段和方法。然而,这些字段和方法也同样被提升到了外部类型,我们可以直接访问:OtherPeople.PeInfo()
前面我们看到了嵌入类型的方法提升,在 Go 语言中匿名嵌入类型方法集提升的规则:
给定一个结构体类型 S 和一个命名为 T 的类型,方法提升像下面规定的这样被包含在结构体方法集中:
- 如果 S 包含一个匿名字段 T,S 和 *S 的方法集都包含接收器为 T 的方法提升
这条规则说的是当我们嵌入一个类型T,嵌入类型的接收器为值类型的方法将被提升,可以被外部类型的值和指针调用。- 如果 S 包含一个匿名字段 T, *S 类型的方法集包含接收器为 *T 的方法提升
这条规则说的是当我们嵌入一个类型,可以被外部类型的指针调用的方法集只有嵌入类型的接收器为指针类型的方法集,也就是说,当外部类型使用指针调用内部类型的方法时,只有接收器为指针类型的内部类型方法集将被提升。- 如果 S 包含一个匿名字段 *T,S 和 *S 的方法集都包含接收器为 T 或者 *T 的方法提升
这条规则说的是当我们嵌入一个类型的指针,嵌入类型的接收器为值类型或指针类型的方法将被提升,可以被外部类型的值或者指针调用。
这就是语言规范里方法提升中仅有的三条规则,根据这个推导出一条规则:
如果 S 包含一个匿名字段 T,S 的方法集不包含接收器为 *T 的方法提升。
这条规则说的是当我们嵌入一个类型,嵌入类型的接收器为指针的方法将不能被外部类型的值访问。这也是跟我们陈述的接口规则一致。
简单地说也是两条规则:
规则一:如果S包含嵌入字段T,则S和S的方法集都包括具有接收方T的提升方法。S的方法集还包括具有接收方T的提升方法。
规则二:如果S包含嵌入字段T,则S和\S的方法集都包括具有接收器T或T的提升方法。
注意:以上规则由于 t.Meth() 会被自动转换为 (&t).Meth() 这个语法糖,导致我们很容易误解上面的规则不起作用,而实际上规则是有效的,在实际应用中我们可以留意这个问题。
我们通过下面代码验证下:
package main
import (
"fmt"
"reflect"
)
type People struct {
Age int
gender string
Name string
}
type OtherPeople struct {
People
}
type NewPeople People
func (p *NewPeople) PeName(pname string) {
fmt.Println("pold name:", p.Name)
p.Name = pname
fmt.Println("pnew name:", p.Name)
}
func (p NewPeople) PeInfo() {
fmt.Println("NewPeople ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
func (p *People) PeName(pname string) {
fmt.Println("old name:", p.Name)
p.Name = pname
fmt.Println("new name:", p.Name)
}
func (p People) PeInfo() {
fmt.Println("People ", p.Name, ": ", p.Age, "岁, 性别:", p.gender)
}
func methodSet(a interface{}) {
t := reflect.TypeOf(a)
fmt.Printf("a=>%T\n", a)
for i, n := 0, t.NumMethod(); i < n; i++ {
m := t.Method(i)
fmt.Println(i, ":", m.Name, m.Type)
}
}
func main() {
p := OtherPeople{People{26, "Male", "张三"}}
p.PeInfo() //People 张三 : 26 岁, 性别: Male
p.PeName("Joke") //old name: 张三 //new name: Joke
methodSet(p)
//a=>main.OtherPeople
//0 : PeInfo func(main.OtherPeople)
methodSet(&p)
//a=>*main.OtherPeople
//0 : PeInfo func(*main.OtherPeople)
//1 : PeName func(*main.OtherPeople, string)
pp := NewPeople{42, "Male", "李四"}
pp.PeInfo() //NewPeople 李四 : 42 岁, 性别: Male
pp.PeName("Haw") //pold name: 李四 //pnew name: Haw
methodSet(&pp)
//a=>*main.NewPeople
//0 : PeInfo func(*main.NewPeople)
//1 : PeName func(*main.NewPeople, string)
}
我们可以从上面输出看到,*OtherPeople 下有两个方法PeInfo(),PeName(string)可以调用,而OtherPeople只有一个方法PeInfo()可以调用。
但是在Go中存在一个语法糖:
p.PeInfo()
p.PeName("Joke")
methodSet(p) // T方法提升
虽然P 只有一个方法:PeInfo func(main.OtherPeople),但我们依然可以调用p.PeName(“Joke”)。
这里Go自动转为(&p).PeName(“Joke”),其调用后结果让我们以为p有两个方法,其实这里p只有一个方法。
21.协程(goroutine)
并发: 指的是程序的逻辑结构。如果程序代码结构中的某些函数逻辑上可以同时运行,但物理上未必会同时运行。
并行: 并行是指程序的运行状态。并行则指的就是在物理层面也就是使用了不同CPU在执行不同或者相同的任务。
并发
并发是在同一时间处理(dealing with)多件事情。并行是在同一时间做(doing)多件事情。并发的目的在于把当个 CPU 的利用率使用到最高。并行则需要多核 CPU 的支持。
Go 语言在语言层面上支持了并发,goroutine是Go语言提供的一种用户态线程,有时我们也称之为协程。所谓的协程,某种程度上也可以叫做轻量线程,它不由os而由应用程序创建和管理,因此使用开销较低(一般为4K)。我们可以创建很多的goroutine,并且它们跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都能使用cpu,并且是尽可能公平地使用cpu资源。
在操作系统的OS Thread和编程语言的User Thread之间,实际上存在3种线程对应模型,也就是:1:1,1:N,M:N。
N:1 多个(N)用户线程始终在一个内核线程上跑,context上下文切换很快,但是无法真正的利用多核。 1:1 一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文切换很慢,切换效率很低。 M:N 多个goroutine在多个内核线程上跑,这个可以集齐上面两者的优势,但是无疑增加了调度的难度。
M:N 综合两种方式(N:1,1:1)的优势。多个 goroutines 可以在多个 OS threads 上处理。既能快速切换上下文,也能利用多核的优势,而Go正是选择这种实现方式。
Go 语言中的goroutine是运行在多核CPU中的(通过runtime.GOMAXPROCS(1)设定CPU核数)。 实际中运行的CPU核数未必会和实际物理CPU数相吻合。
每个goroutine都会被一个特定的P(某个CPU)选定维护,而M(物理计算资源)每次挑选一个有效P,然后执行P中的goroutine。
每个P会将自己所维护的goroutine放到一个G队列中,其中就包括了goroutine堆栈信息,是否可执行信息等等。
默认情况下,P的数量与实际物理CPU的数量相等。当我们通过循环来创建goroutine时,goroutine会被分配到不同的G队列中。 而M的数量又不是唯一的,当M随机挑选P时,也就等同随机挑选了goroutine。
所以,当我们碰到多个goroutine的执行顺序不是我们想象的顺序时就可以理解了,因为goroutine进入P管理的队列G是带有随机性的。
P的数量由runtime.GOMAXPROCS(1)所设定,通常来说它是和内核数对应,例如在4Core的服务器上会启动4个线程。G会有很多个,每个P会将goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。
runtime.NumCPU() // 返回当前CPU内核数
runtime.GOMAXPROCS(2) // 设置运行时最大可执行CPU数
runtime.NumGoroutine() // 当前正在运行的goroutine 数
P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出一个goroutine执行。
假如有两个M,即两个OS Thread线程,分别对应一个P,每一个P调度一个G队列。如此一来,就组成的goroutine运行时的基本结构:
- 当有一个M返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS Thread线程那里窃取一个P过来,如果没有拿到,它就把goroutine放在一个global runqueue里,然后自己进入线程缓存里。
- 如果某个P所分配的任务G很快就执行完了,这会导致多个队列存在不平衡,会从其他队列中截取一部分goroutine到P上进行调度。一般来说,如果P从其他的P那里要取任务的话,一般就取run queue的一半,这就确保了每个OS线程都能充分的使用。
- 当一个OS Thread线程被阻塞时,P可以转而投奔另一个OS线程。
我们可以运行下面代码体验下Go语言中通过设定runtime.GOMAXPROCS(2) ,也即手动指定CPU运行的核数,来体验多核CPU在并发处理时的威力。不得不提,递归函数的计算很费CPU和内存,运行时可以根据电脑配置修改循环或递归数量。
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
var quit chan int = make(chan int)
func loop() {
for i := 0; i < 1000; i++ {
Factorial(uint64(1000))
}
quit <- 1
}
func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
var wg1, wg2 sync.WaitGroup
func main() {
fmt.Println("1:", time.Now())
fmt.Println(runtime.NumCPU()) // 默认CPU核数
a := 5000
for i := 1; i <= a; i++ {
wg1.Add(1)
go loop()
}
for i := 0; i < a; i++ {
select {
case <-quit:
wg1.Done()
}
}
fmt.Println("2:", time.Now())
wg1.Wait()
fmt.Println("3:", time.Now())
runtime.GOMAXPROCS(2) // 设置执行使用的核数
a = 5000
for i := 1; i <= a; i++ {
wg2.Add(1)
go loop()
}
for i := 0; i < a; i++ {
select {
case <-quit:
wg2.Done()
}
}
fmt.Println("4:", time.Now())
wg2.Wait()
fmt.Println("5:", time.Now())
}
输出:
1: 2024-04-06 17:30:53.7871419 +0800 CST m=+0.003283801
4
2: 2024-04-06 17:30:56.161056 +0800 CST m=+2.377197901
3: 2024-04-06 17:30:56.161056 +0800 CST m=+2.377197901
4: 2024-04-06 17:31:00.6790079 +0800 CST m=+6.895149801
5: 2024-04-06 17:31:00.6790079 +0800 CST m=+6.895149801
我的测试电脑CPU默认是4核,对比手动设置CPU在2核时的运行耗时,4核耗时约8秒,2核约14秒,当然这是一种比较理想化的测试,因为阶乘很快导致unit64为0,所以这个测试并不严谨,但从中我们仍然可以体验到Go语言在处理并发(cpu)时代码之简单,控制之方便。
在实际中运行速度延缓可能不一定仅仅是由于CPU的竞争,可能还有内存或者I/O的原因导致的,我们需要根据情况仔细分析。
最后,runtime.Gosched()用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
goroutine
在Go语言中,协程(goroutine)的使用很简单,直接在函数(代码块)前加上关键字 go 即可。go关键字就是用来创建一个协程(goroutine)的,后面的代码块就是这个协程(goroutine)需要执行的代码逻辑。
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; i < 10; i++ {
go func(i int) {
fmt.Println(i)
}(i)
}
// 暂停一会,保证打印全部结束
time.Sleep(1e9)
}
time.Sleep(1e9)让主程序不会马上退出,以便让协程(goroutine)运行完成,避免主程序退出时协程(goroutine)未处理完成甚至没有开始运行。
有关于协程(goroutine)之间的通信以及协程(goroutine)与主线程的控制以及多个协程(goroutine)的管理和控制,我们后续通过channel、context以及锁来进一步说明。
22.通道(channel)
Go 奉行通过通信来共享内存,而不是共享内存来通信。所以,channel 是goroutine之间互相通信的通道,goroutine之间可以通过它发消息和接收消息。
channel是进程内的通信方式,因此通过channel传递对象的过程和调用函数时的参数传递行为比较一致,比如也可以传递指针等。
channel是类型相关的,一个channel只能传递(发送或接受 | send or receive)一种类型的值,这个类型需要在声明channel时指定。
默认的,信道的存消息和取消息都是阻塞的 (叫做无缓冲的信道)。使用make来建立一个通道:var channel chan int = make(chan int)
或者 channel := make(chan int)
Go中channel可以是发送(send)、接收(receive)、同时发送(send)和接收(receive)。
// 定义接收的channel
receive_only := make (<-chan int)
// 定义发送的channel
send_only := make (chan<- int)
// 可同时发送接收
send_receive := make (chan int)
- chan<- 表示数据进入通道,要把数据写进通道,对于调用者就是发送。
- <-chan 表示数据从通道出来,对于调用者就是得到通道的数据,当然就是接收。
定义只发送或只接收的channel意义不大,一般用于在参数传递中:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int) // 不使用带缓冲区的channel
go send(c)
go receive(c)
time.Sleep(2 * time.Second)
close(c)
}
// 只能向chan里send数据
func send(c chan<- int) {
for i := 0; i < 10; i++ {
fmt.Println("send ready ", i)
c <- i
fmt.Println("send ", i)
}
}
// 只能接收channel中的数据
func receive(c <-chan int) {
for i := range c {
fmt.Println("received ", i)
}
}
运行结果上我们可以发现一个现象,往channel 发送数据后,这个数据如果没有取走,channel是阻塞的,也就是不能继续向channel 里面发送数据。因为上面代码中,我们没有指定channel 缓冲区的大小,默认是阻塞的。
我们可以建立带缓冲区的 channel:c := make(chan int, 1024)
我们把前面的程序修改下:
package main
import (
"fmt"
"time"
)
func main() {
c := make(chan int, 10) // 使用带缓冲区的channel
go send(c)
go recv(c)
time.Sleep(1 * time.Second)
close(c)
}
// 只能向chan里send发送数据
func send(c chan<- int) {
for i := 0; i < 10; i++ {
fmt.Println("send ready ", i)
c <- i
fmt.Println("send ", i)
}
}
// 只能接收channel中的数据
func recv(c <-chan int) {
for i := range c {
fmt.Println("received ", i)
}
}
从运行结果我们可以看到(每次执行顺序不一定相同,goroutine 运行导致的原因),带有缓冲区的channel,在缓冲区有数据而未填满前,读取不会出现阻塞的情况。
无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。
这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的 goroutine 阻塞等待。
这种对通道进行发送和接收的交互行为本身就是同步的。
有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。
这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。
这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。
如果给定了一个缓冲区容量,通道就是异步的。只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
可以通过内置的close函数来关闭channel实现。
- channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
- 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
- 关闭channel后,可以继续向channel接收数据,不能继续发送数据;
- 对于nil channel,无论收发都会被阻塞。
23.同步与锁
同步锁
Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者是读写锁。
互斥锁是传统的并发程序对共享资源进行访问控制的主要手段,在Go中,似乎更推崇由channel来实现资源共享和通信。它由标准库代码包sync中的Mutex结构体类型代表。只有两个公开方法:调用Lock()获得锁,调用unlock()释放锁。
- 使用Lock()加锁后,不能再继续对其加锁(同一个goroutine中,即:同步调用),否则会panic。只有在unlock()之后才能再次Lock()。异步调用Lock(),是正当的锁竞争,当然不会有panic了。适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁也叫做全局锁。
- func (m *Mutex) Unlock()用于解锁m,如果在使用Unlock()前未加锁,就会引起一个运行错误。已经锁定的Mutex并不与特定的goroutine相关联,这样可以利用一个goroutine对其加锁,再利用其他goroutine对其解锁。
建议:同一个互斥锁的成对锁定和解锁操作放在同一层次的代码块中。 使用锁的经典模式:
var lck sync.Mutex
func foo() {
lck.Lock()
defer lck.Unlock()
// ...
}
lck.Lock()会阻塞直到获取锁,然后利用defer语句在函数返回时自动释放锁。
下面代码通过3个goroutine来体现sync.Mutex 对资源的访问控制特征:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
wg := sync.WaitGroup{}
wg.Add(3)
defer wg.Wait()
var mutex sync.Mutex
fmt.Println("Locking (G0)")
mutex.Lock()
defer mutex.Unlock()
fmt.Println("locked (G0)")
for i := 1; i < 4; i++ {
go func(i int) {
fmt.Println("-----------------")
fmt.Printf("Locking (G%d)\n", i)
mutex.Lock()
fmt.Printf("locked (G%d)\n", i)
time.Sleep(time.Second * 2)
mutex.Unlock()
fmt.Printf("unlocked (G%d)\n", i)
wg.Done()
fmt.Println("++++++++++++++")
}(i)
}
time.Sleep(time.Second * 2)
fmt.Println("---ready unlock (G0)")
fmt.Println("----unlocked (G0)")
}
输出:
Locking (G0)
locked (G0)
-----------------
Locking (G3)
-----------------
Locking (G1)
-----------------
Locking (G2)
---ready unlock (G0)
----unlocked (G0)
locked (G3)
unlocked (G3)
++++++++++++++
locked (G1)
unlocked (G1)
locked (G2)
++++++++++++++
unlocked (G2)
++++++++++++++
通过程序执行结果我们可以看到,当有锁释放时,才能进行lock动作,G0锁释放时,才有后续锁释放的可能,这里是G1抢到释放机会。
Mutex也可以作为struct的一部分,这样这个struct就会防止被多线程更改数据。
package main
import (
"fmt"
"sync"
"time"
)
type Book struct {
BookName string
Lock *sync.Mutex
}
func (bk *Book) SetName(wg *sync.WaitGroup, name string) {
defer func() {
fmt.Println("Unlock set name:", name)
bk.Lock.Unlock()
wg.Done()
}()
bk.Lock.Lock()
fmt.Println("Lock set name:", name)
time.Sleep(1 * time.Second)
bk.BookName = name
}
func main() {
wg := &sync.WaitGroup{}
wg.Add(3)
defer wg.Wait()
var bk Book
bk.Lock = &sync.Mutex{}
books := []string{"《三国演义》", "《道德经》", "《西游记》"}
for _, book := range books {
go bk.SetName(wg, book)
}
}
输出:
Lock set name: 《三国演义》
Unlock set name: 《三国演义》
Lock set name: 《西游记》
Unlock set name: 《西游记》
Lock set name: 《道德经》
Unlock set name: 《道德经》
读写锁
读写锁是分别针对读操作和写操作进行锁定和解锁操作的互斥锁。在Go语言中,读写锁由结构体类型sync.RWMutex代表。
基本遵循原则:
- 写锁定情况下,对读写锁进行读锁定或者写锁定,都将阻塞;而且读锁与写锁之间是互斥的;
- 读锁定情况下,对读写锁进行写锁定,将阻塞;加读锁时不会阻塞;
- 对未被写锁定的读写锁进行写解锁,会引发Panic;
- 对未被读锁定的读写锁进行读解锁的时候也会引发Panic;
- 写解锁在进行的同时会试图唤醒所有因进行读锁定而被阻塞的goroutine;
- 读解锁在进行的时候则会试图唤醒一个因进行写锁定而被阻塞的goroutine。
与互斥锁类似,sync.RWMutex类型的零值就已经是立即可用的读写锁了。在此类型的方法集合中包含了两对方法,即:
RWMutex提供四个方法:
func (*RWMutex) Lock // 写锁定
func (*RWMutex) Unlock // 写解锁
func (*RWMutex) RLock // 读锁定
func (*RWMutex) RUnlock // 读解锁
package main
import (
"fmt"
"sync"
"time"
)
var m *sync.RWMutex
func main() {
wg := sync.WaitGroup{}
wg.Add(20)
var rwMutex sync.RWMutex
Data := 0
for i := 0; i < 10; i++ {
go func(t int) {
rwMutex.RLock()
defer rwMutex.RUnlock()
fmt.Printf("Read data: %v\n", Data)
wg.Done()
time.Sleep(1 * time.Second)
// 这句代码第一次运行后,读解锁。
// 循环到第二个时,读锁定后,这个goroutine就没有阻塞,同时读成功。
}(i)
go func(t int) {
rwMutex.Lock()
defer rwMutex.Unlock()
Data += t
fmt.Printf("Write Data: %v %d \n", Data, t)
wg.Done()
// 这句代码让写锁的效果显示出来,写锁定下是需要解锁后才能写的。
time.Sleep(1 * time.Second)
}(i)
}
time.Sleep(3 * time.Second)
wg.Wait()
}
sync.WaitGroup
前面例子中我们有使用WaitGroup,它用于线程同步,WaitGroup等待一组线程集合完成,才会继续向下执行。 主线程(goroutine)调用Add来设置等待的线程(goroutine)数量。 然后每个线程(goroutine)运行,并在完成后调用Done。 同时,Wait用来阻塞,直到所有线程(goroutine)完成才会向下执行。Add(-1)和Done()效果一致。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(t int) {
defer wg.Done()
fmt.Println(t)
}(i)
}
wg.Wait()
}
输出:
9
4
0
1
2
3
6
5
7
8
sync.Once
sync.Once.Do(f func())能保证once只执行一次,这个sync.Once块只会执行一次。
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var once sync.Once
func main() {
arr := make([]string, 10)
for i := 0; i < 10; i++ {
once.Do(onces)
go func(i int) {
arr[i] = strconv.Itoa(i)
}(i)
}
fmt.Println("arr:", arr)
time.Sleep(4000)
}
func onces() {
fmt.Println("onces")
}
输出:
onces
arr: [0 1 2 3 4 5 6 7 8 9]
sync.Map
随着Go1.9的发布,有了一个新的特性,那就是sync.map,它是原生支持并发安全的map。虽然说普通map并不是线程安全(或者说并发安全),但一般情况下我们还是使用它,因为这足够了;只有在涉及到线程安全,再考虑sync.map。
但由于sync.Map的读取并不是类型安全的,所以我们在使用Load读取数据的时候我们需要做类型转换。
sync.Map的使用上和map有较大差异,详情见代码。
package main
import (
"fmt"
"sync"
)
func main() {
var m sync.Map
//Store
m.Store("name", "Joe")
m.Store("gender", "Male")
//LoadOrStore
//若key不存在,则存入key和value,返回false和输入的value
v, ok := m.LoadOrStore("name1", "Jim")
fmt.Println(ok, v) //false Jim
fmt.Println(m.Load("name1")) //Jim true
//若key已存在,则返回true和key对应的value,不会修改原来的value
v, ok = m.LoadOrStore("name", "aaa")
fmt.Println(ok, v) //true Joe
//Load
v, ok = m.Load("name")
if ok {
fmt.Println("key存在,值是: ", v)
} else {
fmt.Println("key不存在")
}
//Range
//遍历sync.Map
f := func(k, v interface{}) bool {
fmt.Println(k, v)
return true
}
m.Range(f)
//Delete
m.Delete("name1")
fmt.Println(m.Load("name1"))
}
输出:
false Jim
true Joe
key存在,值是: Joe
name Joe
gender Male
name1 Jim
<nil> false
24.指针和内存
指针
一个指针变量可以指向任何一个值的内存地址。它指向那个值的内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上号(前缀)来获取指针所指向的内容,这里的号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
一个指针变量通常缩写为 ptr。
符号 “*” 可以放在一个指针前,如 “*intP”,那么它将得到这个指针指向地址上所存储的值;这被称为反引用(或者内容或者间接引用)操作符;另一种说法是指针转移。
对于任何一个变量 var, 如下表达式都是正确的:var == *(&var)
注意事项:
你不能得到一个数字或常量的地址,下面的写法是错误的。
package main
import "fmt"
func main() {
var i = 5
ptr := &i // 0xc00000a098
fmt.Println(ptr)
ptr2 := *ptr // 5
fmt.Println(ptr2)
}
所以说,Go 语言和 C、C++ 以及 D 语言这些低级(系统)语言一样,都有指针的概念。
但是对于经常导致 C 语言内存泄漏继而程序崩溃的指针运算(所谓的指针算法,如:pointer+2,移动指针指向字符串的字节数或数组的某个位置)是不被允许的。
Go 语言中的指针保证了内存安全,更像是 Java、C# 和 VB.NET 中的引用。
因此 c = *p++ 在 Go 语言的代码中是不合法的。
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
另一方面(虽然不太可能),由于一个指针导致的间接引用(一个进程执行了另一个地址),指针的过度频繁使用也会导致性能下降。
指针也可以指向另一个指针,并且可以进行任意深度的嵌套,导致你可以有多级的间接引用,但在大多数情况这会使你的代码结构不清晰。
如我们所见,在大多数情况下 Go 语言可以使程序员轻松创建指针,并且隐藏间接引用,如:自动反向引用。
对一个空指针的反向引用是不合法的,并且会使程序崩溃:
package main
func main() {
var p *int = nil
*p = 0
}
panic: runtime error: invalid memory address or nil pointer dereference
指针的使用方法:
- 定义指针变量;
- 为指针变量赋值;
- 访问指针变量中指向地址的值;
- 在指针类型前面加上*号来获取指针所指向的内容。
package main
import "fmt"
func main() {
var a, b = 20, 30 // 声明实际变量
var ptra *int // 声明指针变量
var ptrb = &b
ptra = &a // 指针变量的存储地址
fmt.Printf("a 变量的地址是: %x\n", &a) //a 变量的地址是: c00000a098
fmt.Printf("b 变量的地址是: %x\n", &b) //b 变量的地址是: c00000a0b0
// 指针变量的存储地址
fmt.Printf("ptra 变量的存储地址: %x\n", ptra)//ptra 变量的存储地址: c00000a098
fmt.Printf("ptrb 变量的存储地址: %x\n", ptrb)//ptrb 变量的存储地址: c00000a0b0
// 使用指针访问值
fmt.Printf("*ptra 变量的值: %d\n", *ptra)//*ptra 变量的值: 20
fmt.Printf("*ptrb 变量的值: %d\n", *ptrb)//*ptrb 变量的值: 30
}
new() 和 make() 的区别
看起来二者没有什么区别,都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
- new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型*T的内存地址:这种方法返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。
你并不总是知道变量是分配到栈还是堆上。在C++中,使用new创建的变量总是在堆上。在Go中,即使是使用 new() 或者 make() 函数来分配,变量的位置还是由编译器决定。编译器根据变量的大小和泄露分析的结果来决定其位置。这也意味着在局部变量上返回引用是没问题的,而这在C或者C++这样的语言中是不行的。
如果你想知道变量分配的位置,在"go build"或"go run"上传入"-m" “-gcflags”(即,go run -gcflags -m app.go)。
垃圾回收和 SetFinalizer
Go 语言开发者一般不需要写代码来释放不再使用的变量或结构体占用的内存,在 Go语言运行时有一个独立的进程,即垃圾收集器(GC),会专门处理这些事情,它搜索不再使用的变量然后释放它们占用的内存,这是自动垃圾回收;还有一种是主动垃圾回收,通过显式调用 runtime.GC()来实现。
通过调用 runtime.GC() 函数可以显式的触发 GC,这在某些的场景下非常有用,比如当内存资源不足时调用 runtime.GC(),它会在此函数执行的点上立即释放一大片内存,但此时程序可能会有短时的性能下降(因为 GC 进程在执行)。
下面代码中的func (p *Person) NewOpen()在某些情况下非常有必要这样处理,比如某些资源占用申请,开发人员可能忘记使用defer Close()来销毁处理,但通过SetFinalizer,如果GC自动运行或者手动运行GC,则都能及时销毁这些资源,释放占用的内存而避免内存泄漏。
GC过程中重要的函数func SetFinalizer(obj interface{}, finalizer interface{})有两个参数,参数一:obj必须是指针类型。参数二:finalizer是一个函数,其参数类型是obj的类型,其没有返回值。
package main
import (
"log"
"runtime"
"time"
)
type Person struct {
Name string
Age int
}
func (p *Person) Close() {
p.Name = "NewName"
log.Println(p)
log.Println("Close")
}
func (p *Person) NewOpen() {
log.Println("Init")
runtime.SetFinalizer(p, (*Person).Close)
}
func Tt(p *Person) {
p.Name = "NewName"
log.Println(p)
log.Println("Tt")
}
// Mem 查看内存情况
func Mem(m *runtime.MemStats) {
runtime.ReadMemStats(m)
log.Printf("%d Kb\n", m.Alloc/1024)
}
func main() {
var m runtime.MemStats
Mem(&m)
var p = &Person{Name: "lee", Age: 4}
p.NewOpen()
log.Println("Gc完成第一次")
log.Println("p:", p)
runtime.GC()
time.Sleep(time.Second * 2)
Mem(&m)
var p1 = &Person{Name: "Goo", Age: 9}
runtime.SetFinalizer(p1, Tt)
log.Println("Gc完成第二次")
time.Sleep(time.Second * 2)
runtime.GC()
time.Sleep(time.Second * 2)
Mem(&m)
}
输出:
2024/04/09 13:33:14 75 Kb
2024/04/09 13:33:14 Init
2024/04/09 13:33:14 Gc完成第一次
2024/04/09 13:33:14 p: &{lee 4}
2024/04/09 13:33:14 &{NewName 4}
2024/04/09 13:33:14 Close
2024/04/09 13:33:16 80 Kb
2024/04/09 13:33:16 Gc完成第二次
2024/04/09 13:33:18 &{NewName 9}
2024/04/09 13:33:18 Tt
2024/04/09 13:33:20 80 Kb
25.面向对象
Go 中的面向对象
我们总结一下前面看到的:Go 没有类,而是松耦合的类型、方法对接口的实现。
OO 语言最重要的三个方面分别是:封装,继承和多态,在 Go 中它们是怎样表现的呢?
Go实现面向对象的两个关键是struct和interface,结构代替类,因为Go语言不提供类,但提供了结构体或自定义类型,方法可以被添加到结构体或自定义类型中。结构体之间可以嵌套,类似继承。而interface定义接口,实现多态性。
封装:
和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:
1)包范围内的:通过标识符首字母小写,对象 只在它所在的包内可见
2)可导出的:通过标识符首字母大写,对象对所在包以外也可见类型只拥有自己所在包中定义的方法。
继承:
Go没有显式的继承,而是通过组合实现继承,内嵌一个(或多个)包含想要的行为(字段和方法)的结构体;多重继承可以通过内嵌多个结构体实现。
多态:
多态是运行时特性,而继承则是编译时特征,也就是说,继承关系在编译时就已经确定了,而多态则可以实现运行时的动态绑定。Go用接口实现多态,某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且:接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。
另外Go没有构造函数,如果一定要在初始化对象的时候进行一些工作的话,可以自行封装产生实例的方法。实例化的时候可以初始化属性值,如果没有指明则默认为系统默认值。加&符号和new的是指针对象,没有的则是值对象,在传递对象的时候要根据实际情况来决定是要传递指针还是值。
多重继承
多重继承指的是类型获得多个父类型行为的能力,它在传统的面向对象语言中通常是不被实现的(C++ 和 Python 例外)。因为在类继承层次中,多重继承会给编译器引入额外的复杂度。但是在 Go 语言中,通过在类型中嵌入所有必要的父类型,可以很简单的实现多重继承。
有关方法重载就是一个类中可以有相同的函数名称,但是它们的参数是不一致的,在java、C++中这种做法普遍存在。Go中如果尝试这么做会报重新声明(redeclared)错误,但是Go的函数可以声明不定参数,这个非常强大。
func Println(a ...interface{}) (n int, err error) {
return Fprintln(os.Stdout, a...)
}
其中a …interface{}表示参数不定的意思。如果要根据不同的参数实现不同的功能,要在方法内检测传递的参数。
26.测试
在Go语言中,所有的包都应该有必要文档和注释,当然同样甚至更为重要的是对包进行必要的测试。
testing 包就是这样一个标准包,被专门用来进行单元测试以及进行自动化测试,打印日志和错误报告,方便程序员调试代码,并且还包含一些基准测试函数的功能。
testing 包含测试函数、测试辅助代码和示例函数;测试函数包括Test开头的单元测试函数和以Benchmark开头的基准测试函数两种,测试辅助代码是为测试函数服务的公共函数、初始化函数、测试数据等,而示例函数则是以Example开头的说明被测试函数用法的函数,而示例函数通常被保存在example_*_test.go文件中。
单元测试
开发中经常需要对一个包做(单元)测试,写一些可以频繁(每次更新后)执行的小块测试单元来检查代码的正确性,于是我们必须写一些 Go 源文件来测试代码。
使用testing包,我们只需要遵守简单的规则,就可以很好地写出通用的测试程序。因为其他开发人员也会遵循这个包的规则来进行测试。
首先测试程序是独立的文件,他必须属于被测试的包,和这个包的其他程序放在一起,并且文件名满足这种形式 *_test.go。由于是独立的测试文件,所以测试代码和包中的业务代码是分开的。Go语言这样规定的好处是不言而喻的,因为在其他语言开发的程序中,我们经常可以看到代码中注释掉的测试代码,而且有把开发版作为生产版发布到线上导致异常的问题出现。
当然,好的规则需要我们遵守并严格执行。
_test 程序不会被普通的 Go 编译器编译,所以当放应用部署到生产环境时它们不会被部署;只有 Gotest 会编译所有的程序:普通程序和测试程序。
测试文件中必须导入 “testing” 包,测试函数名字是以 TestXxx 打头的全局函数,Xxx部分可以为任意的字母数字的组合,但是首字母不能是小写字母[a-z],函数名我们可以以被测试函数的字母描述,如 TestFmtInterface,TestPayEmployees 等。测试用例会按照测试源代码中写的顺序依次执行。
测试函数一般都要求这种形式的头部:func TestAbcde(t *testing.T)
*testing.T是传给测试函数的结构类型,用来管理测试状态,支持格式化测试日志,如 t.Log,t.Error,t.ErrorF 等。t.Log函数就像我们常常使用的fmt.Println一样,可以接受多个参数,方便输出调试结果。
用下面这些函数来通知测试失败:
1)func (t *T) Fail() 标记测试函数为失败,然后继续执行剩下的测试。
2)func (t *T) FailNow() 标记测试函数为失败并中止执行;文件中别的测试也被略过,继续执行下一个文件。
3)func (t *T) Log(args …interface{}) args 被用默认的格式格式化并打印到错误日志中。
4)func (t *T) Fatal(args …interface{}) 结合 先执行 3),然后执行 2)的效果。
运行 Go test 来编译测试程序,并执行程序中所有的 TestXxx 函数。如果所有的测试都通过会打印出 PASS。
当然,对于包中不能导出的函数不能进行单元或者基准测试。
Gotest 可以接收一个或多个函数程序作为参数,并指定一些选项。
在系统标准包中,有很多 _test.go 结尾的程序,大家可以用来测试,为节约篇幅这里我就不写具体例子了。
基准测试
testing 包中有一些类型和函数可以用来做简单的基准测试;测试代码中必须包含以 BenchmarkZzz 打头的函数并接收一个 *testing.B 类型的参数,比如:
func BenchmarkReverse(b *testing.B) {
...
}
命令 Go test –test.bench=.* 会运行所有的基准测试函数;代码中的函数会被调用 N 次(N是非常大的数,如 N = 1000000),可以根据情况指定b.Z的值,并展示 N 的值和函数执行的平均时间,单位为 ns(纳秒,ns/op)。如果是用 testing.Benchmark 调用这些函数,直接运行程序即可。
下面我们看一个测试的具体例子:
package even
func Loop(n uint64) (result uint64) {
result = 1
var i uint64 = 1
for ; i <= n; i++ {
result *= i
}
return result
}
func Factorial(n uint64) (result uint64) {
if n > 0 {
result = n * Factorial(n-1)
return result
}
return 1
}
在 even 包的路径下,我们创建一个名为 even_test.go 的测试程序:
package even
import (
"testing"
)
func TestLoop(t *testing.T) {
t.Log("Loop:", Loop(uint64(32)))
}
func TestFactorial(t *testing.T) {
t.Log("Factorial:", Factorial(uint64(32)))
}
func BenchmarkLoop(b *testing.B) {
for i := 0; i < b.N; i++ {
Loop(uint64(40))
}
}
func BenchmarkFactorial(b *testing.B) {
for i := 0; i < b.N; i++ {
Factorial(uint64(40))
}
}
现在我们可以在这个包的目录下使用命令:gobench even来测试 even 包。
输出:
API server listening at: 127.0.0.1:49238
goos: windows
goarch: amd64
pkg: calc/even
cpu: Intel(R) Core(TM) i3-9100F CPU @ 3.60GHz
BenchmarkLoop
BenchmarkLoop-4 12335626 92.87 ns/op
BenchmarkFactorial
BenchmarkFactorial-4 15297283 79.15 ns/op
PASS
递归函数的确是很耗费系统资源,而且运行也慢,不建议使用。
27.反射(reflect)
反射是应用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。每种语言的反射模型都不同,并且有些语言根本不支持反射。Go语言实现了反射,反射机制就是在运行时动态调用对象的方法和属性,标准库reflect提供了相关的功能。在reflect包中,通过reflect.TypeOf(),reflect.ValueOf()分别从类型、值的角度来描述一个Go对象。
func TypeOf(i interface{}) Type
type Type interface
func ValueOf(i interface{}) Value
type Value struct
在Go语言的实现中,一个interface类型的变量存储了2个信息, 一个<值,类型>对, :(value, type)
value是实际变量值,type是实际变量的类型。两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。
例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64,reflect.ValueOf(x) 返回 3.4。实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:
func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value
reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。
Type主要有: Kind() 将返回一个常量,表示具体类型的底层类型 Elem()方法返回指针、数组、切片、map、通道的基类型,这个方法要慎用,如果用在其他类型上面会出现panic
Value主要有: Type() 将返回具体类型所对应的 reflect.Type(静态类型) Kind() 将返回一个常量,表示具体类型的底层类型
反射可以在运行时检查类型和变量,例如它的大小、方法和 动态 的调用这些方法。这对于没有源代码的包尤其有用。
由于反射是一个强大的工具,但反射对性能有一定的影响,除非有必要,否则应当避免使用或小心使用。下面代码针对int、数组以及结构体分别使用反射机制,其中的差异请看注释。
package main
import (
"fmt"
"reflect"
)
type Student struct {
name string
}
func main() {
var a = 50
v := reflect.ValueOf(a) // 返回Value类型对象,值为50
t := reflect.TypeOf(a) // 返回Type类型对象,值为int
fmt.Println(v, t, v.Type(), t.Kind()) //50 int int int
var b = [5]int{5, 6, 7, 8}
fmt.Println(reflect.TypeOf(b)) //[5]int
fmt.Println(reflect.TypeOf(b).Kind()) //array
fmt.Println(reflect.TypeOf(b).Elem()) //int
var s Student
p := reflect.ValueOf(s) // 使用ValueOf()获取到结构体的Value对象
fmt.Println(p.Type()) // 输出:main.Student
fmt.Println(p.Kind()) // 输出:struct
}
在Go语言中,类型包括 static type和concrete type. 简单说 static type是你在编码是看见的类型(如int、string),concrete type是实际的类型,runtime系统看见的类型。
Type()返回的是静态类型,而kind()返回的是concrete type。上面代码中,在int,数组以及结构体三种类型情况中,可以看到kind(),type()返回值的差异。
通过反射可以修改原对象
d.CanAddr()方法:判断它是否可被取地址 d.CanSet()方法:判断它是否可被取地址并可被修改
通过一个settable的Value反射对象来访问、修改其对应的变量值:
package main
import (
"fmt"
"reflect"
)
type Student struct {
name string //小写字母的属性值不能修改 可以修改为Name
Age int
}
func main() {
var a = 50
v := reflect.ValueOf(a) // 返回Value类型对象,值为50
t := reflect.TypeOf(a) // 返回Type类型对象,值为int
fmt.Println(v) //50
fmt.Println(t) //int
fmt.Println(t.Kind()) //int
fmt.Println(reflect.ValueOf(&a).Elem()) //50
set := reflect.ValueOf(&a).Elem() // 这样才能让set保存a的值
fmt.Println(set, set.CanSet()) //50 true
set.SetInt(1000)
fmt.Println(set) //1000
fmt.Println(a) //1000
var b = [5]int{5, 6, 7, 8}
fmt.Println(reflect.TypeOf(b)) //[5]int
fmt.Println(reflect.TypeOf(b).Kind()) //array
fmt.Println(reflect.TypeOf(b).Elem()) //int
var s = Student{"joke", 18}
p := reflect.ValueOf(s) // 使用ValueOf()获取到结构体的Value对象
fmt.Println(p.Type()) // 输出:Student
fmt.Println(p.Kind()) // 输出:struct
setStudent := reflect.ValueOf(&s).Elem()
//小写字母的属性值不能被导出 未导出字段,不能修改,panic会发生
//setStudent.Field(0).SetString("Mike")
setStudent.Field(1).SetInt(19)
fmt.Println(setStudent) //{joke 19}
}
虽然反射可以越过Go语言的导出规则的限制读取结构体中未导出的成员,但不能修改这些未导出的成员。因为一个struct中只有被导出的字段才是settable的。
在结构体中有tag标签,通过反射可获取结构体成员变量的tag信息。
package main
import (
"fmt"
"reflect"
)
type Student struct {
name string `desc:"名称" data:"数据"` //标签的分隔符是空格
Age int `json:"years"`
}
func main() {
var s = Student{"joke", 18}
setStudent := reflect.ValueOf(&s).Elem()
sSAge, _ := setStudent.Type().FieldByName("Age")
fmt.Println(sSAge.Tag.Get("json")) // years
sName, _ := setStudent.Type().FieldByName("name")
fmt.Println(sName.Tag.Get("desc")) //名称
fmt.Println(sName.Tag.Get("data")) //数据
}
反射结构体
为了完整说明反射的情况,通过反射一个结构体类型,综合来说明。下面例子较为系统地利用一个结构体,来充分举例说明反射:
package main
import (
"fmt"
"reflect"
)
// 结构体
type S struct {
int
string
bool
float64
}
func (s S) Method1(i int) string {
return "结构体方法1"
}
func (s *S) Method2(i int) string {
return "结构体指针方法2"
}
var structValue = S{20, "结构体", false, 64.0}
// 复杂类型
var complexTypes = []interface{}{
structValue,
&structValue, // 结构体
structValue.Method1,
structValue.Method2, // 方法
}
func main() {
// 测试复杂类型
for i := 0; i < len(complexTypes); i++ {
v := reflect.ValueOf(complexTypes[i])
PrintValue(v)
}
}
func PrintValue(v reflect.Value) {
fmt.Println("--------------------")
// ----- 通用方法 -----
fmt.Println("String :", v.String()) // 反射值的字符串形式
fmt.Println("Type :", v.Type()) // 反射值的类型
fmt.Println("Kind :", v.Kind()) // 反射值的类别
fmt.Println("CanAddr :", v.CanAddr()) // 是否可以获取地址
fmt.Println("CanSet :", v.CanSet()) // 是否可以修改
if v.CanAddr() {
fmt.Println("Addr: ", v.Addr()) // 获取地址
fmt.Println("UnsafeAddr :", v.UnsafeAddr()) // 获取自由地址
}
// 获取方法数量
fmt.Println("NumMethod :", v.NumMethod())
if v.NumMethod() > 0 {
// 遍历方法
i := 0
for ; i < v.NumMethod()-1; i++ {
fmt.Printf(" ┣ %v\n", v.Method(i).String())
}
fmt.Printf(" ┗ %v\n", v.Method(i).String())
// 通过名称获取方法
fmt.Println("MethodByName:", v.MethodByName("String").String())
}
switch v.Kind() {
// 结构体:
case reflect.Struct:
fmt.Println("=== 结构体 ===")
// 获取字段个数
fmt.Println("NumField :", v.NumField())
if v.NumField() > 0 {
var i int
// 遍历结构体字段
for i = 0; i < v.NumField()-1; i++ {
field := v.Field(i) // 获取结构体字段
fmt.Printf(" ├ %-8v %v\n", field.Type(), field.String())
}
field := v.Field(i) // 获取结构体字段
fmt.Printf(" └ %-8v %v\n", field.Type(), field.String())
// 通过名称查找字段
if v := v.FieldByName("ptr"); v.IsValid() {
fmt.Println("FieldByName(ptr) :", v.Type().Name())
}
// 通过函数查找字段
v := v.FieldByNameFunc(func(s string) bool { return len(s) > 3 })
if v.IsValid() {
fmt.Println("FieldByNameFunc :", v.Type().Name())
}
}
case reflect.Func:
fmt.Println("=== 方法 ===")
fmt.Println("Method :", &v)
case reflect.Ptr:
fmt.Println("=== 指针 ===")
fmt.Println("pointerValue :", &v)
default:
break
}
}
输出:
--------------------
String : <main.S Value>
Type : main.S
Kind : struct
CanAddr : false
CanSet : false
NumMethod : 1
┗ <func(int) string Value>
MethodByName: <invalid Value>
=== 结构体 ===
NumField : 4
├ int <int Value>
├ string 结构体
├ bool <bool Value>
└ float64 <float64 Value>
--------------------
String : <*main.S Value>
Type : *main.S
Kind : ptr
CanAddr : false
CanSet : false
NumMethod : 2
┣ <func(int) string Value>
┗ <func(int) string Value>
MethodByName: <invalid Value>
=== 指针 ===
pointerValue : <*main.S Value>
--------------------
String : <func(int) string Value>
Type : func(int) string
Kind : func
CanAddr : false
CanSet : false
NumMethod : 0
=== 方法 ===
Method : <func(int) string Value>
--------------------
String : <func(int) string Value>
Type : func(int) string
Kind : func
CanAddr : false
CanSet : false
NumMethod : 0
=== 方法 ===
Method : <func(int) string Value>
28.排序(sort)
sort包介绍
Go语言标准库sort包中实现了几种基本的排序算法:插入排序、快排和堆排序,但在使用sort包进行排序时无需具体考虑使用那种排序方式。
func insertionSort(data Interface, a, b int)
func heapSort(data Interface, a, b int)
func quickSort(data Interface, a, b, maxDepth int)
sort.Interface接口定义了三个方法,注意sort包中接口Interface这个名字,是大写字母I开头,不要和interface关键字混淆,这里就是一个接口名而已。
type Interface interface {
// Len 为集合内元素的总数
Len() int
// 如果index为i的元素小于index为j的元素,则返回true,否则false
Less(i, j int) bool
// Swap 交换索引为 i 和 j 的元素
Swap(i, j int)
}
这三个方法分别是:获取数据集合长度的Len()方法、比较两个元素大小的Less()方法和交换两个元素位置的Swap()方法。只要实现了这三个方法,就可以对数据集合进行排序,sort包会根据实际数据自动选择高效的排序算法。
sort包原生支持[]int、[]float64和[]string三种内建数据类型切片的排序操作,即不必我们自己实现相关的Len()、Less()和Swap()方法。
以[]int为例,我们看看在sort包中的是怎么定义排序操作的:
type IntSlice []int
先通过 []int 来定义新类型IntSlice,然后在IntSlice上定义三个方法,Len(),Less(i, j int),Swap(i, j int),实现了这三个方法也就意味着实现了sort.Interface。
方法 func (p IntSlice) Sort() 通过调用 sort.Sort§ 函数来实现排序。而p因为是sort.Interface类型,但IntSlice实现了这三个接口方法,也是sort.Interface类型,因此可以直接调用得到排序结果。其他[]float64和[]string的排序也基本上按照这种方式来实现。
其他类型并没有在标准包中给出实现方法,需要我们自己来定义实现。下面第二节 自定义sort.Interface排序 就是专门来讲怎么实现的,但有了这三个实现的实例,自定义实现排序也就很容易了。
func (p IntSlice) Len() int { return len(p) }
func (p IntSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p IntSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p IntSlice) Sort() { Sort(p) }
来看看[]int,[]string排序的实例:
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{3, 5, 4, -1, 9, 11, -14}
sort.Ints(a)
fmt.Println(a)//[-14 -1 3 4 5 9 11]
sort.Sort(sort.Reverse(sort.IntSlice(a)))
fmt.Printf("After reverse: %v\n", a)//After reverse: [11 9 5 4 3 -1 -14]
ss := []string{"surface", "ipad", "mac pro", "mac air", "think pad", "idea pad"}
sort.Strings(ss)
fmt.Println(ss)
//[idea pad ipad mac air mac pro surface think pad]
sort.Sort(sort.Reverse(sort.StringSlice(ss)))
fmt.Printf("After reverse: %v\n", ss)
//After reverse: [think pad surface mac pro mac air ipad idea pad]
}
默认结果都是升序排列,如果我们想对一个 sortable object 进行逆序排序,可以自定义一个type。但 sort.Reverse 帮你省掉了这些代码。
package main
import (
"fmt"
"sort"
)
func main() {
a := []int{4, 3, 2, 1, 5, 9, 8, 7, 6}
sort.Sort(sort.Reverse(sort.IntSlice(a)))
fmt.Println("After reversed: ", a)
}
自定义sort.Interface排序
如果是具体的某个结构体的排序,就需要自己实现Interface了。数据集合(包括自定义数据类型的集合)排序需要实现sort.Interface接口的三个方法,即:Len(),Swap(i, j int),Less(i, j int),数据集合实现了这三个方法后,即可调用该包的Sort()方法进行排序。Sort(data Interface) 方法内部会使用quickSort()来进行集合的排序。quickSort()会根据实际情况来选择排序方法。
任何实现了 sort.Interface 的类型(一般为集合),均可使用该包中的方法进行排序。这些方法要求集合内列出元素的索引为整数。
package main
import (
"fmt"
"sort"
)
type person struct {
Name string
Age int
}
type personSlice []person
func (s personSlice) Len() int {
return len(s)
}
func (s personSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s personSlice) Less(i, j int) bool {
return s[i].Age < s[j].Age
}
func main() {
a := personSlice{
{Name: "AAA", Age: 55},
{Name: "BBB", Age: 22},
{Name: "CCC", Age: 0},
{Name: "DDD", Age: 22},
{Name: "EEE", Age: 11},
}
sort.Sort(a)
fmt.Println("Sort:", a)
sort.Stable(a)
fmt.Println("Stable:", a)
}
输出:
Sort: [{CCC 0} {EEE 11} {BBB 22} {DDD 22} {AAA 55}]
Stable: [{CCC 0} {EEE 11} {BBB 22} {DDD 22} {AAA 55}]
sort.Slice
利用sort.Slice 函数,而不用提供一个特定的 sort.Interface 的实现,而是 Less(i,j int) 作为一个比较回调函数,可以简单地传递给 sort.Slice 进行排序。这种方法一般不建议使用,因为在sort.Slice中使用了reflect。
package main
import (
"fmt"
"sort"
)
type Peak struct {
Name string
Elevation int
}
func main() {
peaks := []Peak{
{"Aconcagua", 22838},
{"Denali", 20322},
{"Kilimanjaro", 19341},
{"Mount Elbrus", 18510},
{"Mount Everest", 29029},
{"Mount Kosciuszko", 7310},
{"Mount Vinson", 16050},
{"Puncak Jaya", 16024},
}
sort.Slice(peaks, func(i, j int) bool {
return peaks[i].Elevation >= peaks[j].Elevation
})
fmt.Println(peaks)
}
输出:
[{Kosciua 12310}
{Puncaks 16024}
{Vinsons 16050}
{Elbrust 18510}
{Kiliman 19341}
{Denalis 20322}
{Aconcas 22838}
{Everest 29029}]