1. 初识
1.1 特性
- 自动垃圾回收——GC
- 更丰富的内置类型——map、slice
- 函数的多返回值——python
- 错误处理——defer、panic、recover
- 匿名函数和闭包
- 类型和接口
- 并发编程——goruntine
- 反射
- 语言交互性
1.2 命名规范
1.2.1 区分大小写的语言
命名规则涉及变量、常量、全局函数、结构、接口、方法等的命名。 Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
- 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Analysize,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
- 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )
1.2.2 包名称
保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。
package domain
package main
1.2.3 文件命名
尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。
approve_service.go
1.2.4 结构体命名
type MainConfig struct {
Port string `json:"port"`
Address string `json:"address"`
}
config := MainConfig{"1234", "123.221.134"}
1.2.5 接口命名
type Reader interface {
Read(p []byte) (n int, err error)
}
1.2.6 变量命名
和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:
- 如果变量为私有,且特有名词为首个单词,则使用小写,如 appService
- 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
1.2.7 常量命名
常量均需使用全部大写字母组成,并使用下划线分词
const APP_URL = "https://www.baidu.com"
如果是枚举类型的常量,需要先创建相应类型:
type Scheme string
const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)
2. 顺序编程
2.1 类型
类型都是值传递的,改变变量的值,只能传递指针
类型 | 含义 |
---|---|
数组 | 数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。 |
切片Slice | 对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者Python中的 list 类型) |
映射map | 一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),其他编程语言中也称为字典(Python)、hash 和 HashTable |
列表list | 列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等 |
2.1.1 数组
var 数组变量名 [元素数量]Type
遍历数组
var a [3]int // 定义三个整数的数组
fmt.Println(a[0]) // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素
// 打印索引和元素
for i, v := range a {
fmt.Printf("%d %d\n", i, v)
}
// 仅打印元素
for _, v := range a {
fmt.Printf("%d\n", v)
}
2.1.2 切片
slice [开始位置 : 结束位置]
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
// 输出3个切片
fmt.Println(strList, numList, numListEmpty)
// 输出3个切片大小
fmt.Println(len(strList), len(numList), len(numListEmpty))
// 切片判定空的结果
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)
使用 make() 函数构造切片
如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:
make( []Type, size, cap)
其中 Type 是指切片的元素类型,size 指的是为这个类型分配多少个元素,cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。
示例如下:
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))
代码输出如下:
[0 0] [0 0]
2 2
append()为切片添加元素
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
切片复制
copy( destSlice, srcSlice []T) int
//copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
切片删除
Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
从开头位置删除
删除开头的元素可以直接移动数据指针:
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):
a = []int{1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
还可以用 copy() 函数来删除开头的元素:
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
删除指定位置的元素
package main
import "fmt"
func main() {
seq := []string{"a", "b", "c", "d", "e"}
// 指定删除位置
index := 2
// 查看删除位置之前的元素和之后的元素
fmt.Println(seq[:index], seq[index+1:])
// 将删除点前后的元素连接起来
seq = append(seq[:index], seq[index+1:]...)
fmt.Println(seq)
}
2.1.3 映射
var mapname map[keytype]valuetype
package main
import "fmt"
func main() {
var mapLit map[string]int
//var mapCreated map[string]float32
var mapAssigned map[string]int
mapLit = map[string]int{"one": 1, "two": 2}
mapCreated := make(map[string]float32)
mapAssigned = mapLit
mapCreated["key1"] = 4.5
mapCreated["key2"] = 3.14159
mapAssigned["two"] = 3
fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
map 容量
和数组不同,map 可以根据新增的 key-value 动态的伸缩,因此它不存在固定长度或者最大限制,但是也可以选择标明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
例如:
map2 := make(map[string]float, 100)
当 map 增长到容量上限的时候,如果再增加新的 key-value,map 的大小会自动加 1,所以出于性能的考虑,对于大的 map 或者会快速扩张的 map,即使只是大概知道容量,也最好先标明。
这里有一个 map 的具体例子,即将音阶和对应的音频映射起来:
noteFrequency := map[string]float32 {"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,"G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}
2.1.4 列表
初始化
- 通过 container/list 包的 New() 函数初始化 list
变量名 := list.New()
- 通过 var 关键字声明初始化 list
var 变量名 list.List
插入元素
方 法 | 功 能 |
---|---|
InsertAfter(v interface {}, mark * Element) * Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark * Element) *Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
删除元素
package main
import "container/list"
func main() {
l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
// 尾部添加后保存元素句柄
element := l.PushBack("fist")
// 在fist之后添加high
l.InsertAfter("high", element)
// 在fist之前添加noon
l.InsertBefore("noon", element)
// 使用
l.Remove(element)
}
每次操作列表变化
操作内容 | 列表元素 |
---|---|
l.PushBack(“canon”) | canon |
l.PushFront(67) | 67, canon |
element := l.PushBack(“fist”) | 67, canon, fist |
l.InsertAfter(“high”, element) | 67, canon, fist, high |
l.InsertBefore(“noon”, element) | 67, canon, noon, fist, high |
l.Remove(element) | 67, canon, noon, high |
遍历列表
遍历双链表需要配合 Front() 函数获取头元素,遍历时只要元素不为空就可以继续进行,每一次遍历都会调用元素的 Next() 函数
l := list.New()
// 尾部添加
l.PushBack("canon")
// 头部添加
l.PushFront(67)
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
""
2.2 流程控制
2.2.1 分支结构
{}
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}
if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断,代码如下:
if err := Connect(); err != nil {
fmt.Println(err)
return
}
Connect 是一个带有返回值的函数,err:=Connect() 是一个语句,执行 Connect 后,将错误保存到 err 变量中。
err != nil 才是 if 的判断表达式,当 err 不为空时,打印错误并返回。
2.2.2 循环结构
与多数语言不同的是,Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,关键字 for 的基本使用方法与C语言和 C++ 中非常接近:
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
无限循环
sum := 0
for {
sum++
if sum > 100 {
break
}
}
for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel)
通过 for range 遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
- map 返回键和值。
- 通道(channel)只返回通道内的值。
2.2.3 选择结构
Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行,示例代码如下:
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)}
1) 一分支多值
当出现多个 case 要放在一起的时候,可以写成下面这样:
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}
不同的 case 表达式使用逗号分隔。
2) 分支表达式
case 后不仅仅只是常量,还可以和 if 一样添加表达式,代码如下:
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}
注意,这种情况的 switch 后面不再需要跟判断变量。
2.2.4 关键字
名称 | 用途 |
---|---|
goto | 通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助 |
break | 结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的 for、switch 和 select 的代码块上 |
continue | 结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环 |
goto退出多重循环
package main
import "fmt"
func main() {
for x := 0; x < 10; x++ {
for y := 0; y < 10; y++ {
if y == 2 {
// 跳转到标签
goto breakHere
}
}
}
// 手动返回, 避免执行进入标签
return
// 标签
breakHere:
fmt.Println("done")
}
代码说明如下:
- 第 1 行,执行某逻辑,返回错误。
- 第 2~6 行,如果发生错误,打印错误退出进程。
- 第 8 行,执行某逻辑,返回错误。
- 第 10~14 行,发生错误后退出流程。
- 第 16 行,没有任何错误,打印完成。
goto集中错误处理
err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()
代码说明如下:
- 第 3 行和第 9 行,发生错误时,跳转错误标签 onExit。
- 第 17 行和第 18 行,汇总所有流程进行错误打印并退出进程
break跳出循环
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
break OuterLoop
case 3:
fmt.Println(i, j)
break OuterLoop
}
}
}
}
continue结束内存循环开启外层循环
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
continue OuterLoop
}
}
}
}
2.3 函数
Go语言里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 lambda 函数
- 方法
2.3.1 普通函数声明
函数声明包括函数名、形式参数列表、返回值列表(可省略)以及函数体。
func 函数名(形式参数列表)(返回值列表){
函数体
}
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
2.3.2 返回值
1)同一类型返回值
返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型
func typedTwoValues() (int, int) {
return 1, 2
}
func main() {
a, b := typedTwoValues()
fmt.Println(a, b)
}
2) 带有变量名的返回值
Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型
func namedRetValues() (a, b int) {
a = 1
b = 2
return
}
调用函数
返回值变量列表 = 函数名(参数列表)
result := add(1,1)
2.3.4 函数变量
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中,下面的代码定义了一个函数变量 f,并将一个函数名为 fire() 的函数赋给函数变量 f,这样调用函数变量 f 时,实际调用的就是 fire() 函数
package main
import (
"fmt"
)
func fire() {
fmt.Println("fire")
}
func main() {
var f func()
f = fire
f()
}
2.3.5 匿名函数
不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成
func(参数列表)(返回参数列表){
函数体
}
1) 在定义时调用匿名函数
匿名函数可以在声明后调用,例如:
func(data int) {
fmt.Println("hello", data)
}(100)
注意第3行
}
后的(100),表示对匿名函数进行调用,传递参数为 100。
2) 将匿名函数赋值给变量
匿名函数可以被赋值,例如:
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
匿名函数作回调函数
package main
import (
"fmt"
)
// 遍历切片的每个元素, 通过给定函数进行元素访问
func visit(list []int, f func(int)) {
for _, v := range list {
f(v)
}
}
func main() {
// 使用匿名函数打印切片内容
visit([]int{1, 2, 3, 4}, func(v int) {
fmt.Println(v)
})
}
代码说明如下:
- 第 8 行,使用 visit() 函数将整个遍历过程进行封装,当要获取遍历期间的切片值时,只需要给 visit() 传入一个回调参数即可。
- 第 18 行,准备一个整型切片 []int{1,2,3,4} 传入 visit() 函数作为遍历的数据。
- 第 19~20 行,定义了一个匿名函数,作用是将遍历的每个值打印出来。
使用匿名函数实现操作封装
package main
import (
"flag"
"fmt"
)
var skillParam = flag.String("skill", "", "skill to perform")
func main() {
flag.Parse()
var skill = map[string]func(){
"fire": func() {
fmt.Println("chicken fire")
},
"run": func() {
fmt.Println("soldier run")
},
"fly": func() {
fmt.Println("angel fly")
},
}
if f, ok := skill[*skillParam]; ok {
f()
} else {
fmt.Println("skill not found")
}
}
代码说明如下:
=
2.3.6 闭包
Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数 + 引用环境 = 闭包
闭包的记忆效应
package main
import (
"fmt"
)
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
// 返回一个闭包
return func() int {
// 累加
value++
// 返回一个累加值
return value
}
}
func main() {
// 创建一个累加器, 初始值为1
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator)
// 创建一个累加器, 初始值为10
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", &accumulator2)
}
代码说明如下:
- 第 8 行,累加器生成函数,这个函数输出一个初始值,调用时返回一个为初始值创建的闭包函数。
- 第 11 行,返回一个闭包函数,每次返回会创建一个新的函数实例。
- 第 14 行,对引用的 Accumulate 参数变量进行累加,注意 value 不是第 11 行匿名函数定义的,但是被这个匿名函数引用,所以形成闭包。
- 第 17 行,将修改后的值通过闭包的返回值返回。
- 第 24 行,创建一个累加器,初始值为 1,返回的 accumulator 是类型为 func()int 的函数变量。
- 第 27 行,调用 accumulator() 时,代码从 11 行开始执行匿名函数逻辑,直到第 17 行返回。
- 第 32 行,打印累加器的函数地址。
闭包实现生成器
闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程
package main
import (
"fmt"
)
// 创建一个玩家生成器, 输入名称, 输出生成器
func playerGen(name string) func() (string, int) {
// 血量一直为150
hp := 150
// 返回创建的闭包
return func() (string, int) {
// 将变量引用到闭包中
return name, hp
}
}
func main() {
// 创建一个玩家生成器
generator := playerGen("high noon")
// 返回玩家的名字和血量
name, hp := generator()
// 打印值
fmt.Println(name, hp)
}
2.3.7 可变参数
...type
可变参数类型
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
myfunc(2, 3, 4)
任意类型的可变参数
用 interface{} 传递任意类型数据是Go语言的惯例用法
func Printf(format string, args ...interface{}) {
// ...
}
package main
import "fmt"
func MyPrintf(args ...interface{}) {
for _, arg := range args {
switch arg.(type) {
case int:
fmt.Println(arg, "is an int value.")
case string:
fmt.Println(arg, "is a string value.")
case int64:
fmt.Println(arg, "is an int64 value.")
default:
fmt.Println(arg, "is an unknown type.")
}
}
}
func main() {
var v1 int = 1
var v2 int64 = 234
var v3 string = "hello"
var v4 float32 = 1.234
MyPrintf(v1, v2, v3, v4)
}
遍历可变参数列表——获取每一个参数的值
package main
import (
"bytes"
"fmt"
)
// 定义一个函数, 参数数量为0~n, 类型约束为字符串
func joinStrings(slist ...string) string {
// 定义一个字节缓冲, 快速地连接字符串
var b bytes.Buffer
// 遍历可变参数列表slist, 类型为[]string
for _, s := range slist {
// 将遍历出的字符串连续写入字节数组
b.WriteString(s)
}
// 将连接好的字节数组转换为字符串并输出
return b.String()
}
func main() {
// 输入3个字符串, 将它们连成一个字符串
fmt.Println(joinStrings("pig ", "and", " rat"))
fmt.Println(joinStrings("hammer", " mom", " and", " hawk"))
}
2.3.8 延迟执行
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件
多个延迟执行语句的处理顺序
package main
import (
"fmt"
)
func main() {
fmt.Println("defer begin")
// 将defer放入延迟调用栈
defer fmt.Println(1)
defer fmt.Println(2)
// 最后一个放入, 位于栈顶, 最先调用
defer fmt.Println(3)
fmt.Println("defer end")
}
代码输出如下:
defer begin
defer end
3
2
1
1) 使用延迟并发解锁
在下面的例子中会在函数中并发使用 map,为防止竞态问题,使用 sync.Mutex 进行加锁
func readValue(key string) int {
valueByKeyGuard.Lock()
// defer后面的语句不会马上调用, 而是延迟到函数结束时调用
defer valueByKeyGuard.Unlock()
return valueByKey[key]
}
2) 使用延迟释放文件句柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源,在下面的例子中将实现根据文件名获取文件大小的函数,函数中需要打开文件、获取文件大小和关闭文件等操作,由于每一步系统操作都需要进行错误处理,而每一步处理都会造成一次可能的退出,因此就需要在退出时释放资源,而我们需要密切关注在函数退出处正确地释放文件资源
func fileSize(filename string) int64 {
f, err := os.Open(filename)
if err != nil {
return 0
}
// 延迟调用Close, 此时Close不会被调用
defer f.Close()
info, err := f.Stat()
if err != nil {
// defer机制触发, 调用Close关闭文件
return 0
}
size := info.Size()
// defer机制触发, 调用Close关闭文件
return size
}
2.3.9 递归函数
递归函数指的是在函数内部调用函数自身的函数,从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破,在实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等.
构成递归需要具备以下条件:
- 一个问题可以被拆分成多个子问题;
- 拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
- 不能无限制的调用本身,子问题需要有退出递归状态的条件
斐波那契数列
package main
import "fmt"
func main(){
result := 0
for i := 1; i <= 10; i++ {
result = fibonacci(i)
fmt.Printf("fibonacci(%d) is %d", i, result)
}
}
func fibonacci(n int) (res int) {
if n <= 2 {
res = 1
}else{
res = fibonacci(n-1) + fibonacci(n-2)
}
return res
}
数字阶乘
package main
import "fmt"
func main(){
result := 0
for i := 1; i <= 10; i++ {
result = factorial(i)
fmt.Printf("factorial(%d) is %d", i, result)
}
}
func factorial(n int) (res int){
if n > 0 {
// n!=n*(n-1)!
res = n * factorial(n-1)
return res
}
return 1
}
3. 面向对象编程
3.1 类型系统
类型都是值传递的,改变变量的值,只能传递指针
3.1.1 值语义和引用语义
值语义和引用语义的差别在于赋值
如
b = a
b.Modify()
如果b的修改不会影响a的值,那么此类型属于值类型,否则为引用类型
大多数类型都属于值语义,包括:
- 基本类型:byte、int、bool、float32、float64和string
- 复合类型:array、struct、pointer等
引用语义,包括:
- slice:切片
- map:映射
- channel :执行体(goruntime)间的通信设施
- interface:接口
3.1.2 结构体
结构体是由零个或多个任意类型的值聚合成的实体,每个值都可以称为结构体的成员。
结构体成员也可以称为“字段”,这些字段有以下特性:
- 字段拥有自己的类型和值;
- 字段名必须唯一;
- 字段的类型也可以是结构体,甚至是字段所在结构体的类型。
使用关键字 type可以将各种基本类型定义为自定义类型,基本类型包括整型、字符串、布尔等。结构体是一种复合的基本类型,通过 type 定义为自定义类型后,使结构体更便于使用。
结构体的定义格式如下:
type 类型名 struct {
字段1 字段1类型
字段2 字段2类型
…
}
对各个部分的说明:
type 类型名 struct{}
实例化结构体
1、实例化–var声明
type Point struct {
X int
Y int
}
var p Point
p.X = 10
p.Y = 20
2、实例化–new关键字
Go语言中,还可以使用 new 关键字对类型(包括结构体、整型、浮点数、字符串等)进行实例化,结构体在实例化后会形成指针类型的结构体
使用 new 的格式如下:
//ins := new(T)
//其中:
//T 为类型,可以是结构体、整型、字符串等。
//ins:T 类型被实例化后保存到 ins 变量中,ins 的类型为 *T,属于指针。
type Player struct{
Name string
HealthPoint int
MagicPoint int
}
tank := new(Player)
tank.Name = "Canon"
tank.HealthPoint = 300
Go语言和 C/C++
在 C/C++ 语言中,使用 new 实例化类型后,访问其成员变量时必须使用->操作符。
在Go语言中,访问结构体指针的成员变量时可以继续使用‘.’,这是因为Go语言为了方便开发者访问结构体指针的成员变量,使用了语法糖(Syntactic sugar)技术,将 ins.Name 形式转换为 (*ins).Name。
取结构体的地址实例化
在go语言中,对结构体进行&取地址操作时,视为对该类型进行一次new的实例化操作,去地址格式如下:
ins := &T{}
其中:
- T表示结构体类型
- ins为结构体的实例,类型为*T,是指针类型
type Command struct {
Name string // 指令名称
Var *int // 指令绑定的变量
Comment string // 指令的注释
}
var version int = 1
cmd := &Command{}
cmd.Name = "version"
cmd.Var = &version
cmd.Comment = "show version"
fmt.Printf("cmd point: %p\n", cmd)
cmd1 := &Command{}
cmd1.Name = "version"
cmd1.Var = &version
cmd1.Comment = "show version"
fmt.Printf("cmd1 point: %p\n", cmd1)
取地址实例化是最广泛的一种结构体实例化方式,可以使用函数封装上面的初始化过程
func newCommand(name string, varref *int, comment string) *Command {
return &Command{
Name: name,
Var: varref,
Comment: comment,
}
}
cmd = newCommand(
"version",
&version,
"show version",
)
3、赋值初始化
func main() {
fan := person{
name: "fan",
age: 10,
}
}
//赋值顺序
func main() {
fan := person{
"fan",
10,
}
}
初始化结构体
结构体在实例化时可以直接对成员变量进行初始化,初始化有两种形式分别是以字段“键值对”形式和多个值的列表形式,键值对形式的初始化适合选择性填充字段较多的结构体,多个值的列表形式适合填充字段较少的结构体。
//键值对填充结构体
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
//多值列表初始化结构体
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr)
3.1.3 可见性
使用大写字母开头的符号,能对其他包可见
3.1.4 接口
package main
import (
"fmt"
)
// 定义一个Invoker接口
type Invoker interface {
// 需要实现一个Call方法
Call(interface{})
}
// 结构体类型
type Struct struct {
}
// 实现Invoker的Call
func (s *Struct) Call(p interface{}) {
fmt.Println("from struct", p)
}
// 函数定义为类型
type FuncCaller func(interface{})
// 实现Invoker的Call
func (f FuncCaller) Call(p interface{}) {
// 调用f函数本体
f(p)
}
func main() {
// 声明接口变量
var invoker Invoker
// 实例化结构体
s := new(Struct)
// 将实例化的结构体赋值到接口
invoker = s
// 使用接口调用实例化结构体的方法Struct.Call
invoker.Call("hello")
// 将匿名函数转为FuncCaller类型,再赋值给接口
invoker = FuncCaller(func(v interface{}) {
fmt.Println("from function", v)
})
// 使用接口调用FuncCaller.Call,内部会调用函数本体
invoker.Call("hello")
}
package main
import "fmt"
import "math"
type Shape interface {
area() float64
}
type MultiShape interface {
Shape //嵌入式
}
type Rectangle struct {
width float64
height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.height * r.width
}
func (c Circle) area() float64 {
return math.Pi * math.Pow(c.radius,2)
}
func getArea(shape MultiShape) float64 { //改为MultiShape
return shape.area()
}
func main(){
r := Rectangle{20,10}
c := Circle{4}
fmt.Println("Rectangle Area =",getArea(r))
fmt.Println("Circle Area =",getArea(c))
}
4. 并发编程
4.1 并发基础
- 多进程。多进程是操作系统层面进行并发的基本模式,也是开销最大的模式。好处在于简单、进程间互不影响,坏处在于由内核管理,系统开销大;
- 多线程。多线程是系统层面的并发模式。比多进程开销小很多,但依旧较大,且在高并发模式下,效率会有影响。
- 基于回调的非阻塞/异步IO。源于多线程会很快耗尽内存和CPU资源。通过事件驱动的方式使用异步IO,尽可能地少使用线程,降低开销。
- 协程。用户态线程。轻量级线程,系统开销小,编程简单,结构清晰,支持语言少
4.2 goruntime
Go语言中协程(轻量级线程)实现,使用关键字go
4.3 channel
“不要通过共享内存来通信,而应该通过通信来共享内存”
package main
import "fmt"
func Count(ch chan int) {
fmt.Println("Counting")
ch <- 1
}
func main() {
chs := make([]chan int, 10)
for i := 0;i < 10;i++{
chs[i] = make(chan int)
go Count(chs[i])
}
for _, ch := range chs {
<- ch
}
}
4.3.1 基本语法
一般声明形式
var chanName chan ElementType
声明并初始化
ch := make(chan int)
常见的用法,写入和读取
// 写入,会导致程序阻塞,直到有其他goruntime从这个channel中读取数据
ch <- value
// 读取,如果之前没有写入数据,会导致程序阻塞,直到被写入数据为止
value := <- ch
4.3.2 select
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述,每个case语句里必须有个channel操作,用于 处理异步IO问题
select {
case <-chan1:
// 如果chan1成功读到数据,则进行
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行
default:
// 如果上面都没成功,则进行
}
4.3.3 缓冲机制
c := make(chan int, 1024)
即使没有读取方,写入方也可以一直往channel里写入,在缓冲区填完之前都不会阻塞
4.3.4 超时机制
Go语言没有提供直接的超时处理机制,但可以利用select机制
// 实现并执行一个匿名超时等待函数
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) //等待1秒钟
timeout <- true
}()
select {
case <- ch:
//从ch中读取数据
case <- timeout:
// 一直没有从ch中读取数据,但从timeout中读取到了数据
}
程序会在timeout中获取到一个数据后继续执行,无论对ch的读取是否还处于等待状态,从而达到1秒超时效果
4.4 同步
4.4.1 同步锁
sync包中提供了两种锁类型:sync.Mutex和sync.RWMutex。Mutex是最简单的一种锁类型,当一个goroutine获得Mutex后,其他goruntine就只能等待这个goruntine释放该Mutex。RWMutex相对友好,经典的单写多读模型,在读锁占用的情况下,只会阻止写,也就是多个goruntine可同时获取读锁(调用RLock()方法;而写锁(调用Lock()方法,会阻止任何其他goruntine进来,相当于独占))
//一一对应,否则导致该锁的所有goruntine处于饥饿状态,甚至死锁
// 写锁
Lock() <--> Unlock()
// 读锁
RLock() <--> RUnlock()
4.4.2 全局唯一性
全局初始化操作
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}
once的Do方法保证全局访问内只调用制定的函数一次
测试
Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能
测试规则
要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以_test.go结尾,单元测试源码文件可以由多个测试用例(可以理解为函数)组成,每个测试用例的名称需要以 Test 为前缀,例如:
func TestXxx( t *testing.T ){
//......
}
编写测试用例有以下几点需要注意:
_test.goTestBenchmark(t *testing.T)(t *testing.B)go test_test.goTest
Go语言的 testing 包提供了三种测试方式,分别是单元(功能)测试、性能(压力)测试和覆盖率测试。
测试模块
package demo
// 根据长宽获取面积
func GetArea(weight int, height int) int {
if weight > height{
return weight * height
}
return weight * height
}
单元(功能)测试
package demo
import "testing"
func TestGetArea(t *testing.T) {
area := GetArea(40, 50)
if area != 2000 {
t.Error("测试失败")
}
}
执行命令:
go test -v
=== RUN TestGetArea
--- PASS: TestGetArea (0.00s)
PASS
ok _/home/hjy/TestCode/go/demo 0.001s
性能(压力)测试
package demo
import "testing"
func BenchmarkGetArea(t *testing.B) {
for i := 0; i < t.N; i++ {
GetArea(40, 50)
}
}
执行命令:
go test -bench='.'
goos: linux
goarch: amd64
BenchmarkGetArea-6 1000000000 0.256 ns/op
PASS
ok _/home/hjy/TestCode/go/demo 0.285s
覆盖率测试
覆盖率测试能知道测试程序总共覆盖了多少业务代码(也就是 demo_test.go 中测试了多少 demo.go 中的代码),可以的话最好是覆盖100%
package demo
import "testing"
func TestGetArea(t *testing.T) {
areas := GetArea(40, 50)
if areas != 2000 {
t.Error("测试失败")
}
}
func TestGetArea1(t *testing.T) {
areas := GetArea(50, 40)
if areas != 2000 {
t.Error("测试失败")
}
}
func BenchmarkGetArea(t *testing.B) {
for i := 0; i < t.N; i++{
GetArea(40, 50)
}
}
执行命令:
go test -cover
PASS
coverage: 100% of statements
ok _/home/hjy/TestCode/go/demo 0.001s