主要参考自http://c.biancheng.net/golang
栈可用于内存分配,栈的分配和回收速度非常快。下面的代码展示了栈在内存分配上的作用:
func calc(a, b int) int {
var c int
c = a * b
var x int
x = c * 10
return x
}
代码说明如下:
第 1 行,传入 a、b 两个整型参数。
第 2 行,声明整型变量 c,运行时,c 会分配一段内存用以存储 c 的数值。
第 3 行,将 a 和 b 相乘后赋值给 c。
第 5 行,声明整型变量 x,x 也会被分配一段内存。
第 6 行,让 c 乘以 10 后赋值给变量 x。
第 8 行,返回 x 的值。
上面的代码在没有任何优化的情况下,会进行变量 c 和 x 的分配过程。Go语言默认情况下会将 c 和 x 分配在栈上,这两个变量在 calc() 函数退出时就不再使用,函数结束时,保存 c 和 x 的栈内存再出栈释放内存,整个分配内存的过程通过栈的分配和回收都会非常迅速。
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配
Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
go run -gcflags "-m -l" main.go
使用 go run 运行程序时,-gcflags 参数是编译参数。其中 -m 表示进行内存分配分析,-l 表示避免程序内联,也就是避免进行程序优化
package main
import "fmt"
// 声明空结构体测试结构体逃逸情况
type Data struct {
}
func dummy() *Data {
// 实例化c为Data类型
var c Data
//返回函数局部变量地址
return &c
}
func main() {
fmt.Println(dummy())
}
上述方法引用局部变量地址,在C/C++中会因为局部变量被释放,所以无法找到局部变量
在go中执行变量逃逸分析后发现
./main.go:12:6: moved to heap: c
这句话表示,Go 编译器已经确认如果将变量 c 分配在栈上是无法保证程序最终结果的,如果这样做,dummy() 函数的返回值将是一个不可预知的内存地址,这种情况一般是 C/C++ 语言中容易犯错的地方,引用了一个函数局部变量的地址
Go语言最终选择将 c 的 Data 结构分配在堆上。然后由垃圾回收器去回收 c 的内存
变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。
变量的生命周期与变量的作用域有着不可分割的联系:
- 全局变量:它的生命周期和整个程序的运行周期是一致的;
- 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
- 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
var global *int
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
上述代码中,函数 f 里的变量 x 必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的。用Go语言的术语说,这个局部变量 x 从函数 f 中逃逸了。
相反,当函数 g 返回时,变量 *y 不再被使用,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间,也可以选择在堆上分配,然后由Go语言的 GC(垃圾回收机制)回收这个变量的内存空间。
虽然Go语言能够帮助我们完成对内存的分配和释放,但是为了能够开发出高性能的应用我们任然需要了解变量的声明周期。例如,如果将局部变量赋值给全局变量,将会阻止 GC 对这个局部变量的回收,导致不必要的内存占用,从而影响程序的性能
iota常量生成器
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
模拟枚举
type Weapon int
const (
Arrow Weapon = iota // 开始生成枚举值, 默认为0
Shuriken
SniperRifle
Rifle
Blower
)
// 输出所有枚举值
fmt.Println(Arrow, Shuriken, SniperRifle, Rifle, Blower)
// 使用枚举类型并赋初值
var weapon Weapon = Blower
fmt.Println(weapon)
生成标志位常量
const (
FlagNone = 1 << iota
FlagRed
FlagGreen
FlagBlue
)
fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue)
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue)
像type Weapon int
这种写法,是表明Weapon具有int的各项属性,但并不具有int的方法;定义类型不获得方法,需要自己定义;定义别名是可以获得方法的,但是不可以覆盖方法;如果想要继承方法可以采用组合的方式实现继承,也可以覆盖原方法
区分类型别名与类型定义
类型定义type TypeName Type
类型别名type TypeAlias = Type
package main
import (
"fmt"
)
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
func main() {
// 将a声明为NewInt类型
var a NewInt
// 查看a的类型名
fmt.Printf("a type: %T\n", a)
// 将a2声明为IntAlias类型
var a2 IntAlias
// 查看a2的类型名
fmt.Printf("a2 type: %T\n", a2)
}
// a type: main.NewInt
// a2 type: int
IntAlias 类型只会在代码中存在,编译完成时,不会有 IntAlias 类型
非本地类型不能定义方法
package main
import (
"time"
)
// 定义time.Duration的别名为MyDuration
type MyDuration = time.Duration
// 为MyDuration添加一个函数
func (m MyDuration) EasySet(a string) {
}
func main() {
}
上述代码报错,因为MyDuration是Duration的Alias,非本地类型
解决这个问题有下面两种方法:
- 修改为
type MyDuration time.Duration
,也就是将 MyDuration 从别名改为类型 - 将 MyDuration 的别名定义放在 time 包中
在切片的开头添加元素
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。
因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
使用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个位置
从开头位置删除元素
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
a = append(a[:0], a[1:]...) // 不移动数据指针,删除开头1个元素,在原内存空间完成
a = append(a[:0], a[N:]...) // 删除开头N个元素
从中间位置删除元素
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
从尾部删除
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来
连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表container/list等能快速从删除点删除元素)
当迭代切片时,关键字 range 会返回两个值,第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本,不是直接返回对该元素的引用;迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,所以 index和value 的地址总是相同的
数组以及基本类型在声明时即可初始化,而指针、切片、映射、通道、函数和接口在声明时未初始化,都为nil
切片,map都是引用类型,数组非引用类型,其指针类型为*[n]int
,对引用类型可取地址
数组指针 切片指针 结构体指针
区分数组指针和指针数组
数组指针:指的是一个指针,只不过这个指针指向了一个数组
指针数组:指的是一个数组,这个数组里面装满了指针
在go语言中,内存地址相同并不意味着其表示的数据或者数据类型就完全相同
var arr [5]int = [5]int{1, 2, 3, 4, 5}
p1, p2 := &arr, &arr[0]
fmt.Printf("%p\n", p1)
fmt.Println(p2)
*仅对于访问下标时可以不写
var arr [5]int = [5]int{1,2,3,4,5}
p := &arr
fmt.Println(*p[0]) // 错,由于初等运算符[]优先级大于单目运算符*
fmt.Println((*p)[0]) // 完整写法
fmt.Println(p[0]) // 省略写法
切片指针
go语言中切片名本身就是一个地址,通过切片名加下标的方式访问切片元素原本就是指针访问的一种表现,类似于go语言中*寻址运算符可以不写,p[0]
在二重与二重以上的指针参与运算的时候,*寻址运算符则是一个必不可少的角色
**重要:**切片指针作为函数参数
切片指针作为函数参数传入函数内部时,不论是修改还是追加都能保证函数内的操作影响到函数外部
切片作为函数参数传入函数内部,只有修改会影响外部,而追加则无法对外部造成影响
func sliceParam(tempSlice []int){
tempSlice = append(tempSlice, 4,5,6)
}
func sliceParamPointer(tempSlicePointer *[]int){
*tempSlicePointer = append(*tempSlicePointer,4,5,6)
}
func main() {
slice := []int{1,2,3}
sliceParam(slice)
fmt.Println(slice)//[1,2,3]
sliceParamPointer(&slice);
fmt.Println(slice)//[1,2,3,4,5,6]
}
因为当切片作为参数的时候,一旦对tempSlice追加数据。那么tempSlice变量的值,即所保存的内存地址就会变化
因此
- 数组传参只能改不能加且无法影响外部
- 切片传参改能影响外部,加无法影响外部;注因为切片传参等价于重新创造了指向同一地层数组的切片,若append操作更改了底层数组,则改也无法影响外部;因此append操作使用return方式实现;python中list就相当于引用类型,可改可加,且能影响外部
- 字典传参能改能加且均会影响外部
- 切片指针能改能加且均会影响外部
在调用过程中,结构体的内存会被复制后传入函数,并创建一块新的内存,当函数返回时,又会将返回值复制一次,并创建一块新的内存,赋给函数返回值的接收变量;若使用原来的变量,则只是赋值
cap的适用类型
数组:len和cap相等
切片:由于不需要初始化,所以cap>=len
字典:随用随加,只有len
chan:len和cap相等
mapCreated := make(map[string]float)
等价于mapCreated := map[string]float{}
并发map
并发的 map 读和 map 写,也就是说使用了两个并发函数不断地对 map 进行读和写而发生了竞态问题,map 内部会对这种并发操作进行检查并提前发现
需要并发读写时,一般的做法是加锁,但这样性能并不高,Go语言在 1.9 版本中提供了一种效率较高的并发安全的 sync.Map,sync.Map 和 map 不同,不是以语言原生形态提供,而是在 sync 包下的特殊结构
sync.Map 有以下特性:
- 无须初始化,直接声明即可。
var scene sync.Map
- sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
- 使用 Range 配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range 参数中回调函数的返回值在需要继续迭代遍历时,返回 true,终止迭代遍历时,返回 false。
package main
import (
"fmt"
"sync"
)
func main() {
var scene sync.Map
// 将键值对保存到sync.Map
scene.Store("greece", 97)
scene.Store("london", 100)
scene.Store("egypt", 200)
// 从sync.Map中根据键取值
fmt.Println(scene.Load("london"))
// 根据键删除对应的键值对
scene.Delete("london")
// 遍历所有sync.Map中的键值对
scene.Range(func(k, v interface{}) bool {
fmt.Println("iterate:", k, v)
return true
})
}
nil标识符是不能比较的,Go语言中的 nil 和其他语言中的 null 有很多不同点,这点和 python 等动态语言是不同的,在 python 中,两个 None 值永远相等
nil不是关键字或保留字
nil没有默认类型
不同类型的nil指针是一样的
func main() {
var arr []int
var num *int
fmt.Printf("%p\n", arr)
fmt.Printf("%p", num)
}
唯一可以进行比较的情况为,将某类型的空值直接与nil标识符比较
func main() {
var s1 []int
fmt.Println(s1 == nil)
}
nil是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值
不同类型的nil值占用内存大小可能不一样,一个类型的所有的值的内存布局是一样的,nil的大小和同类型非nil类型的大小是一样的
new和make
new 和 make 是两个内置函数,主要用来创建并分配类型的内存,new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化
new 函数只接受一个参数,这个参数是一个类型,并且返回一个指向该类型内存地址的指针。同时 new 函数会把分配的内存置为零,也就是该类型的零值。
a := new(int)
// *a为0
type Student struct {
name string
age int
}
s := new(Student)
// *s为每一项都为零值的结构体
make也用于内存分配,但它只用于chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了
make 函数只用于 map,slice 和 channel,并且不返回指针。如果想要获得一个显式的指针,可以使用 new 函数进行分配,或者显式地使用一个变量的地址
实现原理
在编译期的类型检查阶段,Go语言其实就将代表 make 关键字的 OMAKE 节点根据参数类型的不同转换成了 OMAKESLICE、OMAKEMAP 和 OMAKECHAN 三种不同类型的节点,这些节点最终也会调用不同的运行时函数来初始化数据结构s
内置函数 new 会在编译期的 SSA 代码生成阶段经过 callnew 函数的处理,如果请求创建的类型大小是 0,那么就会返回一个表示空指针的 zerobase 变量,在遇到其他情况时会将关键字转换成 newobjects
goto退出多重循环
func main() {
for x := 0; x < 10; x++ {
for y := 0; y < 10; y++ {
if y == 2 {
// 跳转到标签
goto breakHere
}
}
}
// 手动返回, 避免执行进入标签
return
// 标签
breakHere:
fmt.Println("done")
}
goto处理多个错误
func main() {
err := firstCheckError()
if err != nil {
goto onExit
}
err = secondCheckError()
if err != nil {
goto onExit
}
fmt.Println("done")
return
onExit:
fmt.Println(err)
exitProcess()
}
break跳出多层循环
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继续下一个指定循环
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
}
}
}
}
var f func()
此时f称为回调函数
链式处理
func StringProccess(list []string, chain []func(string) string) {
// 遍历每一个字符串
for index, str := range list {
// 遍历每一个处理链
for _, proc := range chain {
// 输入一个字符串进行处理,返回数据作为下一个处理链的输入。
str = proc(str)
}
// 将结果放回切片
list[index] = str
}
}
// 处理函数链
chain := []func(string) string{
strings.TrimSpace,
strings.ToUpper,
}
匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式,由一个不带函数名的函数声明和函数体组成
1 匿名函数在声明后调用
func(data int) {
fmt.Println("hello", data)
}(100)
2 匿名函数赋值给变量
// 将匿名函数体保存到f()中
f := func(data int) {
fmt.Println("hello", data)
}
// 使用f()调用
f(100)
自定义类型实现接口
type Invoker interface {
// 需要实现一个Call()方法
Call(interface{})
}
// 结构体类型
type Struct struct {
}
func (s Struct) Call(p interface{}) {
fmt.Println("from struct", p)
}
func (s Struct) Call2() {
fmt.Println("Call 2")
}
// 自定义类型1
type Type1 int
func (t Type1) Call(p interface{}) {
fmt.Println("from Type1", p)
}
// 自定义类型2,函数类型
type Type2 func()
func (t Type2) Call(p interface{}) {
fmt.Println("from Type2", p)
}
// 函数类型
type FuncCaller func(interface {})
func (f FuncCaller) Call(p interface{}) {
fmt.Println("from Function", p)
}
var invoker Invoker
invoker = Struct{}
invoker.Call("hello")
invoker.Call2("hello") // 报错,因为只根据调用者来找方法,接口不包含Call2
invoker = Type1(1) // 使用右值转换为Type1
invoker.Call("hello")
invoker = Type2(func() {}) // 使用右值转换为Type2
invoker.Call("hello")
invoker = FuncCaller(func(v interface{}) {}) // 使用右值转换为FuncCaller
invoker.Call("hello")
闭包
函数+引用环境=闭包
函数是编译期静态的概念,而闭包是运行期动态的概念
闭包(Closure)在某些编程语言中也被称为 Lambda 表达式,闭包对环境中变量的引用过程也可以被称为“捕获”
在闭包内部修改引用的变量
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改
str := "hello world"
func() {
str = "hello dude"
}()
fmt.Println(str)
闭包的记忆效应
通过捕获外部变量并进行修改,变量会跟随闭包生命期一起存在, 闭包便如同变量一样
// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
// 返回一个闭包
return func() int {
// 累加
value++
// 返回一个累加值
return value
}
}
// 创建一个累加器, 初始值为1
accumulator := Accumulate(1)
// 累加1并打印
fmt.Println(accumulator())
fmt.Println(accumulator())
// 打印累加器的函数地址
fmt.Printf("%p\n", accumulator)
// 创建一个累加器, 初始值为1
accumulator2 := Accumulate(10)
// 累加1并打印
fmt.Println(accumulator2())
// 打印累加器的函数地址
fmt.Printf("%p\n", accumulator2)
accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例
可变参数
形如...type
格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数,它是一个语法糖(syntactic sugar),即这种语法对语言的功能并没有影响,但是更方便程序员使用
从内部实现机理上来说,类型...type
本质上是一个数组切片,也就是[]type
任意类型可变参数
func Printf(format string, args ...interface{}) {
// ...
}
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.")
}
}
}
var a interface{}
a = 1
fmt.Println(a.(int))
if v, ok := a.(int); ok {
fmt.Println(v)
}
defer语句
Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行
典型的例子就是对一个互斥解锁,或者关闭一个文件
错误接口
type error interface {
Error() string
}
自定义错误
var err = errors.New("this is an error")
type ParseError struct {
Filename string // 文件名
Line int // 行号
}
// 实现error接口,返回错误描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
switch detail := e.(type) {
case *ParseError: // 这是一个解析错误
fmt.Printf("Filename: %s Line: %d\n", detail.Filename, detail.Line)
default: // 其他类型的错误
fmt.Println("other error")
}
错误对象都要实现 error 接口的 Error() 方法,这样,所有的错误都可以获得字符串的描述,如果想进一步知道错误的详细信息,可以通过类型断言,将错误对象转为具体的错误类型进行错误详细信息的获取
宕机panic
func panic(v interface{}) //panic() 的参数可以是任意类型的
defer fmt.Println("宕机后要做的事情1")
defer fmt.Println("宕机后要做的事情2")
panic("宕机")
defer fmt.Println("宕机后要做的事情3")
1 defer反序执行
2 panic后的语句就算是defer也不会执行
recover() 可以让进入宕机流程中的 goroutine 恢复过来,recover 仅在延迟函数 defer 中有效
在正常的执行过程中,调用 recover 会返回 nil 并且没有其他任何效果,如果当前的 goroutine 宕机,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行
Go语言没有异常系统,其使用 panic 触发宕机类似于其他语言的抛出异常,recover 的宕机恢复机制就对应其他语言中的 try/catch 机制
recover恢复的例子
type panicContext struct {
function string // 所在函数
}
// 保护方式允许一个函数
func ProtectRun(entry func()) {
// 延迟处理的函数,先预执行defer再执行entry
defer func() {
// 发生宕机时,获取panic传递的上下文并打印
err := recover()
switch err.(type) {
case runtime.Error: // 运行时错误
fmt.Println("runtime error:", err)
default: // 非运行时错误
fmt.Println("error:", err)
}
}()
entry()
}
func main() {
fmt.Println("运行前")
// 允许一段手动触发的错误
ProtectRun(func() {
fmt.Println("手动宕机前")
// 使用panic传递上下文
panic(&panicContext{
"手动触发panic",
})
fmt.Println("手动宕机后")
})
// 故意造成空指针访问错误
ProtectRun(func() {
fmt.Println("赋值宕机前")
var a *int
*a = 1
fmt.Println("赋值宕机后")
})
fmt.Println("运行后")
}
代码输出结果:
运行前
手动宕机前
error: &{手动触发panic}
赋值宕机前
runtime error: runtime error: invalid memory address or nil pointer dereference
运行后
Ps: 当 panic 触发崩溃时,ProtectRun() 函数将结束运行
panic 和 recover 的组合有如下特性:
- 有 panic 没 recover,程序宕机
- 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行
在 panic 触发的 defer 函数内,可以继续调用 panic,进一步将错误外抛,直到程序整体崩溃
start := time.Now() // 获取当前时间
elapsed := time.Since(start)
elapsed := time.Now().Sub(start)
Go 编译器产生的汇编代码是一种中间抽象态,它不是对机器码的映射,而是和平台无关的一个中间态汇编描述
函数的多值返回实质上是在栈上开辟多个地址分别存放返回值
测试
在同一文件夹下创建两个Go语言文件,分别命名为 demo.go 和 demo_test.go
demo.go
package demo
// 根据长宽获取面积
func GetArea(weight int, height int) int {
return weight * height
}
demo_test.go
// 功能测试
package demo
import "testing"
func TestGetArea(t *testing.T) {
area := GetArea(40, 50)
if area != 2000 {
t.Error("测试失败")
}
}
// 性能压力测试
func BenchmarkGetArea(t *testing.B) {
for i := 0; i < t.N; i++ {
GetArea(40, 50)
}
}
结构体的定义只是一种内存布局的描述,只有当结构体实例化时,才会真正地分配内存,因此必须在定义结构体并实例化后才能使用结构体的字段
实例化结构体
1 var a person
2 a := person{}
3 a := new(person)
结构体成员中只能包含结构体的指针类型,包含非指针类型会引起编译错误
type person struct {
name string `json:"fyh"`
age int
child *person
}
匿名结构体
匿名结构体的初始化写法由结构体定义和键值对初始化两部分组成,结构体定义时没有结构体类型名,只有字段和类型定义,键值对初始化部分由可选的多个键值对组成
// 打印消息类型, 传入匿名结构体,注意类型名为详细描述
func printMsgType(msg *struct {
id int
data string
}) {
// 使用动词%T打印msg的类型
fmt.Printf("%T\n", msg)
}
func main() {
// 实例化一个匿名结构体
msg := &struct { // 定义部分
id int
data string
}{ // 值初始化部分
1024,
"hello",
}
printMsgType(msg)
}
模拟构造函数重载
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color,
}
}
由于Go语言中没有函数重载,为了避免函数名字冲突,使用 NewCatByName() 和 NewCatByColor() 两个不同的函数名表示不同的 Cat 构造过程
模拟父级构造调用
type Cat struct {
Color string
Name string
}
type BlackCat struct {
Cat // 嵌入Cat, 类似于派生
}
// “构造基类”
func NewCat(name string) *Cat {
return &Cat{
Name: name,
}
}
// “构造子类”
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{}
cat.Color = color
return cat
}
这个例子中,Cat 结构体类似于面向对象中的“基类”,BlackCat 嵌入 Cat 结构体,类似于面向对象中的“派生”,实例化时,BlackCat 中的 Cat 也会一并被实例化。
总之,Go语言中没有提供构造函数相关的特殊机制,用户根据自己的需求,将参数使用函数传递到结构体构造参数中即可完成构造函数的任务
go语言 方法
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数
接收器类型可以是(几乎)任何类型,不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型,因为接口是一个抽象定义,而方法却是具体实现
一个类型加上它的方法等价于面向对象中的一个类,一个重要的区别是,在Go语言中,类型的代码和绑定在它上面的方法的代码可以不放置在一起,它们可以存在不同的源文件中,唯一的要求是它们必须是同一个包的
因为方法是函数,所以同样的,不允许方法重载,即对于一个类型只能有一个给定名称的方法,但是如果基于接收器类型,是有重载的:具有同样名字的方法可以在 2 个或多个不同的接收器类型上存在,比如在同一个包里这么做是允许的
接收器中的参数变量名在命名时,官方建议使用接收器类型名的第一个小写字母
接收器根据接收器的类型可以分为指针接收器、非指针接收器,两种接收器在使用时会产生不同的效果,根据效果的不同,两种接收器会被用于不同性能和功能要求的代码中
指针类型的接收器由一个结构体的指针组成,更接近于面向对象中的 this 或者 self
由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法结束后,修改都是有效的
当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效
注意指针方法和非指针方法中,由于map是一个引用类型,因此使用非指针接收器即可以修改或者增加;而对于切片而言,非指针方法只能改不能加,指针方法可以改可以加
在计算机中,小对象由于值复制时的速度较快,所以适合使用非指针接收器,大对象因为复制性能较低,适合使用指针接收器,在接收器和参数间传递时不进行复制,只是传递指针
爬取网页原代码
golang
import "net/http"
// 实例化一个HTTP客户端
client := &http.Client{}
// 创建一个http请求
req, err := http.NewRequest("POST", "http://baijiahao.baidu.com/s?id=1653584491632417184&wfr=spider&for=pc", strings.NewReader("key=value"))
// 发现错误就打印并退出
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
// 为标头添加信息
req.Header.Add("User-Agent", "Chrome/79.0.3945.88")
// 开始请求
resp, err := client.Do(req)
// 处理请求的错误
if err != nil {
fmt.Println(err)
os.Exit(1)
return
}
data, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(data))
defer resp.Body.Close()
python
import urllib.request
opener=urllib.request.build_opener()
opener.addheaders=[("User-Agent", "Chrome/79.0.3945.88")]
urllib.request.install_opener(opener)
url="http://baijiahao.baidu.com/s?id=1653584491632417184&wfr=spider&for=pc"
data=urllib.request.urlopen(url)
data_str = str(data.read().decode("utf-8"))
print(data_str)
方法和函数的统一调用
// 声明一个结构体
type class struct {
}
// 给结构体添加Do方法
func (c *class) Do(v int) {
fmt.Println("call method do:", v)
}
// 普通函数的Do
func funcDo(v int) {
fmt.Println("call function do:", v)
}
// 声明一个函数回调
var delegate func(int)
// 创建结构体实例
c := new(class)
// 将回调设为c的Do方法
delegate = c.Do
// 调用
delegate(100)
// 将回调设为普通函数
delegate = funcDo
// 调用
delegate(100)
这段代码能运行的基础在于:无论是普通函数还是结构体的方法,只要它们的签名一致,与它们签名一致的函数变量就可以保存普通函数或是结构体方法
// 实例化一个通过字符串映射函数切片的map
var eventByName = make(map[string][]func(interface{}))
// 注册事件,提供事件名和回调函数
func RegisterEvent(name string, callback func(interface{})) {
// 通过名字查找事件列表
list := eventByName[name]
// 在列表切片中添加函数
list = append(list, callback)
// 将修改的事件列表切片保存回去
eventByName[name] = list
}
// 调用事件
func CallEvent(name string, param interface{}) {
// 通过名字找到事件列表
list := eventByName[name]
// 遍历这个事件的所有回调
for _, callback := range list {
// 传入参数调用回调
callback(param)
}
}
注意这里eventByName[name] = list
,和切片传递一样,如果发生了值传递,则只能改不能加;而因为这里eventByName是一个全局变量,所以只使用eventByName[name]可以加可以改
一般来说,事件系统不保证同一个事件实现方多个函数列表中的调用顺序,事件系统认为所有实现函数都是平等的
内嵌结构体
Go语言的结构体内嵌有如下特性
(1) 内嵌的结构体可以直接访问其成员变量
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的字段。例如,ins.a.b.c的访问可以简化为ins.c。
(2) 内嵌结构体的字段名是它的类型名
内嵌结构体字段仍然可以使用详细的字段进行一层层访问,内嵌结构体的字段名就是它的类型名
内嵌匿名结构体
// 车
type Car struct {
Wheel
// 引擎
Engine struct {
Power int // 功率
Type string // 类型
}
}
// 初始化
c := Car{
// 初始化轮子
Wheel: Wheel{
Size: 18,
},
// 初始化Engine使用匿名结构体
Engine: struct {
Power int
Type string
}{
Type: "1.4T",
Power: 143,
},
}
JSON解析匿名结构体
// 定义手机屏幕
type Screen struct {
Size float32 // 屏幕尺寸
ResX, ResY int // 屏幕水平和垂直分辨率
}
// 定义电池
type Battery struct {
Capacity int // 容量
}
// 创建匿名结构体并转为json数据
raw := &struct {
Screen
Battery
HasTouchID bool // 序列化时添加的字段:是否有指纹识别
}{
// 屏幕参数
Screen: Screen{
Size: 5.5,
ResX: 1920,
ResY: 1080,
},
// 电池参数
Battery: Battery{
2910,
},
// 是否有指纹识别
HasTouchID: true,
}
// 将数据序列化为JSON
jsonData, _ := json.Marshal(raw)
return jsonData
// 只需要屏幕和指纹识别信息的结构和实例
screenAndTouch := struct {
Screen
HasTouchID bool
}{}
// 反序列化到screenAndTouch
json.Unmarshal(jsonData, &screenAndTouch)
在转换 JSON 格式时,JSON 的各个字段名称默认使用结构体的名称,如果想要指定为其它的名称我们可以在声明结构体时添加一个`json:" "`标签,在" "
中可以填入我们想要的内容
我们还可以在上面的标签的" "
中加入 omitempty(使用逗号,
与前面的内容分隔),来过滤掉转换的 JSON 格式中的空值
type Skill struct {
Name string `json:"name,omitempty"`
Level int `json:"level"`
}
GC
GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC()
函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用 runtime.GC()
,这样会立即释放一大片内存,但是会造成程序短时间的性能下降
finalizer(终止器)是与对象关联的一个函数,通过 runtime.SetFinalizer 来设置,如果某个对象定义了 finalizer,当它被 GC 时候,这个 finalizer 就会被调用,以完成一些特定的任务,例如发信号或者写日志等
func SetFinalizer(x, f interface{})
- 参数 x 必须是一个指向通过 new 申请的对象的指针,或者通过对复合字面值取址得到的指针。
- 参数 f 必须是一个函数,它接受单个可以直接用 x 类型值赋值的参数,也可以有任意个被忽略的返回值。
SetFinalizer 函数可以将 x 的终止器设置为 f,当垃圾收集器发现 x 不能再直接或间接访问时,它会清理 x 并调用 f(x)
另外,x 的终止器会在 x 不能直接或间接访问后的任意时间被调用执行,不保证终止器会在程序退出前执行,因此一般终止器只用于在长期运行的程序中释放关联到某对象的非内存资源
终止器会按依赖顺序执行:如果 A 指向 B,两者都有终止器,且 A 和 B 没有其它关联,那么只有 A 的终止器执行完成,并且 A 被释放后,B 的终止器才可以执行
终止器只有在对象被 GC 时,才会被执行。其他情况下,都不会被执行,即使程序正常结束或者发生错误
type Road int
func findRoad(r *Road) {
log.Println("road:", *r)
}
func entry() {
var rd Road = Road(999)
r := &rd
runtime.SetFinalizer(r, findRoad)
}
func main() {
entry()
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
runtime.GC()
}
}
golang中的链表
- 首元结点:就是链表中存储第一个元素的结点,如下图中 a1 的位置。
- 头结点:它是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以存储链表的长度或者其它的信息,也可以为空不存储任何信息。头节点不是必须的
- 头指针:它是指向链表中第一个结点的指针。若链表中有头结点,则头指针指向头结点;若链表中没有头结点,则头指针指向首元结点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vu7kxNEP-1588841509486)(C:\Users\56909\AppData\Roaming\Typora\typora-user-images\image-20200102104613575.png)]
当要处理的数据具有环型结构特点时,就特别适合采用循环链表
接口
- 接口类型名:使用 type 将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加 er,如有写操作的接口叫 Writer,有字符串功能的接口叫 Stringer,有关闭功能的接口叫 Closer 等。
- 方法名:当方法名首字母是大写时,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
- 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以被忽略
类型断言
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)
- 如果 T 是具体某个类型,类型断言会检查 x 的动态类型是否等于具体类型 T。如果检查成功,类型断言返回的结果是 x 的动态值,其类型是 T。
- 如果 T 是接口类型,类型断言会检查 x 的动态类型是否满足 T。如果检查成功,x 的动态值不会被提取,返回值是一个类型为 T 的接口值;换句话说,对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保护了接口值内部的动态类型和值的部分
- 无论 T 是什么类型,如果 x 是 nil 接口值,类型断言都会失败。
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功:*os.file具有读写功能
由于将 []string 定义成 MyStringList 类型,以下两种写法等价
type MyStringList []string
a := MyStringList([]string{"fyh", "ztd"})
a := MyStringList{"fyh", "ztd"}
预定义的一些方法
sort.Strings/sort.Ints/sort.Slice
二叉树的递归算法
输出一个给定二叉树的嵌套括号表示
-
首先输出根节点,然后再依次输出左子树和右子树,在输出左子树前打印输出左括号“(”,在输出右子树后打印输出右括号“)”;
-
另外,依次输出的左、右子树要至少有一个不为空,若都为空就不必输出了。
func PrintBT(n *Node) {
if n != nil {
fmt.Printf("%v", n.Data)
if n.Left != nil || n.Right != nil {
fmt.Printf("(")
PrintBT(n.Left)
if n.Right != nil {
fmt.Printf(",")
}
PrintBT(n.Right)
fmt.Printf(")")
}
}
}
计算二叉树的深度
- 若一棵二叉树为空,则其深度为 0;
- 否则,其深度等于左子树或右子树的最大深度加 1。
func Depth(n *Node) int {
var depleft, depright int
if n == nil {
return 0
} else {
depleft = Depth(n.Left)
depright = Depth(n.Right)
if depleft > depright {
return depleft + 1
} else {
return depright + 1
}
}
}
统计二叉树叶子节点数
- 若一棵二叉树为空,则其叶子节点数为 0;
- 若一棵二叉树的左、右子树均为空,则其叶子节点数为 1;
- 否则叶子数等于左子树与右子树叶子总数之和。
func LeafCount(n *Node) int {
if n == nil {
return 0
} else if (n.Left == nil) && (n.Right == nil) {
return 1
} else {
return (LeafCount(n.Left) + LeafCount(n.Right))
}
}
前序遍历、中序遍历、后序遍历
func PreOrder(n *Node) {
if n != nil {
fmt.Printf("%v", n.Data)
PreOrder(n.Left)
PreOrder(n.Right)
}
}
func InOrder(n *Node) {
if n != nil {
PreOrder(n.Left)
fmt.Printf("%v", n.Data)
PreOrder(n.Right)
}
}
func PostOrder(n *Node) {
PreOrder(n.Left)
PreOrder(n.Right)
fmt.Printf("%v", n.Data)
}
几种包的引用格式
// 标准引用格式
import "fmt"
// 别名引用格式
import F "fmt"
// 省略引用格式,相当于合并到当前程序
import . "fmt"
// 匿名引用格式,只是执行初始化init函数,而不使用任何包内的结构和类型和定义函数
import _ "fmt"
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过import _ "包的路径"
这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
- 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
- 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
- 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次
Go语言包的初始化有如下特点:
- 包初始化程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。
- Go 编译器会将有向无环图转换为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化。
- 单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的 init 函数。
封装
在Go语言中封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作
封装的实现步骤:
- 将结构体、字段的首字母小写;
- 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
- 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
- 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。
在 GOPATH 指定的工作目录下,代码总是会保存在 $GOPATH/src 目录下。在工程经过 go build、go install 或 go get 等指令后,会将产生的二进制可执行文件放在 $GOPATH/bin 目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下
log 包中提供了三类日志输出接口,Print、Fatal 和 Panic。
- Print 是普通输出;
- Fatal 是在执行完 Print 后,执行 os.Exit(1);
- Panic 是在执行完 Print 后调用 panic() 方法
单例模式构建
// 使用init构建,也线程安全
var cfg *config
func init() {
cfg = new(config)
}
// 全局变量
var cfg *config = new(config)
// 双重检查上锁,注意instance需要判断两次nil
func GetInstance() *Tool {
if instance == nil {
lock.Lock()
if instance == nil {
instance = new(Tool)
}
lock.Unlock()
}
return instance
}
// sync.Once
var instance *Tool
func GetInstance() *Tool {
once.Do(func() {
instance = new(Tool)
})
return instance
}
// sync.Once本质,和双重检查一样
func (o *Once) Do(f func()) {
// 判断是否执行过该方法,如果执行过则不执行
if atomic.LoadUint32(&o.done) == 1 {
return
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
// 进行加锁,再做一次判断,如果没有执行,则进行标志已经扫行并调用该方法
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
定时器
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i) //每秒都会执行的任务
}
执行go get
命令,在下载依赖包的同时还可以指定依赖包的版本。
- 运行
go get -u
命令会将项目中的包升级到最新的次要版本或者修订版本; - 运行
go get -u=patch
命令会将项目中的包升级到最新的修订版本; - 运行
go get [包名]@[版本号]
命令会下载对应包的指定版本或者将对应包升级到指定的版本。
提示:go get [包名]@[版本号]
命令中版本号可以是 x.y.z 的形式,例如 go get foo@v1.2.3,也可以是 git 上的分支或 tag,例如 go get foo@master,还可以是 git 提交时的哈希值,例如 go get foo@e3702bed2。