go学习笔记

#

go 由包组织(即一个文件夹,文件夹的名字对应包的名字,文件夹可以包含一个多个 go 源文件),每个源文件用packge 包名在开头声明
main 包:定义一个独立可执行程序,总是程序开始的地方

包的导入
通过import 包名import (多个包名)*导入包
包名重复需要指定别名避免重复import 指定名 包名
空导入import _ 包名,主要想使用包中的func init(){}函数
包导入时是搜索到包的源码,将其编译为.a文件,放在临时目录里,然后链接在一起
注意:其实import 后跟的并不是包名,而是包导入路径,但是一般约定路径最后一段(目录名)和包名一致,如果不一致也最好在导入时显示表示

包的初始化
可以声明 func init(){}函数,不能被调用和引用,程序启动时自动执行

声明#

命名采用驼峰,包名总是小写,名字大写意味着包外可以访问

Copy
var 变量 var 名字 类型=表达式

类型和表达式只能省略一个,省略类型,则由表达式推断得,省略表达式,则赋予对应类型的初始值(接口引用等初始值为 nil)
短变量声明(一般用于局部变量中不重要的变量)名字:=表达式

命名规则
img

赋值#

可以多重赋值

Copy
i,j=1,2 i,j=j,i//交换i与j的值

控制逻辑#

尽量遵循:出现错误就返回。成功逻辑不要放到if else中

循环#

Copy
for initialization;condition;post{ } //其中三个部分都可以省略

for range的坑

  • i是重用的,不是每次迭代都重新创建,range遍历的是a的副本,除非使用&a或切片a[:]
  • break 跳出的是所在的for select switch层,go中break continue可以后接标签
Copy
for i := range a{ // a是数组 }

switch#

不像c等是顺序执行,default可以在任意顺序,switch可以有无标签的形式

Copy
func r() int{ return 0 } func main() { switch r(){ case 2: fmt.Println(2) default: fmt.Println(1) case 0: fmt.Println(0) } a:=10 switch { case a<10: fmt.Println("小于") case a>10: fmt.Println("大于") default: fmt.Println("等于") } }

输入输出#

抽象数据实例的读写gob包,另外还有xml包,json包等
文本字符串读写fmt.Fsacn fmt.Fprint
定长表示抽象数据的二进制读写binary.Read binary.Write
带缓冲区的读写bufio
带压缩数据的读写compress/gzip包

JSON#

json.Marshal与json.Unmarshal负责转为json格式和转回,json.MarshalIndent可以输出格式化的json,第二个参数为自定义的前缀,第三个为自定义的缩进
能转为 json 的必须首字母大写,如下例的secert首字母小写,转化的data就不包含他
可以定义一些标签,用原生字符串表示,表示转化为json时,对应的key值替换为指定的,如data中的name被替换为nickname,其中omitempty 表示如果原始数据没指定这个值,用零值表示,如小王中没指定Age为多少,在data中被指定为为0
在转回时,定义的数据的变量名首字母需要大写,他会自动匹配数据中的首字母小写的,如data中为nickname,nameLIst为Nickname,两者会匹配
在转化时json.Unmarshal会自动丢弃不需要的数据

Copy
type people struct{ Name string `json:"nickname"` Age int `json:"Age,omitempty"` Hobby []string secret string } var peoples=[]people{ {Name:"小明",Age:12,Hobby:[]string{"画画","唱歌"},secret:"哈哈"}, {Name:"小王",Hobby:[]string{"学习"},secret:"爱吃……"}, {Name:"小白",Age:12,Hobby:[]string{"打篮球"}}, } //转为json //data,err:=json.Marshal(peoples) data,err:=json.MarshalIndent(peoples,""," ") if err!=nil{ fmt.Printf("转换json失败%s",err) }else{ fmt.Printf("%s\n",data) } //转回,只取nickname var nameList []struct{Nickname string} if err:=json.Unmarshal(data,&nameList);err!=nil{ fmt.Printf("转换json失败%s",err) }else{ fmt.Printf("%s",nameList) } [ // { // "nickname": "小明", // "Age": 12, // "Hobby": [ // "画画", // "唱歌" // ] // }, // { // "nickname": "小王", // "Hobby": [ // "学习" // ] // }, // { // "nickname": "小白", // "Age": 12, // "Hobby": [ // "打篮球" // ] // } //] //[{小明} {小王} {小白}]

模板#

以"text/template"为例

Copy
type Pet struct{ Nickname string Kind string } type people struct{ Name string `json:"nickname"` Age int `json:"Age,omitempty"` Pets []Pet } p:=people{Name:"小明",Age:12,Pets:[]Pet{Pet{"小花","猫"},Pet{"小黄","狗"}}} //通过原生字符串定义一个模板,模板中Age后的|表示将数据交给year2Month处理,其中range .Pets表示循环展开.Pets const temp=`-------分割线---------------------------- 我叫{{.Name}},今年{{.Age| year2Month}}个月了 我养的宠物有: {{range .Pets}} {{.Nickname}},它是个{{.Kind}} {{end}}` //创建模板,然后添加其中要处理的函数 out,err:=template.New("temp"). Funcs(template.FuncMap{"year2Month": year2Month}). Parse(temp) if err!=nil{ fmt.Println("出错了1") } //执行这个模板,格式化数据 if err:=out.Execute(os.Stdout,p);err!=nil{ fmt.Println("出错了") } //-------分割线---------------------------- //我叫小明,今年144个月了 //我养的宠物有: // //小花,它是个猫 // //小黄,它是个狗

文件读取和写入#

Copy
func main() { //从窗口标准输入读到要写入的文件名 outfilename:="" fmt.Scan(&outfilename) ioutil.WriteFile(outfilename,[]byte("***"),7777) //读取文件中内容 data,err:=os.ReadFile(outfilename) if err==nil{ fmt.Printf("%s",data) } //从命令行参数读取文件名,并统计每句话次数,最后输出 counts:=make(map[string]int)//键值对,键为string,值为int infilename:=os.Args[1]//读取命令行参数 f,err:=os.Open(infilename)//打开文件 if err==nil{//如果没有错误err为nil input:=bufio.NewScanner(f) for input.Scan(){ counts[input.Text()]++ } } f.Close() for line,n:=range counts{ fmt.Println("%f\t%s\n",n,line) } }

数据类型#

基础类型:数字,字符串,布尔
聚合类型:数组,结构体
引用类型:指针,slice,map,函数,通道
接口类型

整数#

有符号:int8 int16 int32 int64
无符号:uint8 uint16 uint32 uint64
另外有 int 和 uint,虽然其一般看做 int32,但是两者还是不同的类型
另外有 rune,其等于 int32,只是强调为 unicode 码点
另外有 byte8,其等于 int8,只是强调值为原始数据
另外有 unitptr,其为无符号整数,但大小不明确,用于底层

无符号整数一般只用于位运算和特定算术运算,极少用*表示非负值

进制:0开头 8进制,0x开头 16 进制

Copy
a:=07777 fmt.Printf("%d %o %x %X %#o %#x %#X",a,a,a,a,a,a,a)//#为输出前缀 //输出4095 7777 fff FFF 07777 0xfff 0XFFF

文字字符用单引号

Copy
a:='a' b:='啊' fmt.Printf("%c %c %q",a,b,b) //a 啊 '啊'

浮点数#

float32 float64

Copy
a:=2340.45678 fmt.Printf("%g %e %f %8.4f",a,a,a,a) //2340.45678 2.340457e+03 2340.456780 2340.4568

布尔值#

布尔值不能直接与 0,1 相互转换

字符串#

string也相当于一个“描述符”,是指向字符串的指针

len()得到字符串的字节数,不是长度
字符串不可变,所以 b 和 c 实际指向的是在同一地方的字符串"abandon"

Copy
a:="学习" fmt.Println(len(a)) b:="abandon" fmt.Println(b[0]) c:=b[:2] fmt.Println(c) fmt.Println(c+a+b) //6 //97 //ab //ab学习abandon

go 源文件总以 utf-8 编码,每个字符占 1-4 个字节,可以用码点值*表示 Unicode 字符,所以字符串就是码点序列
Unicode仅仅只是一个字符集,规定了符号对应的二进制代码,至于这个二进制代码如何存储则没有任何规定,utf-8等实现了这个规定

可以通过[]rune 将字符串转换为其对应的码点序列,并且转换回字符串

Copy
a:="abandon世界" b:=[]rune(a) fmt.Printf("%x\n",b) fmt.Println(string(b)) //[61 62 61 6e 64 6f 6e 4e16 754c] //abandon世界

通过 range 对字符串遍历,它隐式对字符串进行了解码

Copy
for i,r :=range a{ fmt.Printf("%d %c %x\n",i,r,r) } //0 a 61 //1 b 62 //2 a 61 //3 n 6e //4 d 64 //5 o 6f //6 n 6e //7 世 4e16 //10 界 754c

string可以与slice与byte相互转换,操作字符串的四个包
strings:字符串的搜索切割等
bytes:操作字符串 slice,及字符串增量构建
strconv:字符串与数字间的转换
unicode:判断字符串是数字及大小写转换等

原生字符串:用``反引号包裹,将完全输出字符串原*的格式

常量#

go 中的常量一般无类型的,便于保存高精度的值,可以隐式或显示转换为其它类型

常量定义

Copy
const a=0 const( e=2.71 pi=3.14 )

常量生成器

Copy
const( b=iota c d ) fmt.Println(b,c,d) //0 1 2

数组#

数组长度固定,slice 可长可短,一般不用数组
如果不同数组间元素可比较,且数组长度相同,那么数组可以直接比较

Copy
var a [2]int b:=[2]int{1,2} c:=[...]int{1,2} fmt.Println(a[0],b[0],c[0]) fmt.Println("%t",b==c)

slice切片#

切片是数组的“描述符”,切片的底层是数组,切片保存它指向的数组中元素的指针,长度和容量。长度是切片本身的长度,容量为指向元素到数组末尾的长度。
创建切片s := make([]int, 5)
数组的切片化u[low:high](u为一个数组)

go中数组变量表示整个数组,不是c中那样为指向数组的第一个元素。所以函数传递数组一般使用切片

切片是动态扩容的,但是使用append直到达到底层数组的容量界限,该切片就会与原数组解绑
使用append时,如果要扩容时会重新分配底层数组,并将原*元素复制

如下图所示,Q2与summer是months数组上的两个slice,其中len,cap的概念如图所示

img

slice 在初始化时可以不指定大小,也可以通过 make 生成,第一个参数是 slice 的 len 长度,第三个参数是 cap 容量

Copy
a:=make([]string,2,4) b:=[]int{1,2} c:=[5]int{1,2,3,4,5}//一个数组 d:=c[:3]//数组上的slice fmt.Println(b) fmt.Println(a) fmt.Println(d)

增加,复制与删除

Copy
func remove(slice []int ,i int)[] int{ copy(slice[i:],slice[i+1:])//将i之后的元素复制到i处 return slice[:len(slice)-1] } func main() { a:=[]int{1,2,3,4,5,6,7,8,9} a=append(a,10) fmt.Println(a) //删除第3个元素 a=remove(a,2) fmt.Println(a) } //[1 2 3 4 5 6 7 8 9 10] //[1 2 4 5 6 7 8 9 10]

map#

map 是对散列表的引用,map 的键必须是相同类型,值必须是相同类型
创建初始化,添加和删除

Copy
a:=map[int]string{1:"hello",2:"hi",3:"dog",4:"cat"} fmt.Println(a) b:=make(map[int]string) b[2]="2" b[1]="1" fmt.Println(b) delete(b,2) fmt.Println(b)

排序
按键值排序,需要使用 sort

Copy
a:=map[int]string{5:"mouse",1:"hello",2:"hi",3:"dog",4:"cat"} var keyInSort []int for key:=range a{ keyInSort=append(keyInSort,key) } sort.Ints(keyInSort) for _,key:=range keyInSort{ fmt.Println(a[key]) }

查找
查找 map 中是否有键,如果存在,value 为键值,ok 为 true,如果不存在 value 为类型对应的零值,ok 为 false

Copy
a := map[int]string{5: "mouse", 1: "hello", 2: "hi", 3: "dog", 4: "cat"} if value, ok := a[5]; !ok { fmt.Println("没有这个值") }else{ fmt.Println(value) }

结构体#

结构体定义和初始化

Copy
type people struct{ name string age int next *people } p:=people{name:"小明",age:10} fmt.Println(p.name)

结构体嵌套
结构体不能包含自己类型的结构体,但可以使用如上所示,使用指针
在嵌套其它类型是,可以使用匿名成员,这样直接通过.号就能访问嵌套的结构体的值,但是在初始化时会麻烦点

Copy
type point struct{ x int y int } type circle struct{ point radius int } c:=circle{point:point{x:2,y:3},radius:4} fmt.Println(c.x)

函数#

声明

Copy
func name(parameter-list)(result-list){ body }

形参收到的是每个实参的副本,所以修改形参不会影响调用者提供的实参,但是如果传入的是引用类型:指针,slice,map,函数,通道就会影响

返回值
返回值可以不指定变量名,但是如果指定了,在返回时只用写return

Copy
func year2Month(age int) (month int, day int){ month=age*12 day=age*356 return }

函数变量
函数变量可以赋给变量,或者从其它函数返回
注意作用域规则陷阱:在循环里创建的函数变量共享相同的变量

Copy
func add1(v int) int{return v+1} func eachAdd1(f func(v int)int,s []int){ for p,i:= range s{ s[p]=f(i) } } func main() { var f func(int) int//定义了函数变量f f=add1//用函数add1为f赋值 a:=[]int{1,2,3,4,5} eachAdd1(f,a)//函数变量作为参数传递到函数中 fmt.Println(a) }

匿名函数
匿名函数可以使用外层函数的变量(go支持的闭包特性)

Copy
func add1() func()int{ var x int return func()int{ x++ return x } } func main() { f:=add1() fmt.Println(f())//1 fmt.Println(f())//2 fmt.Println(f())//3 fmt.Println(f())//4 f=add1() fmt.Println(f())//1 fmt.Println(f())//2 }

延迟函数
在调用函数前加defer使得函数在return之后才才执行

Copy
func timeNow() { fmt.Println(time.Now()) } func main() { fmt.Println("程序开始") defer timeNow() fmt.Println("程序结束") } //程序开始 //程序结束 //2022-08-27 14:18:58.4305243 +0800 CST m=+0.001673801

在函数开始和结束时分别调用
可以用于输出调试信息,保存旧值并恢复等作用

Copy
func timeNow() func(){ start:=time.Now() fmt.Println("函数开始") return func(){ fmt.Println(time.Since(start)) fmt.Println("函数结束") } } func main() { defer timeNow()() time.Sleep(time.Second*10) } //函数开始 //10.0110625s //函数结束

变长参数函数

Copy
func s(in ...int) { }

在函数内in就相当于一个[]int类型
应用:在功能选项模式中使用

Copy
type Computer struct { CPU int GPU int Memory int } type Option func(*Computer) func NewComputer(option ...Option) *Computer { // 默认值 c:=&Computer{ CPU:0, GPU:0, Memory:0 } // 根据传入的操作修改配置 for _,op:=range option { option(h) } return c } // 一些配置操作 func optCPU(cpu int) Option { return func(c *Computer) { c.CPU = cpu } } //等等

方法#

只需要在函数名前加入类型定义,任何定义都可,但是不能是指针和接口类型(相当于类的成员函数)

Copy
type Point struct{X,Y int} func (p Point)show(){ fmt.Println(p.X,p.Y) } func main() { var p=Point{1,2} p.show() }

不能指针,不能的是本身是指针的类型

Copy
//错误 type Point *struct{X,Y int} func (i Point)show(){ } //正确 type Point struct{X,Y int} func (i *Point)show(){ }

方法本质是一个以方法所绑定的类型实例为第一个参数的函数
方法变量与方法表达式

Copy
type Point struct{X,Y int} func (p Point)sub(q Point) float64{ return math.Pow(float64(p.X-q.X), 2) + math.Pow(float64(p.Y-q.Y), 2) } func main() { var p=Point{1,1} var q=Point{4,5} //方法变量 f:=p.sub result:=f(q) fmt.Println(result) //方法表达式 e:=Point.sub result=e(p,q) fmt.Println(result) }

接口#

如果某个类型的方法的集合是接口类型的方法的集合的超集,则该类型实现了该接口
如,fmt有控制输出的机制,各类型可以实现这个接口*控制自己的输出,接口为

Copy
type Stringer interface{ Sting() string }

如Point类型如果输出的话,它默认输出{2 3}

Copy
type Point struct{X,Y int64} func main() { p:=Point{2,3} fmt.Println(p) }

我们自己为Point实现这个接口,控制它输出自己X+Y的和,这里输就会是5

Copy
type Point struct{X,Y int64} func (p Point) String() string{ return strconv.FormatInt(p.X+p.Y,10) } func main() { p:=Point{2,3} fmt.Println(p) }

接口可以组合形成新的接口,如Reader与Writer都是接口

Copy
type ReadWriter interface{ Reader Writer }

接口在运行时表示为eface(空接口)或iface(非空接口),两个类型都由指向实际数据的指针和类型信息结构组成
将值类型转为引用类型为装箱,将任意类型赋给接口也是装箱操作,该过程会复制一份原类型的数据

当函数处理未知类型数据时可以传入空接口,但尽量不要使用

嵌入#

垂直组合:
接口嵌入接口
结构体嵌入接口
结构体嵌入结构体(继承的实现)

水平组合:
使用接口作为水平组合的连接
接受接口类型的函数和方法
包裹函数
适配器

可以通过已有类型定义新类型
基于接口类型定义的新类型与原接口类型的方法集合是一致的
基于非接口类型定义的新类型不会得到原类型的方法集合

一些内置接口#

实现sort.Interface*排序,只要实现Len(返回序列长度),Less(定义比较方法),Swap(定义交换方法)三种方法

Copy
type Student struct{ name string grade int } type byName []*Student func (b byName)Len() int{ return len(b) } func(b byName)Less(i,j int)bool{ return b[i].name<b[j].name } func(b byName)Swap(i,j int){ b[i],b[j]=b[j],b[i] } type byGrade []*Student func (b byGrade)Len() int{ return len(b) } func(b byGrade)Less(i,j int)bool{ return b[i].name>=b[j].name } func(b byGrade)Swap(i,j int){ b[i],b[j]=b[j],b[i] } func main() { studentList:=[]*Student{ {"小明",23}, {"纳兰性德",70}, {"王子",63}, } //安照姓名排 sort.Sort(byName(studentList)) for i,s:=range studentList{ fmt.Printf("%v\t%-8v\t%v\t\n",i,s.name,s.grade) } //按照成绩排 sort.Sort(byGrade(studentList)) for i,s:=range studentList{ fmt.Printf("%v\t%-8v\t%v\t\n",i,s.name,s.grade) } //按照程序反排 sort.Sort(sort.Reverse(byGrade(studentList))) for i,s:=range studentList{ fmt.Printf("%v\t%-8v\t%v\t\n",i,s.name,s.grade) } }

类型断言#

express.(type)

用*识别错误,识别特性

并发同步#

goroutine#

go的并发线程并不是由操作系统线程*管理的,而是原生实现了goroutine,由Go在运行时负责调度,调度采用G-P-M模型
程序只有一个主goroutine*调用main函数,新的goroutine通过go语句创建

img

goroutine与操作系统线程区别:

  • goroutine栈可以增大缩小
  • 操作系统线程由时钟触发调度,需要上下文切换,goroutine由go语言结构触发调度,不需要切换到内核环境

服务端

Copy
func handleConn(c net.Conn){ defer c.Close() for{ _,err:=io.WriteString(c,time.Now().Format("15:04:05\n")) if err!=nil{ return } time.Sleep(1*time.Second) } } func main() { listener,err:=net.Listen("tcp","localhost:8000") if err!=nil{ log.Fatalln(err) } for{ conn,err:=listener.Accept() if err!=nil{ log.Println(err) continue } go handleConn(conn) } }

客户端

Copy
package main import ( "io" "log" "net" "os" ) func main(){ conn,err:=net.Dial("tcp","localhost:8000") if err!=nil{ log.Fatalln(err) } defer conn.Close() mustCopy(os.Stdout,conn) } func mustCopy(dst io.Writer,src io.Reader){ if _,err:=io.Copy(dst,src);err!=nil{ log.Fatalln(err) } }

通道#

传统的是基于共享内存的模型,go中还实现了基于CSP通信进程顺序模型的channel。CSP模型中,各个进程是独立运行的,它们通过发送和接收消息*进行通信。CSP模型的基本元素包括进程、通道和消息。共享内存的模型强调并发进程之间通过共享内存*进行通信和同步,需要采取合适的同步机制*保证数据的一致性和互斥访问,否则可能出现竞态条件

通道是两个goroutine之间的通信机制,每个通道有具体的类型,如chan intint 类型的通道
创建无缓冲通道(同步通道)ch:=make(chan int)//ch的类型为chan int
创建缓冲通道,容量为3ch:=make(chan int,3)
向通道发送数据xch<-x
从通道接收数据到x中x=<-ch
关闭通道close(ch)
获取通道容量cap(ch)
获取通道中元素个数len(ch)

只接收通道作为参数 <-chan
只发送通道作为参数 chan<-

Copy
ch1:=make(chan int) ch2:=make(chan int) go func(){ for x:=0;x<=100;x++ { ch1 <- x } close(ch1) }() go func(){ for x:=range ch1{ ch2<-x*x } close(ch2) }() for x:=range ch2{ fmt.Println(x) }

多路复用#

使用select对多个通道进行操作,channel与select结合可以实现超时机制,心跳机制,利用default避免阻塞

Copy
abort:=make(chan struct{}) go func(){ os.Stdin.Read(make([]byte,1)) abort<- struct {}{} }() tick:=time.Tick(1*time.Second) for{ select{ case<-tick: fmt.Println(<-tick) case<-abort: fmt.Println("结束") return default: //fmt.Println("等待中") } }

并发读取文件

Copy
var sema =make(chan struct{},20) func dirents(dir string) []os.FileInfo{ sema<-struct{}{} defer func(){<-sema}() entries,err:=ioutil.ReadDir(dir) if err!=nil{ return nil } return entries } func walkDir(dir string,n *sync.WaitGroup,filename chan string){ defer n.Done() for _,entry:=range dirents(dir){ if entry.IsDir(){ n.Add(1) go walkDir(filepath.Join(dir, entry.Name()),n,filename) }else{ filename<-entry.Name() } } } func main() { roots:=[]string{"D:\\"} var n sync.WaitGroup filename:=make(chan string) for _,root:=range roots{ n.Add(1) go walkDir(root,&n,filename) } go func() { n.Wait() close(filename) }() tick:=time.Tick(1*time.Second) var nfiles int64 loop: for { select { case <-tick: fmt.Printf("已读取文件数%d", nfiles) case name, ok := <-filename: if !ok { break loop } nfiles++ fmt.Println(name) } } }

聊天

Copy
type client chan<-string var( entering=make(chan client) leaving=make(chan client) message=make(chan string) ) func broadcaster() { clients:=make(map[client]bool) for{ select{ case msg:=<-message: for cli:=range clients{ cli<-msg } case cli:=<-entering: clients[cli]=true case cli:=<-leaving: delete(clients,cli) close(cli) } } } func handleConn(conn net.Conn){ ch:=make(chan string) go clientWriter(conn,ch) who:=conn.RemoteAddr().String() ch<-"你"+who+"已上线\n" message<-who+"已上线\n" entering<-ch input:=bufio.NewScanner(conn) for input.Scan(){ message<-who+": "+input.Text() } leaving<-ch message<-who+"已下线\n" conn.Close() } func clientWriter(conn net.Conn,ch<-chan string){ for msg:=range ch{ fmt.Fprintf(conn,msg) } } func main() { listener,err:=net.Listen("tcp","localhost:8000") if err!=nil{ fmt.Println(err) } go broadcaster() for{ conn,err:=listener.Accept() if err!=nil{ fmt.Println(err) continue } go handleConn(conn) } }
Copy
func main(){ conn,err:=net.Dial("tcp","localhost:8000") if err!=nil{ log.Fatalln(err) } go func() { io.Copy(os.Stdout, conn) log.Println("done") }() mustCopy(conn, os.Stdin) conn.Close() } func mustCopy(dst io.Writer,src io.Reader){ if _,err:=io.Copy(dst,src);err!=nil{ log.Fatalln(err) } }

互斥锁#

除了通道,go也有传统的锁,在sync包中实现
sync.Mutex,相当于一个容量为1的通道,通过获取令牌和释放令牌保证同一时间最多只有一个goroutine访问共享变量

Copy
var mu sync.Mutex mu.Lock() mu.RUnlock()

多读单写锁

Copy
var mu sync.RWMutex mu.RLock() mu.RUnlock()

一次性初始化锁
它不仅是一个互斥锁,还定义了一个布尔值,开始布尔值为假,第一次调用会将其设置为真,并且进行初始化操作

Copy
var loadIconsOnce sync.Once loadIconsOnce.Do(初始化函数)

条件变量 sync.Cond

sync.Once实现单例模式

并发的重复抑制的非阻塞缓存例子
pow是一个要记忆的函数,通过Memo可以保存每次函数f(这里也就是pow)的结果值,当之后有相同调用时不用运行函数,就可以直接从保存的结果中取出值

Copy
func pow(x int)(result int){ time.Sleep(time.Second*1) return x*x } type entry struct{ res int ready chan struct{} } type Func func(key int)int type Memo struct { f Func cache map[int] *entry mu sync.Mutex } func New(f Func) *Memo{ return &Memo{f:f,cache: make(map[int]*entry)} } func(memo *Memo) Get(key int)*entry{ memo.mu.Lock() e:=memo.cache[key] if e==nil{ e=&entry{ready:make(chan struct{})} memo.cache[key]=e memo.mu.Unlock() e.res=memo.f(key) close(e.ready) }else{ memo.mu.Unlock() <-e.ready } return memo.cache[key] } func main() { m:=New(pow) value:=[12]int{1,2,3,4,5,6,1,3,5,7,9,9} var n sync.WaitGroup for _,i:=range value{ n.Add(1) go func(j int){ start:=time.Now() res:=m.Get(j) fmt.Printf("%d %s %d\n",j,time.Since(start),res.res) n.Done() }(i) } n.Wait() }

在New时,相当于新建了一个Memo实例,并运行了server这个监管goroutine
在server中,对每个请求查询缓存,如果缓存命中,通过deliver中ready通道广播,如果没有命中,通过call得到结果,放入缓存,再通过deliver中ready通道广播
每一次执行函数,通过Get构造请求,等待server处理,最后得到server处理的结果

Copy
func pow(x int)(result int){ time.Sleep(time.Second*1) return x*x } type entry struct{ res int ready chan struct{} } type request struct{ key int response chan<- entry } type Func func(key int)int type Memo struct {requests chan request} func New(f Func) *Memo{ memo:=&Memo{requests: make(chan request)} go memo.server(f) return memo } func(memo *Memo) Get(key int)*entry{ response:=make(chan entry) memo.requests<-request{key,response} res:=<-response return &res } func(memo *Memo)server(f Func){ cache:=make(map[int]*entry) for req:=range memo.requests{ e:=cache[req.key] if e==nil{ e=&entry{ready:make(chan struct{})} cache[req.key]=e go e.call(f,req.key) } go e.deliver(req.response) } } func(e *entry)call(f Func,key int){ e.res=f(key) close(e.ready) } func(e *entry)deliver(response chan<-entry){ <-e.ready response<-*e } func main() { m:=New(pow) defer close(m.requests) value:=[12]int{1,2,3,4,5,6,1,3,5,7,9,9} var n sync.WaitGroup for _,i:=range value{ n.Add(1) go func(j int){ start:=time.Now() res:=m.Get(j) fmt.Printf("%d %s %d\n",j,time.Since(start),res.res) n.Done() }(i) } n.Wait() }

原子操作#

atomic包:主要同步一个整型变量或自定义类型变量,对临界区数据同步还是使用sync

并发模式#

创建模式

Copy
func f() chan T { c:=make(chan T) go func(){ }() return c } int main() { c := f() } //通过通道建立两个goroutine之间的联系

退出模式:分离模式,join模式,通知等待模式(可以通知goroutine退出,不是被动等待)

管道模式:扇出模式,扇入模式

超时取消模式:使用go context包,在超时或不需要时取消原有操作

错误处理#

构造错误值

Copy
err:= errors.New("error string") // 返回errors.errorString类型 err1 = fmt.Errorf("%d", i) errWrap = fmt.Errorf("%w", err) // 返回fmt.wrapError类型

透明错误处理策略:完全不关心错误的上下文,一有错误就立马进入唯一处理路径
哨兵模式:根据返回的错误信息,选择不同的路径(可以通过errors.Is判断错误值是否与一个error类型变量相同)
错误值类型检视策略:利用类型断言或类型选择判断错误值与自定义错误类型是否相同,或使用errors.As
错误行为特征检视策略:将错误类型共有行为抽象为接口,之后通过返回错误值是否为接口判断选择路径

网络编程#

网络IO模型

  1. 阻塞
  2. 非阻塞(之后通过轮询查询数据是否就位)
  3. IO多路复用(也是非阻塞,只不过不轮询,而是采用事件通知的方式,基于select/poll)
  4. 异步IO(上述同步方式等数据准备好后需要自己复制数据自己调用函数处理,而异步在数据准备好后将数据复制到用户空间,再调用函数处理)

tcp编程
服务端客户端读写

Copy
func handleConn(c net.Conn) { defer c.Close() for { buffer := make([]byte, 100) n, err := c.Read(buffer) if err != nil { fmt.Println("accept err:", err) return } fmt.Printf("read %d byte content is %s\n",n,string(buffer[:n])) } } func main() { l, err := net.Listen("tcp", ":9999") if err != nil { fmt.Println("listen err:", err) return } defer l.Close() for { c, err:= l.Accept() if err != nil { fmt.Println("accept err:", err) return } go handleConn(c) } }
Copy
func main() { c, err := net.Dial("tcp", ":9999") if err != nil { fmt.Println("listen err:", err) return } defer c.Close() c.Write([]byte("hello world")) }

http编程

Copy
func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world\n") }) http.ListenAndServe(":8888", nil) // https使用ListenAndServeTLS }

time包#

time.Now返回当前时间,由一个结构体time.Time,结构体包含挂钟时间,单调时间和时区
时间比较采用Equal
使用Before After比较时间先后关系
并能通过Sub运算返回time.Duration类型的纳秒值(基于这个函数还有Since与Until)
格式化输出采用助记符而不是类似“%Y-%M”

一次性定时器的创建,三种方法

Copy
func main() { _ = time.AfterFunc(1*time.Second, func() { fmt.Println("afterfunc timer") }) select { case <-time.After(2 * time.Second): fmt.Println("After timer") } Timer3 := time.NewTimer(3 * time.Second) select { case <-Timer3.C: fmt.Println("NewTimer timer") } }

Stop停止定时器
Reset重置定时器

信号处理#

收到信号三种处理方式:

  • 忽略信号
  • 执行系统默认处理
  • 执行自定义处理

服务端进程一般以守护进程运行,需要系统信号执行退出操作,但是如果采用系统默认退出,可能有一些尚未保存的信息丢失,所以采用自定义处理

使用os/signal包
func Notify(c chan<- os.Signal, sig ...os.Signal)
sig为想要捕获的信号
c为捕获后放入的channel,用于通知用户

img

go捕获的信号

  • 同步信号:一些bug引起的信号,需要转为panic立马处理
  • 异步信号:捕获后放入channel等待用户处理
  • 不能捕获的信号:由于操作系统限制不能捕获:SIGKILL SIGSTOP

http服务退出例子

Copy
func main() { var wg sync.WaitGroup http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "hello world\n") }) var srv = http.Server{ Addr:"localhost:8888", } wg.Add(2) srv.RegisterOnShutdown(func() { // 退出时清理资源 wg.Done() }) go func(){ quit:=make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGHUP) <-quit timeoutCtx, cf := context.WithTimeout(context.Background(), time.Second * 5) defer cf() var done = make(chan struct{}, 1) go func(){ if err:= srv.Shutdown(timeoutCtx); err!=nil { //Shutdown err } done<- struct{}{} wg.Done() }() select { case <-timeoutCtx.Done(): // timout case <- done: } }() err := srv.ListenAndServe() if err != nil { } wg.Wait() }

unsafe#

go是类型安全的,不支持隐式类型转换,不支持指针运算,但为了兼顾系统编程问题,使用unsafe
Sizeof获取类型大小
Alignof获取内存地址对齐系数
Offsetof获取结构体中字段的地址偏移量

安全使用模式

  1. *T1->unsafe.Pointer->*T2 需保证Alignof(T1)>=Alignof(T2)
Copy
f float64 *(*uint64)(unsafe.Pointer(&f))

reflect.SliceHeader与reflect.StringHeader必须以这种模式构建,否则sliceHeader.Data可能被GC

  1. unsafe.Pointer->uintptr
Copy
uintptr(unsafe.Pointer(&x))
  1. 模拟指针运算unsafe.Pointer(uintptr(unsafe.Pointer(&b)) + offset)
    这个转化过程需要在一个表达式中完成,否则b可能因为GC而失效
  2. 调用syscall.Syscall系列函数时指针类型到uintptr类型参数的转换
  3. 将reflect.Value.Pointer或reflect.Value.UnsafeAddr转换为指针

反射#

反射是程序在运行时访问检测和修改它本身状态或行为的能力,go的反射依赖于interface{}

img

通过ValueOf得到Value对象,可以得到实例的值信息
通过TypeOf得到Type对象,可以得到实例的类型信息

通过函数类型或包含方法的类型实例反射的Value对象,可以使用Call调用函数或方法

Value.Interface是ValueOf的逆过程

如果反射对象以值形式传递给ValueOf后,再对反射对象信息进行修改是无意义的,被禁止
可以通过CanSet CanAddr CanInterface判断反射对象是否可设置,可寻址,可恢复为interface{}

与c的互操作#

C代码直接以注释形式出现在go源码中
之后紧接着导入名为C的包
之后使用C包调用C代码中定义的函数

在go中链接外部C库
使用#cgo指示符

但是在go中使用c可能导致调用开销增加,线程量暴涨,失去跨平台交叉编译的能力

测试#

go原生支持测试,利用go test命令和testing包

包内测试(白盒)
测试代码与源代码在同一个包内,以*_test.go命名
优点:较高的测试覆盖率(因为可以访问包未导出符号)
缺点:需要随着源代码改变而经常改变,循环引用的问题

包外测试(黑盒)
测试代码放在*_test包中
缺点:只能测试包暴露的“API”
可以在待测试包中使用export_test.go在其中暴露一些符号,以供包外测试

测试代码组织模式:
平铺
xUnit家族
测试固件

go测试代码编写逻辑:给定输入,比较被测函数返回与预期是否一样,不一样则使用testing包输出错误信息,使用Errorf不会中断当前执行,使用Fatalf会中断当前执行

测试的外部数据统一放在testdata文件夹下

将输出数据写入.golden文件可以方便的对比数据处理前后差异

当在测试中依赖外部组件或服务时,可以使用以下几种概念模拟
fake:用函数模拟假的组件或服务
stub
mock

模糊测试:自动或半自动为程序提供随机数据,*检测程序是否有bug
使用go-fuzz
通过创建fuzz.go文件并编写Fuzz函数
如果要测试很多功能,可以在fuzztest中建立多个测试单元,每个单元包含存放语料的目录corpus,包含Fuzz函数的fuzz.go,手工生成初始语料的目录gen和之下的代码main.go

性能测试
可以在*test.go中通过编写Benchamark*的函数测试
测试可以采用串行或并行执行的方式
性能比较工具 benchstat

对性能瓶颈剖析
使用pprof expvar

go 调试工具Delve

go 陷阱#

短变量
同一个代码块中,使用多变量的短变量声明语句重新声明已经声明过的变量时,只会对其重新赋值

Copy
var a int = 5 a, b := 10, 20 //Yes

在不同代码块层次使用多变量的短声明会带*变量遮蔽问题

nil
零值可用,不需要初始化,声明后就可使用

Copy
sync.Mutex bytes.Buffer var strs []string = nil strs = append(strs, "hello")

值为nil的接口类型不总是等于nil
因为接口包含两部分,一部分为类型信息,一部分为值信息

string的零值是"",判断字符串为空是将len(s) 与0比较

for range
对字符串遍历得到的是各字符的码点
对map遍历是随机的
是在副本上遍历
迭代变量是重用的

Copy
for v:= range a { go func(){ //v是一样的 }() } //解决 for v:= range a { go func(v){ //v是一样的 }(v) }

切片
新切片与原切片共享底层存储,如果原切片占用较大内存,新切片的存在又使原切片内存无法释放
使用copy建立独立存储空间

切片支持自动扩容,一旦扩容,新切片与原切片不会共用同一个底层存储

goroutine
所有goroutine都会随着main goroutine退出而退出
如果任何goroutine出现panic,如果没有及时捕获,整个程序都退出,所以采用

Copy
defer func(){ if e := recover(); e != nil { fmt.println(e) } }()

go中break会跳出最内层的switch select for代码块

net/http
需要手动关闭resp.Body

Copy
resp, err := http.Get(url) defer resp.Body.Close()

这样http客户端才会重用带有keep-alive的http连接

go的垃圾回收机制#

https://www.cnblogs.com/cxy2020/p/16321884.html

Golang v1.3之前采用传统采取标记-清除法,需要STW,暂停整个程序的运行。
在v1.5版本中,引入了三色标记法和插入写屏障机制,其中插入写屏障机制只在堆内存中生效。但在标记过程中,最后需要对栈进行STW。
在v1.8版本中结合删除写屏障机制,推出了混合屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率

posted @   启林O_o  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 上周热点回顾(2.17-2.23)
点击右上角即可分享
微信分享提示
CONTENTS