Skip to main content

[Golang] 学习笔记

指针

指针理解

要明白指针,需要知道几个概念:指针地址、指针类型、指针取值

在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作)

  • & 取地址操作
  • * 取值操作

变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。

str := "hello world"

// 对字符串取地址, ptr类型为*string
ptr := &str // ptr为指针变量
fmt.Printf("ptr typr is : %T\n ", ptr) // ptr typr is : *string

// 打印ptr的指针地址
fmt.Printf("address: %p\n", ptr) // address: 0x14000112020

// 对指针进行取值操作
value := *ptr
fmt.Printf("value type: %T\n", value) // value type: string

// 指针取值后就是指向变量的值
fmt.Printf("value: %s\n", value) // value: hello world

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值
x := 1
p := &x // 取地址操作
fmt.Printf("type is : %T", p) // 指针变量 type is : *int
fmt.Println(*p) // 取值操作 "1"
*p = 2 // 修改指针指向的值
fmt.Println(x) // "2"

函数、闭包、递归

匿名函数

// 具名函数
func Add(a, b int) int {
return a+b
}

// 匿名函数
var Add = func(a, b int) int {
return a+b
}

匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送

package main

func main() {
// --- function variable ---
fn := func() { println("Hello, World!") }
fn()

// --- function collection ---
fns := [](func(x int) int){
func(x int) int { return x + 1 },
func(x int) int { return x + 2 },
}
println(fns[0](100))

// --- function as field ---
d := struct {
fn func() string
}{
fn: func() string { return "Hello, World!" },
}
println(d.fn())

// --- channel of function ---
fc := make(chan func() string, 2)
fc <- func() string { return "Hello, World!" }
println((<-fc)())
}

defer

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。

作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

如果一个函数中有多个defer语句,它们会以**LIFO(后进先出)**的顺序执行。

func Demo(){
defer fmt.Println("2")
defer fmt.Println("3")
defer fmt.Println("4")
}
func main() {
Demo()
}
// 4
// 3
// 2

recover错误拦截

Go语言提供了专用于“拦截”运行时panic的内建函数“recover”。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权

recover只有在defer调用的函数中有效

package main

import "fmt"

func Demo(i int) {
//定义10个元素的数组
var arr [10]int
//错误拦截要在产生错误前设置
defer func() {
//设置recover拦截错误信息
err := recover()
//产生panic异常 打印错误信息
if err != nil {
fmt.Println(err)
}
}()
//数组角标越界异常
arr[12] = 10
}

func main() {
Demo(12)
fmt.Println("产生错误 程序继续执行...")
}
// runtime error: index out of range
// 产生错误 程序继续执行...

defer 语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量 v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。闭包复制的是原对象指针,这就很容易解释延迟引用现象

func main() {
for i := 0; i < 3; i++ {
defer func(){ println(i) } ()
}
}
// Output:
// 3
// 3
// 3

因为是闭包,在 for 迭代语句中,每个 defer 语句延迟执行的函数引用的都是同一个 i 迭代变量,在循环结束后这个变量的值为 3,因此最终输出的都是3。

// 修复
func main() {
for i := 0; i < 3; i++ {
i := i // 定义一个循环体内局部变量 i
defer func(){ println(i) } ()
}
}
func main() {
for i := 0; i < 3; i++ {
// 通过函数传入 i
// defer 语句会马上对调用参数求值
defer func(i int){ println(i) } (i)
}
}

一般来说,在 for 循环内部执行 defer 语句并不是一个好的习惯

闭包Closure

TODO

数组

数组是一个由固定长度的特定类型元素组成的序列

定义方式:

var a [3]int                    // 定义长度为 3 的 int 型数组, 元素全部为 0
var b = [...]int{1, 2, 3} // 定义长度为 3 的 int 型数组, 元素为 1, 2, 3
var c = [...]int{2: 3, 1: 2} // 定义长度为 3 的 int 型数组, 元素为 0, 2, 3
var d = [...]int{1, 2, 4: 5, 6} // 定义长度为 6 的 int 型数组, 元素为 1, 2, 0, 0, 5, 6
// 4: 5 代表 index: value; index为4的时候,value为5

Go 语言中数组是值语义。

为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。

数组的长度是数组类型的组成部分,不同长度数组的数组指针类型也是完全不同的

var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针

fmt.Println(a[0], a[1]) // 打印数组的前 2 个元素
fmt.Println(b[0], b[1]) // 通过数组指针访问数组元素的方式和数组类似

for i, v := range b { // 通过数组指针迭代数组的元素
fmt.Println(i, v)
}

切片 Slice

切片就是一种动态数组

切片的结构定义,reflect.SliceHeader:

type SliceHeader struct {
Data uintptr
Len int
Cap int
}

定义切片的方式:

var (
a []int // nil 切片, 和 nil 相等, 一般用来表示一个不存在的切片
b = []int{} // 空切片, 和 nil 不相等, 一般用来表示一个空的集合
c = []int{1, 2, 3} // 有 3 个元素的切片, len 和 cap 都为 3
// :2 从index 0到1 的位置截取c, 但d仍指向c的地址,仍继承c的cap
d = c[:2] // 有 2 个元素的切片, len 为 2, cap 为 3
e = c[0:2:cap(c)] // 有 2 个元素的切片, len 为 2, cap 为 3
f = c[:0] // 有 0 个元素的切片, len 为 0, cap 为 3
g = make([]int, 3) // 有 3 个元素的切片, len 和 cap 都为 3
h = make([]int, 2, 3) // 有 2 个元素的切片, len 为 2, cap 为 3
i = make([]int, 0, 3) // 有 0 个元素的切片, len 为 0, cap 为 3
)
/*  a[:x:y] 即 a[0:x:y]
a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x
*/
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
d1 := slice[6:8]
fmt.Println(d1, len(d1), cap(d1))
d2 := slice[:6:8]
fmt.Println(d2, len(d2), cap(d2))
/* [6 7] 2 4
[0 1 2 3 4 5] 6 8
*/

新切片的容量:

写这个的原因是刚开始学的时候没弄清楚新切片的容量规则,太笨了…

a := []int{2, 3, 5, 7}
lenth := len(a)
for i := 0; i < lenth; i++ {
n := a[i:lenth] // 新切片
fmt.Printf("when index is: %v, new slice cap is :%v, len is : %v, slice is: %v \n", i, cap(n), len(n), n)
}
// n:=a[0:4] when index is: 0, new slice cap is :4, len is :4, slice is: [2 3 5 7]
// n:=a[1:4] when index is: 1, new slice cap is :3, len is :3, slice is: [3 5 7]
// n:=a[2:4] when index is: 2, new slice cap is :2, len is :2, slice is: [5 7]
// n:=a[3:4] when index is: 3, new slice cap is :1, len is :1, slice is: [7]

// n:=a[0:3] when index is: 0, new slice cap is :4, len is :3, slice is: [2 3 5]
// n:=a[1:3] when index is: 1, new slice cap is :3, len is :2, slice is: [3 5]
// n:=a[2:3] when index is: 2, new slice cap is :2, len is :1, slice is: [5]
// n:=a[3:3] when index is: 3, new slice cap is :1, len is :0, slice is: []

// 可以看出新的切片 n[first_i:last_i],其容量为原切片容量值 减 first_i

切片中 Cap 成员表示切片指向的内存空间的最大容量(对应元素的个数,而不是字节数)

下图是 x := []int{2,3,5,7,11}y := x[1:3] 两个切片对应的内存结构。

slice

添加切片元素

内置的泛型函数 append 可以在切片的尾部追加 N 个元素:

var a []int
a = append(a, 1) // 追加 1 个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加 1 个切片, 切片需要解包

在容量不足的情况下,``append的操作会导致重新分配内存。即使容量足够,append` 函数的返回值会更新切片本身,因为新切片的长度发生变化。

append在切片的开头添加元素:

var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头添加 1 个元素 [0 1 2 3]
a = append([]int{-3,-2,-1}, a...) // 在开头添加 1 个切片 [-3 -2 -1 0 1 2 3]

在开头append一般都会导致内存的重新分配,而且会导致已有的元素全部复制 1 次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。

切片中间插入元素:

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 个位置插入切片

// 中间追加元素
s2 := []int{1, 2, 3}
s_x := []int{-1, -2, -3}
s2 = append(s2, s_x...)
copy(s2[1+len(s_x):], s2[1:])
copy(s2[1:], s_x)
fmt.Println(s2)

s3 := []int{5, 6, 7, 8}
s3 = append(s3, 4) // [5,6,7,8,4]
copy(s3[2:], s3[1:]) // [5,6,6,7,8] 把s3 index 为1 2 3的元素复制到s3 index为2 3 4的位置
s3[1] = 10
fmt.Println(s3) // [5,10,6,7,8]

删除切片元素

从尾部删除:

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部 1 个元素
a = a[:len(a)-N] // 删除尾部 N 个元素

从开头删除:

a = []int{1, 2, 3}
a = a[1:] // 删除开头 1 个元素
a = 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 个元素

String操作

// string底层就是一个byte的数组,因此也可以切片操作;但string是不可变的
str := "Hello world"
st := []byte(str) // 中文字符需使用[]rune(str)
st[6] = 'G'
st = st[:8]
st = append(st, '!')
str = string(st)
fmt.Println(str)
// Hello Go!

避免切片内存泄漏

删除切片元素时可能会遇到。假设==切片里存放的是指针对象,那么删除末尾的元素后,被删除的元素依然被切片底层数组引用==,从而导致不能及时被自动垃圾回收器回收(这要依赖回收器的实现方式):

var a []*int{ ... }
a = a[:len(a)-1] // 被删除的最后一个元素依然被引用, 可能导致 GC 操作被阻碍

保险的方式是先将需要自动内存回收的元素设置为 nil,保证自动回收器可以发现需要回收的对象,然后再进行切片的删除操作:

var a []*int{ ... }
a[len(a)-1] = nil // GC 回收最后一个元素内存
a = a[:len(a)-1] // 从切片删除最后一个元素

当然,如果切片存在的周期很短的话,可以不用刻意处理这个问题。因为如果切片本身已经可以被 GC 回收的话,切片对应的每个元素自然也就是可以被回收的了。

map

声明的时候初始化

m := map[string]int{
"alice": 31,
"charlie": 34,
"charlie1": 35,
}

删除操作

delete(m, "alice")

查找

if _, ok := m["charlie"]; ok {
fmt.Println(ok)
}

顺序循环map

map是无序的,如果想顺序循环KV,需要先对K排序

var keys []string
for key := range m {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
fmt.Printf("%s:\n %d\n", key, m[key])
}

map 值为slice类型

sliceMap := make(map[string][]string, 3)
sliceMap["k1"] = []string{"v1", "v2"}
sliceMap["k2"] = []string{"v3", "v4"}
sliceMap["k3"] = []string{"v5", "v6"}
fmt.Println(sliceMap)
// map[k1:[v1 v2] k2:[v3 v4] k3:[v5 v6]]

map 作为slice的值

slice := make([]map[string]string, 5) // slice cap为5,未赋值的元素则默认为nil -> map[]
slice[0] = make(map[string]string, 3)
slice[0]["name"] = "王五"
slice[0]["age"] = "18"
slice[0]["address"] = "上海"

// 直接赋值
slice[1] = map[string]string{
"name": "李四",
"age": "19",
"address": "北京",
}
fmt.Println(slice)
// [map[address:上海 age:18 name:王五] map[address:北京 age:19 name:李四] map[] map[] map[]]

结构体

TODO

cobra 构建命令行程序的框架

refer to Go 语言现代命令行框架 Cobra 详解

// https://github.com/spf13/cobra
// https://github.com/spf13/cobra-cli

gotests 使用

$ go get -u github.com/cweill/gotests/...

// add gopath/bin to zshrc
// cd path: Project_Go/ollama-gin/config
gotests -all -w config.go
// will generate all test func in new file named config_test.go