Go语言笔记

go语言官方文档 https://golang.google.cn/doc/

go tool
go tool compile --help

go语言的基本哲学理念:

不要通过共享内存来通信,二是通过通信来共享内存
通过共享内存来通信,则并发时需要对临界区域加锁;通过通信来共享内存则只需要保证通信管道的并发安全,即可保证系统的并发安全;

占位符 https://www.cnblogs.com/jxd283465/p/11765201.html

占位符     说明                           举例                   输出
%v      相应值的[默认格式]。            Printf("%v", people)   {zhangsan},
%+v     打印结构体时,会[添加字段名]      Printf("%+v", people)  {Name:zhangsan}
%#v     相应值的Go语法表示            Printf("#v", people)   main.Human{Name:"zhangsan"}
%T      相应值的[类型]的Go语法表示        Printf("%T", people)   main.Human
%%      字面上的百分号,并非值的占位符    Printf("%%")            %
%t      Bool占位符 true 或 false。      Printf("%t", true)       true

初始化与init() 函数

https://zhuanlan.zhihu.com/p/34211611

  1. init函数在程序运行前注册,实现sync.Once的功能

  2. init函数先于main函数之前执行,且不能被其他函数调用

  3. 不同包内的init函数按照导入的依赖关系自动决定执行顺序

  4. 各个文件的初始化顺序为:各个包内的变量初始化 -> 各个包的init函数 (按照依赖顺序)-> main()

  5. 同一个包和每个源文件都可以有多个init函数,包内的init函数执行顺序没有明确定义,不应依赖多个init的执行顺序。

import _ "net/http/pprof"
import . "fmt"
import f "fmt"

别名导入:
golang对没有使用的导入包会编译报错,但是有时我们只想调用该包的init函数,不使用包导出的变量或者方法,这时就采用下划线 _ 导入
如果我们希望使用时可以省略包名fmt,使用 . 导入

go build : 在当前目录编译生成可执行文件

go install : 用来安装可执行文件,然后完成 go build 功能,生成的exe文件放在【$GOPATH/bin】目录下;go install 的包需要指定版本号

go get: 将远程代码下载安装到【$GOPATH/src】 目录下,高版本不再支持安装二进制(就是不再生成exe文件,比如protoc-gen-go);向当前目录的go.mod追加依赖项;

go 安装入门:

  1. 下载最新go 安装包:https://studygolang.com/dl
  2. 配置环境变量 GOPATH 指定路径用于安装 go 依赖包
  3. 命令行输入go env 查看是否安装成功

go 的接口与内存分布
https://www.cnblogs.com/f-ck-need-u/p/9940845.html

golang RoadMap
https://www.golangroadmap.com/class/sql/2.html

查看引用程序依赖库

ldd /path/to/file

有缓存的 channel 与无缓存的 channel

ch1 := make(chan int) // 无缓存的channel
ch2 := make(chan int, 10) // 有缓存的channel,容量为10个int

无缓存的channel 只有当 receive 准备好之后,send 处才会执行,否则会阻塞住。

go语言 switch 与 select

switch 不需要写 break,默认会自动break。如果需要强制执行下一个case, 使用 fallthrough。switch 的 case 可以是函数或表达式,只要所有case类型相同即可。

select 是随机选择一个可运行的case,如果没有case可运行,就会阻塞。如果加入default语句,default总是可运行的。case语句必须是对channel的IO操作,可以是读也可以是写。select主要用于处理并发编程中通道之间异步通信的问题。

select 中的case条件是并发执行的,select会选在先操作成功的条件去执行,如果多个同时返回,会随机选择一个。空的select{}会引起死锁。可以使用time.After(1*time.Second)做一个超时的case取代default子句。

golang 新手避坑
https://www.cnblogs.com/276815076/p/8583589.html

【Golang 快速入门】基础语法 + 面向对象
【Golang 快速入门】高级语法:反射 + 并发

go语言关键数据结构

数组:x := [3]int{1,2,3} 定长为数组,作为函数参数传递时,采用值传递。

slice:x := []int{1,2,3} 数组的切片,是指向数组的指针,作为函数参数传递时,采用引用传递。可以通过 append 方法进行自动扩容,append 方法会返回一个新地址。注意避坑,当 append 数据不超过切片的容量时,会修改同一个切片内的内容,扩容后,两个切片不再引用同一片地址。

x := [6]int{1, 2, 3, 4, 5, 6}
y := []int{7, 8, 9} // 当 s1+y 长度不超过x, append会直接覆盖x内的值,x与s2还是同一个地址
// y := []int{7,8,9,10} // 当 s1+y 长度超过x,append会扩容,x与s2 地址不同,且x还是原始值。
s1 := x[0:3]
s2 := append(s1, y...)

fmt.Println(x)
fmt.Println(s2)

fmt.Printf("%p\n", &x)
fmt.Printf("%p\n", s2)

map:链地址法hash表 + 8元素bucket, 渐进式rehash过程。
map的range遍历过程,多次调用的结果是不同的。因为内部做了随机处理。
map的遍历过程是通过遍历桶实现的,因此遍历时可以删除或增加元素不会报错。但是遍历的过程不是每次都相同的,如果遍历的过程中数据数据写入未遍历过的桶,就会在本次遍历过程中访问到。如果加入的是已经遍历过的桶,就不会在本次遍历过程中访问到。因此写入和删除时遍历map的结果数量是不确定的。

由于 syncMap内部存储都是k,v interface{},可以考虑利用反射提供类型检查:

type ConcurrentMap struct {
    m         sync.Map
    keyType   reflect.Type
    valueType reflect.Type
}

func NewConcurrentMap(keyType, valueType reflect.Type) (*ConcurrentMap, error) {
    if keyType == nil {
        return nil, errors.New("nil key type")
    }
    if !keyType.Comparable() {
        return nil, fmt.Errorf("incomparable key type: %s", keyType)
    }
    if valueType == nil {
        return nil, errors.New("nil value type")
    }
    cMap := &ConcurrentMap{
        keyType:   keyType,
        valueType: valueType,
    }
    return cMap, nil
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
    if reflect.TypeOf(key) != cMap.keyType {
        return
    }
    cMap.m.Delete(key)
}

chan:channel 内部维护了一个互斥锁,来实现线程安全

rune: 用来区分字符值和整数值的结构,rune的实质是int32的别名,但在go语言内作为字符单位用来处理字符串。相当于go语言的char。

go默认字符编码为UTF-8,UTF-8为可变长度编码,每个中文一般占 3 字节。因此字符串修改需要转换为[]rune处理, 不可以直接对string进行slice切片。

使用range 遍历字符串是按照rune正确地迭代,使用for-i遍历字符串是按照字节遍历的。

ss := "go算法"
fmt.Printf("%v len:%v\n", ss, len(ss)) // go算法 len:8
rss := []rune(ss)
fmt.Printf("%v len:%v\n", rss, len(rss)) // [103 111 31639 27861] len:4
rss[0] = '我'
ss = string(rss)
fmt.Printf("%v len:%v\n", ss, len(ss)) // 我o算法 len:10

range 返回的是值拷贝

range 返回的第二个参数是后面迭代对象内的值拷贝,且迭代时原地拷贝复用空间(不改变对象地址)

基本类型:
go 语言中,int在32位机器上占4个字节,在64位机器上占8个字节。i := 32 在64位机器上的类型为int64

通过math.MaxInt可以获得对应的int范围最大值,其他的系统函数strconv.Itoa strconv.Atoi 默认使用的都是int类型,返回int64类型

m := make(map[string]*student)
stus := []student{
    {name: "aaa", age: 18},
    {name: "bbb", age: 23},
    {name: "ccc", age: 28},
}
for _, stu := range stus {
    // stu 是一个临时变量,只会分配一次空间,因此取地址是固定的,指向它自己的空间。
    // 后面的值直接 值拷贝覆盖 stu空间的值,所以最终stu的值是最后一次迭代的对象的值。
    m[stu.name] = &stu // aaa/bbb/ccc --> ccc 错误的迭代
    m[stu.name] = stu[i] // 正确的迭代。虽然还是产生了值拷贝;更好的方式是将stus声明为指针数组,这样遍历时只会产生指针拷贝
}

成员变量的大小写问题:

Go提倡成员变量首字母大写,首字母大写被视为public,外部包可以访问。首字母小写的成员变量视为private, 外部包无法访问, 且进行json,xml, gob等序列化时会被忽略。

main() 主程序退出时 未执行完的 goroutine 会被强行中断导致任务没有正常结束

可使用 sync.WaitGroup 等待所有协程执行完之后再结束main

var wg sync.WaitGroup
wg.Add(1)
go func(pwg *sync.WaitGroup) {
    defer pwg.Done()
    time.Sleep(1 * time.Second)
    fmt.Println("go end")
}(&wg)
wg.Wait()
fmt.Println("main end")

go 的反射包 reflect
用来比较无法使用==的变量,采用值相等的比较方式

reflect.DeepEqual(a,b)

new(T) 与 make(T)

new(T) 和 make(T) 都在堆上分配内内存

new(T) 为每个类型T分配内存,并返回*T 的指针,适用于值类型的结构体,相当于 &T{}
make(T) 返回一个类型为T的初始值,它仅适用于go内部的引用类型:slice, map, channel

p := new([]int) // *p == nil; with len and cap 0
fmt.Println(p) // &[]

v := make([]int, 10, 50) // v is initialed with len 10, cap 50
fmt.Println(v) // [0 0 0 0 0 0 0 0 0 0]

interface 与 struct

interface是由两个部分组成,[动态类型][动态值]

func myFunc(arg interface{}){
    t: = arg.(type) // 获arg的动态类型
    v, ok := arg.(string) // 类型断言,检查arg是否是指定的string类型
}

interface 指向的对象天生是指针传递,map,slice,channel本质都是引用类型,map采用值传递取值也是无法寻址的

m := map[string]Data{"x": {"one"}}
// m["x"].Name = "two" // cannot assign to struct field m["x"].Name in map
v := m["x"]
v.Name = "two" // m["x"]={one}
m["x"] = v     // m["x"]={two}

通过Go的强制类型转换,在编译期检查接口是否实现。

type MoveInterface interface{
    Move()
}
type StructName struct{

}

func (s *CatStruct) Move(){    
}

// 因为interface 是指针类型,因此要转成对应结构体的指针
// 强制赋值以检查对象是否实现指定接口
var _ MoveInterface  = (*CatStruct)(nil)

关于定义别名:

type MyLocker sync.Locker
type MyMutex sync.Mutex
type MyMutex2 struct{
    sync.Mutex
}

由于sync.Locker 为 interface 类型,因此MyLocker 继承其Lock()和Unlock()方法
sync.Mutex 为 struct 类型,MyMutex 没有保留方法集。而使用匿名字段组合的 MyMutex2 拥有 sync.Mutex 的方法集。

defer 返回时执行语句

defer 是在声明它的函数结束并返回时执行。
defer 函数在声明时求值,而不是在调用时求值。如果defer使用了闭包,闭包引用了外部变量,则是在调用时求值。
多个 defer 语句,最后定义的先执行。
defer 中可以对返回值进行重新赋值。
函数发生panic时,只能保证当前goroutine中的 defer 一定会执行,其他goroutine的defer代码不保证。

断言语句失败时,会返回目标类型的“零值”,因此断言时不要使用已定义默认值的变量,会被覆盖为“零值”。

golang 并发包atomic

golang 并发 atomic
atomic 结构体提供的原子方法Load...,Store...,Add...,Swap...,CompareAndSwap...(CAS),Value

如果一个操作不涉及资源竞争,只涉及状态的读写判定,则不需要使用复杂的互斥锁Mutex,只需要使用 atomic这种基于CPU指令的原语。
atomic比Mutex的效率高是因为 atomic 是通过 cpu原子指令修改内存,并通过内存屏障保证数据的可见性。不涉及内核的上下文切换。
而Mutex的使用涉及多次atomic原子操作以及用户态和内核态的线程切换,还有线程调度的开销。

Go的数据结构与算法库

gods 实现了Java常用的数据结构

go-datastructures 高性能与线程安全的GO数据结构

go 数据结构与算法推荐

LeetCode-Go


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 using1174@foxmail.com

文章标题: Go语言笔记

文章字数: 2,957

本文作者: Jun

发布时间: 2022-04-02, 18:14:00

最后更新: 2022-05-16, 20:33:07

原始链接: http://yoursite.com/2022/04/02/Go语言笔记/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录
×

喜欢就点赞,疼爱就打赏