about Golang - chris-wangkk/myWiki GitHub Wiki

  1. 变量
    声明定义方式:
    1.var 声明 1 个或者多个变量
    var a string = “abc”
    var b, c int = 1, 2
    2.Go 可以自动推断已经初始化的变量类型(根据初始化的值推断变量类型)
    var c = true
    3.声明变量且没有给出对应的初始值时,变量将会初始化为零值(根据变量类型提供一个对应的零值)
    var d int
    4.:= 语句是申明并初始化变量的简写
    e := 123(等同于 var e int = 123)

#常量
支持字符、字符串、布尔和数值 常量
const 用于声明一个常量: const s string = “constant”

#循环
for 是 Go 中唯一的循环结构
三种方式:
1.单个循环
i := 1
for i <=3 {

}
2.经典方式
for i:=0; i<=3;i++{

}
3.持续方式
for {
… //直到在循环体内使用了 break 或者 return 来跳出循环
}
if/else 语句
switch 语句
传统:
switch x {
case 1:

case 2,3:

default:

}
不带表达式的 switch 是实现 if/else 逻辑的另一种方式
switch {
case 条件语句1:

}

#数组
一个固定长度的数列,元素的类型和长度(定长,一旦定义就不能变化)都是数组类型的一部分,且存储的元素类型是单一的
关于初始化:
var a 5int //数组默认是零值的[0 0 0 0 0]
b := 5int{1, 2, 3, 4, 5} //注意:[]中的5不加时就成了切片
常用:
内置函数len可得到数组长度
-—>
a:=5int{1,2,3,4,5}
b:=a //深复制(后续修改a时,对b没有影响)
故作为函数参数—>深复制,不会影响原有数据

#切片(go中重要数据类型)
仅由它所包含的元素决定(不像数组中还需要元素的个数)
关于初始化:
s1 := make([]string, 3) //使用内建方法make,创建了一个长度为3(初始化为零值)的 string 类型 slice
s2 := make([]string, 1, 3) //使用内建方法make,创建了一个长度为1(初始化为零值),同时容量为3的 string 类型 slice
注意:
1.长度值相当于当前切片的元素个数,可get或set
2.容量值相当于预分配的空间,但不能直接get或set(会认为越界)
3.长度值<=容量值:当长度值<容量值时,采用append方法不会新申请空间来存储,而是返回原先的存储地址;当长度值==容量值时,采用append方法会新申请空间来存储,此时返回的空间和原空间不是同一存储(相当于深复制)
常用:
1.内置函数len可得到切片长度,cap可得到切片容量
2.append在原有切片后添加1或多个元素,返回的切片是否是新的存储视原切片的容量而定
3.slice[low:high]—→切片操作,得到slice中的一部分(从low到high-1)—→slice[low:]得到slice中从low起始的剩余部分
4. for 索引号,元素值 := range 切片 {…} —>同数组(注意:这里得到的元素值是一个新的元素,不能影响切片本身(但如果元素是切片就另当别论))
-
—→
a:=[]int{1,2,3,4,5} //这种初始化方式len(a)==cap(a)
b:=a //浅复制
故作为函数参数—>浅复制,不会影响原有数据(但注意append的情况哦)
深复制方式:
c := make([]int, len(b))
copy(c, b)

#字典(关联数组)
关于初始化:
要创建一个空 map,需要使用内建的 make:make(map[key-type]val-type) 如 m1 := make(map[int]string)
另一种初始化方式:n := map[string]int{"foo": 1, “bar”: 2}
关于使用:
使用 make[key] = val 语法来设置键值对
使用 name[key] 来获取一个键的值——>
当从一个 map 中取值时,可选的第二返回值指示这个键是在这个 map 中 _,prs := m11
常用:
1.内置函数len可得到字典长度
2.delete 可以从一个 map 中移除键值对 delete(m, “k2”)
3.for 键,值 := range 切片 {…}——→注意:map是无序的,故每次遍历结果可能不同

#函数
##常规函数
func 函数名(参数表) (返回值表) {

}
go函数特点:
1.多返回值(有些返回值不想要就设置为 _)
2.变参函数(参数表写为…,如 nums …int)——→slice 已经有了多个值,想把它们作为变参使用,这样调用 func(slice…)
3.匿名函数(作为变量那样使用)
-—>
nextInt := intSeq() //intSeq()中返回一个匿名函数(个人觉得闭包的原理是基于go特有的变量空间分配和gc机制,只要数据被引用则不会被gc)
4.递归

#结构体里的方法
func(obj 结构体//接收器)方法名(参数表) (返回值表) {…} //这里接收器可以传值或传指,具体是看方法里接收器的定义(而不是看调用时用到的变量是否是指针,编译器会根据方法中的定义自动适配)。

#接口
方法特征的命名集合(类似接口类那样)与传统的C++等语言不同的是,采用的实现机制遵从鸭子定义,结构体类型只要实现了接口中的所有方法即可,无需存在继承关系(go中本身也没有特定的继承语法)

#错误处理
import “errors”
error接口
函数:
errors.New(*) //构造一个使用给定的错误信息的基本error 值
-→自定义错误类型(只需对应结构体类型实现 Error() string 接口即可)

#协程
并行的执行:用户态层面的执行单元,当多个协程并行执行时对操作系统是不感知的(操作系统的执行单元是线程)

#有关协程的辅助特性
1.通道
连接多个 Go 协程的管道
(1)创建一个新的通道
channel := make(chan val-type)
(2)发送 一个新的值到通道中(在没有缓冲的情况下(默认通道是 无缓冲 的)操作是阻塞,直到接收方准备完毕)
channel <-
(3)从通道中 接收 一个值(在没有缓冲的情况下(默认通道是 无缓冲 的)操作是阻塞,直到发送方准备完毕)
<- channel
-—→有缓冲的通道:channel := make(chan val-type, 缓存限定数量的值);缓存满了还是会阻塞
-—→利用阻塞的效果,可利用通道来同步协程间的状态
-—→可细化下通道的方向(只用来接收还是发送):
chan<- val-type:只用来发送的通道类型
<-chan val-type:只用来接收的通道类型
(4)通道的关闭
关闭一个通道意味着不能再向这个通道发送值(硬要发送会panic: send on closed channel)—>用来给这个通道的接收方传达工作已经完成的信息(通过 j, more := <-channel 中的more判断通道是否关闭,若关闭more的值为false;如果不看more去接收一个关闭的通道,则会收到通道里类型的默认值)
总结:
一般而言,由发送方来控制是否关闭通道;接收方则通过第二个参数来判断通道是否关闭(但可以接收关闭通道里缓冲的数据)
(5)通道遍历
for elem := range channel
若channel没有关闭,则循环中继续阻塞执行,等待接收
2.通道的应用封装
(1)定时器
import “time”
timer1 := time.NewTimer(time.Second * 2) //创建一个定时器,参数为定时时间
<-timer1.C //定时器通道 C(源码:C <-chan Time) 在定时器失效(即设置的时间)的值之前,将一直阻塞
vs time.Sleep(time.Second * 2)
同:都是会定时一段时间(阻塞一段时间)
异:在定时器失效之前,可以取消这个定时器:timer1.Stop() //调用定时器中的stop方法可取消定时器
(2)打点器(在固定的时间间隔重复执行,定时执行)
import “time”
ticker := time.NewTicker(time.Millisecond * 500) //创建一个打点器,参数为隔多久打一次
停止打点器:ticker.Stop()
3.各种骚操作
(1)通道选择器(同时等待多个通道操作)
配方:goroutine+channel+select
eg:
c1 := make(chan bool)
c2 := make(chan bool)
go func(){

c1<-true
}()
go func(){

c2<-true
}()
for …
select {
case <-c1:

case <-c2:

}
(2)超时处理(同时等待多个通道操作)——>实际直接采用context包来生成超时上下文来实现
配方:goroutine+channel+select+time包
eg:
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1): //增加超时机制,超过1秒则走给分支(超时处理)
fmt.Println(“timeout 1”)
}
(3)非阻塞通道操作(实现非阻塞 的发送、接收,甚至是非阻塞的多路 select)
配方:goroutine+channel+select
eg:
select {
case msg := <-messages:
fmt.Println(“received message”, msg)
default: //增加default分支,实现非阻塞效果
fmt.Println(“no message received”)
}
(4)工作池(通过创建n个协程来并行接收并处理请求)
工作协程(工作者,大部分工作为IO型),从channel-A中获取数据,处理完后发送到channel-B里;通过不停向channel-A中发送数据,使工作者动起来(只要空闲就获取并进行处理)。
(5)速率限制
配方:goroutine+channel+ticker
eg:
创建一个打点器
<1>limiter := time.Tick(time.Millisecond * 200)—→定时(200毫秒)通过limiter接收time.Time实例(当前时间)
<-limiter.C
<2>limiter := time.NewTicker(time.Millisecond * 200)—→定时(200毫秒)通过limiter.C接收time.Time实例(当前时间)
<-limiter
封装在一个goroutine中
go func() {
for t := range time.Tick(time.Millisecond * 200) {
burstyLimiter <- t //每隔200毫秒写入到channel(带缓冲,这样可以自主向其中加time.Time来灵活控制)中,后续由该channel来实现速率控制
}
}

#状态管理
(1)原子计数
import “sync/atomic”
var ops uint64 = 0 //控制的范围(管理的状态范围)为一个变量
atomic.AddUint64(&ops, 1) //写
opsFinal := atomic.LoadUint64(&ops) //读
(2)互斥锁
import “sync”
var mutex = &sync.Mutex{}
mutex.Lock()
。。。 //控制的范围(管理的状态范围)为一段代码
mutex.Unlock()
(3)状态协程(goroutine+channel)

#排序
import “sort”
方法:
1.sort.Strings/Ints/…(切片实例)——>对切片(其中的元素类型为基本类型)实例进行排序
2.sort.IntsAreSorted/…(切片实例)——>切片实例是否已经排好序
3.自定义排序:
(1)对一个切片类型进行别名设置 eg:type ByLength []string
(2)该类型中要实现 sort.Interface 接口中的 Len,Less和 Swap 方法(谓词),就可以使用 sort 包的通用Sort 方法
Len(得到序列长度) 和 Swap(交换元素) 通常在各个类型中都差不多,Less 将控制实际的自定义排序逻辑(首元素 “小于” 尾元素,则返回true)

#panic
运行程序将会引起 panic,输出一个错误消息和 Go 运行时栈信息,并且返回一个非零的状态码
panic会停掉当前正在执行的程序(注意,不只是协程),但是与os.Exit(-1)这种直愣愣的退出不同,panic的撤退比较有秩序,他会先处理完当前goroutine已经defer挂上去的任务,执行完毕后再退出整个程序
panic退出前会执行本goroutine的defer,方式是栈方式(在defer中通过recover截取panic,从而达到try..catch的效果);但不是本goroutine的defer是不执行的哦

#Defer
1.用来确保一个函数调用在程序执行结束前执行
2.多个defer时是栈的方式(后进先出)

#字符串操作
import s “strings” //给导入的包一个别名
var p = fmt.Println //提供一个别名(很像#define)
--
s.Contains(“test”, “es”) //是否包含
s.Count(“test”, “t”) //内含几个
s.HasPrefix(“test”, “te”) //是否是前缀
s.HasSuffix(“test”, “st”) //是否结尾
s.Index(“test”, “e”) //第一次出现的位置
s.Join([]string{"a", "b"}, “-”) //合并一个字符串切片,中间用特定符号隔开
s.Repeat(“a”, 5) //构建一个字符串aaaaa
s.Replace(“foo”, “o”, “0”, 1) //把foo中的o全换成0
s.Replace(“foo”, “o”, “0”, 1)
s.Split(“a-b-c-d-e”, "
“) //按照-做split(注意坑)
s.ToLower(”TEST") //小写
s.ToUpper(“test”) //大写
--

#文件操作
1.读文件
import “io/ioutil”
import “io”
import “bufio”
<1>dat, err := ioutil.ReadFile(“/tmp/dat”) //将文件内容读取到内存
<2>拆分开来:
f, err := os.Open(“/tmp/dat”) //打开一个文件,得到一个os.File实例f
b1 := make([]byte, 5); n1, err := f.Read(b1) //从文件开始位置读取最多5个字节的数据,n1返回实际读取的数据长度
o2, err := f.Seek(6, 0) //Seek 到一个文件中已知的位置并从这个位置开始进行读取
n3, err := io.ReadAtLeast(f, b3, 2)
r4 := bufio.NewReader(f) //实现了带缓冲的读取
f.Close()

2.写文件

#时间
import “time”
结构体:
type Time struct {
wall uint64
ext int64
loc *Location
}
1.now := time.Now() //得到当前时间(Time实例)
2.构建一个 Time实例 —> then := time.Date(…)
3.提取各部分
Time类的方法:Year(),Month()…
4.比较两个时间(之前,之后或者是同一时刻,精确到秒)
then.Before(now)
then.After(now)
then.Equal(now)
now.Sub(then) //返回一个 Duration(type Duration int64) 来表示两个时间点的间隔时间
5.秒数或者纳秒数
now.Unix()
now.UnixNano()
—>毫秒数不存在,故只能通过 now.UnixNano()/1000000 得到
6.格式化和解析
。。。

#命令行参数(用于指定程序运行参数)
import “os”
os.Args //切片,提供原始命令行参数(其中第一个参数是该程序的路径,后面就是程序的参数)
-→更标准的做法:
命令行标志(命令行程序指定选项)
import “flag” //支持基本的命令行标志解析,基本的标记声明仅支持字符串、整数和布尔值选项
wordPtr := flag.String(“word”, “foo”, “a string”) //三个参数作用分别是:标志,默认值,说明描述,返回值是对应类型的指针
numbPtr := flag.Int(“numb”, 42, “an int”)
boolPtr := flag.Bool(“fork”, false, “a bool”)
—→另一种方式:flag.StringVar(&svar, “svar”, “bar”, “a string var”)
flag.Parse() //命令解析
flag.Args() //后面的位置参数(flag 包需要所有的标志出现位置参数之前(否则,这个标志将会被解析为位置参数))
eg:./command-line-flags -word=opt //若省略则采用默认值
使用 -h 或者 —help 标志来得到自动生成的这个命令行程序的帮助文本
eg:./command-line-flags -h
提供一个没有使用 flag 包指定的标志,程序会输出一个错误信息,并再次显示帮助文本

#信号(响应并处理os发送的信号——>很容易联想到channel)
import “os/signal”
import “syscall”
sigs := make(chan os.Signal, 1) //向一个通道发送 os.Signal 值来进行信号通知
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) //注册指定的接收通道注册特定信号
sig := <-sigs //一旦有信号则接收

#########################################进阶
1.逃逸分析
作用:性能优化,减少了GC的压力<—不逃逸的对象(编译器会)分配在栈上,当函数返回时就回收了资源,不需要GC标记并清除;而分配在堆上的资源会在函数执行结束后由GC负责回收(若太多就会导致GC压力过大,影响性能)
通过逃逸分析可确定哪些变量分配在栈上(栈的分配比堆快)

引起逃逸缘由:GO对使用者消除了堆和栈的区别(有点类似java),使用者无需像C那样考虑变量的申请和释放情况(方便的代价就是编译器构建的内存管理功能可能会带来性能问题).
—>go在编译的时候进行逃逸分析来决定一个对象放栈上还是放堆上:不逃逸的对象放栈上,可能逃逸的放堆上

开启GO编译时的逃逸分析日志:
-gcflags ‘-m -l’ //不让编译时自动内连函数会加 -l

人工判断:
除开函数外部引用的对象(函数返回局部指针变量,闭包引用对象),还有如下情况
a := make([]int, 0, 20) // 栈 空间小
b := make([]int, 0, 20000) // 堆 空间过大(栈空间不足)
l := 20
c := make([]int, 0, l) // 堆 动态分配不定空间

好处:
栈上分配内存比在堆中分配内存有更高的效率
栈上分配的内存不需要GC处理
堆上分配的内存使用完毕会交给GC处理
逃逸分析目的是决定内分配地址是栈还是堆
逃逸分析在编译阶段完成

→函数传递指针真的比传值效率高吗?
传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。
-
————————>得到逃逸分析结果后的措施
(1)对象池 sync.Pool

#########################################环境
link:
https://segmentfault.com/a/1190000020293616

关于go mod
如果代码库在 $GOPATH 中,module 功能是默认不会开启的;
开启:export GO111MODULE=on
GOPROXY 环境变量
设置了该变量,下载源代码时将会通过这个环境变量设置的代理地址,而不再是以前的直接从代码库下载(国内代理:https://goproxy.cn)
Go1.13 将 GOPROXY 默认成了无法访问的 https://proxy.golang.org ,so从今以后必须先修改 GOPROXY 才能正常使用 go 来开发应用(见上)
go env -w GOPROXY=https://goproxy.cn,direct(当 go 在抓取目标模块时,若遇见了 404 错误,那么就回退到 direct 也就是直接去目标模块的源头(比如 GitHub) 去抓取)

#########################################bytes.Buffer
一个缓冲byte类型的缓冲器,里面存放的都是byte(操作方式类似队列)
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}
a buffer is a variable-sized buffer of bytes with Read and Write methods.the zero value for Buffer is an empty buffer readey to use.
(1)创建一个Buffer,底层就是一个[]byte 如果buffer变得太大,会panic(ErrTooLarge)
*var b bytes.Buffer
*b1 := new(bytes.Buffer)
*func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }
*func NewBufferString(s string) *Buffer
方法:
有关写
func (b *Buffer) Write(p []byte) (n int, err error) //把字节切片 p 写入到buffer中
func (b *Buffer) WriteString(s string) (n int, err error) //将一个字符串放到缓冲器的尾部
func (b *Buffer) WriteByte(c byte) error //将一个byte类型的数据放到缓冲器的尾部
有关读
func (b *Buffer) Read(p []byte) (n int, err error) {} //按p当前的容量进行读取(同时b类似队列,也相应偏移),满了或者b里没有了就返回
func (b *Buffer) ReadByte() (c byte, err error) {} //读取缓冲器头部的第一个byte,缓冲器头部第一个byte被拿掉
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error) {} //需要一个byte作为分隔符,读的时候从缓冲器里找第一个出现的分隔符(delim),找到后,把从缓冲器头部开始到分隔符之间的所有byte进行返回,作为byte类型的slice,返回后,缓冲器也会空掉一部分
func (b *Buffer) ReadString(delim byte) (line string, err error)
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {} //把r里的内容读到缓冲器里,n返回读的数量(应该是写)
func (b *Buffer) Reset() //将数据清空,没有数据可读
func (b *Buffer) String() string {} //将未读取的数据返回成 string
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }

#########################################测试
1.单元测试
(1)含有单元测试代码的go文件必须以_test.go结尾(如main_test.go,这里前缀最好和要测试的源文件名对应起来),Go语言测试工具只认符合这个规则的文件
(2)单元测试的函数名必须以Test开头(如TestAdd,这里后缀最好和要测试的函数名对应起来),是可导出公开的函数(首字母大写);必须接收一个指向testing.T类型的指针,并且不能返回任何值
(3)执行命令:go test v
(4)查看覆盖率
<1>只输出覆盖率到屏幕 go test -v -coverprofile=c.out
<2>输出具体的覆盖情况 o tool cover -html=c.out -o=tag.html //打开tag.html可看到具体的覆盖情况
-
—>monkey patch
通过 go mod 方式: import “github.com/bouk/monkey”
替换函数:
patch := Patch(target, replacement interface{}) //使用replacement函数替换target函数
替换方法:
patch := PatchInstanceMethod(target reflect.Type, methodName string, replacement interface{})
//target: 对象实例的反射类型; methodName:方法名; replacement: 要替换的方法对象
还原:
patch.Unpatch()

2.性能测试

⚠️ **GitHub.com Fallback** ⚠️