读源码学到的新的语法,补充自己对go的更多了解
断言
今天在看别人代码的时候发现了一个 interface.([]int) 新用法,以前不知道,这个其实就是断言 assert, 其实php 中也经常用到
1.go 中目前我接触到断言,取map值的时候 ,a, ok := map[“name”], 虽然没有这个ok 也是能正确运行的,比如 a := map[“name”]
2.interface 断言, interface.([]int), 转换成[]int类型
3.断言失败会取断言类型的默认值,如果断言失败还是不知道原因可以用reflect.TypeOf获取断言的真正类型。断言失败的时候经常就是胯类型断言,比如你知道一个类型[]map[string]string, 但你收到这个值的时候不能直接断言interface.([]map[string]string), 而是应该 interface.([]interface{}), for range 每个值断言map[string]string. 所以能用结构体就用结构体接受吧,要不然每层断言很辛苦 ,曾经断言60行的代码,用结构体 不到10行 就接受了,还不用处理一堆的断言错误。
4.关于类型 interface{} 兼容 string, 但不代表 []interface{} 兼容 []string
1 | Can I convert a []T to an []interface{}? ¶ |
5.今个写代码遇到件事, 对于 int 1, 我用 string(int 1) —–> 想直接得到结果,这样是不行的,还是老老实的用 strconv.Itoa, string 应该直接用在字符类型,比如 rune []byte 这类
Slice
奇怪的现象
1.最近用slice 总容易写一个bug, 就是make 的时候给定大小,然后append slice, 这样会导致一直往后面插入slice ,而不是从0 开始修改slice 的值。原因就是 make 的时候 len 和 cap 都是 给定的值,每个位置都有自己的默认值,append 的元素已经没地方放了,只会动态扩容 slice 来容纳给更多的内容。
2.slice 和 map 虽然是地址类型,但是我们 for range 的时候改变值并不会修改自身,原因就是因为for range 的时候是copy 。copy 之后dst 和src 指向的底层 array 不一样,导致指向 dst 和 src 的slice 随意修改都不影响对方。
3.copy 也有需要注意的地方,就是 src 的len > dst 的len ,并不会copy 全,也就是copy的时候并不会动态扩容
4.切片的传递属于引用传递,我们日常使用切片也是引用。所以我们在用切片相互赋值的时候,修改某一个可能会影响两一个。原因就是slice 底层指向了同一个数组
5.我们在什么情况下使用切片不会影响到之前赋值的切片呢,就是在切片动态扩容,改变切片地址指向的时候,比如 append 的时候。因为我们一般切片创建用的 make,make slice len == cap ,当我们append 的时候必然扩容。
6.切片是由3个属性决定, 指针 ,len, cap 一般情况下我们 len 和 cap 都一样, 所以会导致我们觉得 指针改变,切片就变化,指针不变,切片就不变。
1 | func m(modify[]int) []int{ |
6.我们var 定义 []int 的时候,[]int 是nil, 所以我们不能给他赋值。但是我们make 的时候,虽然此时没有开辟内存空间,但是point 是有值的。!!!所以如果用var 定义的变量赋值会报address 不存在错误,如果用make 就不会。
7.for range 的时候只循环len 的内容,不循环cap 的内容
channel定义
之前看过对channel 的定义最好不要在全局,之前不知道为啥原因,当时因为想做缓冲channel, 而以为var 没法做,所以一直没用全局channel
1 | var channel = make([]chan task, 10) |
goroutine的意义
把一个任务分成很多部分,每个任务完成的周期很短。多个任务中我们可以通过channel 进行通信。如果我们通过单个channel ,在进行io的时候会阻塞的, 所以我们需要多个channel 来配合多个goroutine。多个goroutine消耗多个channel, 取数据的时候,可以把channel 传入goroutine当中,来消耗特定的channel。投递数据的时候咋办?当往特定的channel 中投递任务,因为go不像php 那样可以拼变量名,我们可以先把多个channel 放在一个数组中,然后通过数组index 去取特定的channel。
go slice 结构, (指向array 的指针,len, cap)
(go 中slice 的改动会及时没有用 & 也会影响自身)
1 | func handle(a []string) { |
(copy 方便数组的拷贝,不影响原始数组的变动)
(arr := […]int{1,2,3,4}, arr[1:2:3], start 1 end 2 len 3, 索引的位置)
// go 中的引用类型
引用类型和原始的基本类型恰恰相反,它的修改可以影响到任何引用到它的变量。在Go语言中,引用类型有切片、map、接口、函数类型以及chan
。
引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。
// go 中经常这样,类型别名
1 | type Duration int64 |
// go 可变参数
可以变参数,可以是任意多个。我们自己也可以定义可以变参数,可变参数的定义,在类型前加上省略号…即可。
// 组合类型
1 | type user struct { |
// 访问权限
1 | type user struct { |
// race 检测对共享变量的修改
1 | go build -race 10.go |
// sync 包真的是解决并发问题的一个优点
1 | package main |
// 很经典的一个关于获取三个url 最快速的方式
1 | func mirroredQuery() string { |
json 序列化的小问题
json 协议没有int 类型,只有 number 类型。int 都会被解析成 float64, 注意!!
上面的描述有bug ,今天一个str json 反序列化的时候很成功,啥时候会出现上面问题,通过 interface{} 断言的时候。
json 序列化的时候如果没有这个值,就不给客户端 (比如我们更倾向于返回空对象,而不是一个完整对象,然后值都是空的),可以使用json tag 中的 omitempty
json 序列化的时候如果不想要这个,可以直接 - (比如密码这类我们不想暴露给客户端,我们只是我们后端struct 使用,并不需要给客户端)
json 我们也可以用 int 接受 string 类型 (需要注意的是 我们再次 json序列化的时候 还是 string )
今天遇到一个很好用json 方法,就当做json 序列化和 反序列化前的操作
url处理
最近有一个很恶性的需求,就是解析别人填入的url, 再添加一些想要的参数,组成新的url 返回,
1 |
|
http 请求
get
golang 发送http 请求没有 php 那么直接,其实php 也没有那么直接,毕竟curl 那么一大串,只是php 的curl 面向过程,看起来是那么自然,从上而下
1 | import "net/http" |
1 | import "net/http" |
1 | // 本质 |
评论 :Go的get请求面上有好几种请求方式,实则只有一种:
1、使用http.NewRequest
函数获得request
实体
2、利用http.client
结构体的Do
方法,将request
实体传入Do
方法中。
post
1 | import ( |
1 | import ( |
1 | import ( |
当然上面的方法本质上也是用client 发出来的
然后client 本质也是依靠 newRequest
1 | import ( |
!!!notice
1 | 添加request header |
有一点需要注意:在添加header操作的时候,req.Header.Add
和req.Header.Set
都可以,但是在修改操作的时候,只能使用req.Header.Set
。
有一点需要注意:在添加header操作的时候,req.Header.Add
和req.Header.Set
都可以,但是在修改操作的时候,只能使用req.Header.Set
。
这俩方法是有区别的,Golang底层Header的实现是一个map[string][]string
,req.Header.Set
方法如果原来Header中没有值,那么是没问题的,如果又值,会将原来的值替换掉。而req.Header.Add
的话,是在原来值的基础上,再append
一个值,例如,原来header的值是“s”,我后req.Header.Add
一个”a”的话,变成了[s a]
。但是,获取header值的方法req.Header.Get
确只取第一个,所以,如果原来有值,重新req.Header.Add
一个新值的话,req.Header.Get
得到的值不变。
其实不止是header 会这样,query 参数也会这样。
Response
1 | import ( |
获取返回值
Sort
go sort 已经有人封装好包了,相比较自己写快排,冒泡的好处就是,这个包会根据效率自动选择合适的排序方式,我们需要做的就是实现sort 中的接口 (https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter03/03.1.html)
1 | Len() 获取要排序的slice 的len |
对于倒序,很简单的实践方式
1 | sort.Sort(sort.Reverse([]object)) // 帮我们少写了好多代码 |
1 | 上面的用法得写一堆东西,还有简单的方式 |
runtime.Caller
在调用公司组件的时候,发现log 输出信息有问题,错误行号和文件是上一层函数调用,而不是我想输出的地方的调用。问题就出在,runtime.Caller(skip), 这个参数,
1 | for i:=0; i<=4; i++ { |
https://studygolang.com/articles/3116, 这篇文章对 call 和 calls 方法讲解的比较细致
1 | pc := make([]uintptr, 1024) |
https://colobu.com/2018/11/03/get-function-name-in-go/, 这篇文章也对上面两个方法做了详细的解释
http状态码
429 ,限流了
499 , 服务端返回的时间超出客户端设置的超时时间,到这客户端提前关闭
fmt问题
平时为了打印结构体我们就用 fmt.printf(“%+v”), 但当我们用[]struct 的时候,这个打印就不好使了,会直接打印内存地址(我们为啥要用struct, 因为遍历的时候 struct 不能修改值)
1 |
|
Kafka使用
Es使用 (olivere/elastic.v6,我们的es 是6.x 版本,所用的v6)
今天在使用es 的时候,想起了之前使用redis 的时候有not found 的判定,找了下 果然这个es 包中也有。只是这个es 包判定not found 用的是方法,原理是 http 请求的code,相比较redis, 感觉这个更靠谱些。
Time
time 很实用的一个方法, time.Since 可以获取时间差
https://www.jianshu.com/p/f809b06144f7, 时间的很好的一个文章
timer : 延迟触发,只触发一次。 可以重置
ticker : 多次执行。
1 | // timer |
Error
go 中 error 的处理还是没有总结出什么好的办法,目前想到的就是公用的方法,统一内部自己处理异常,当然也会抛出去,外层就可以不用处理了。
几种error 可以不处理的场景:
1.断言失败,有默认值的
2.调用方法,失败了error有返回,对于int float 这些都有默认的返回值。但对于一些结构体,还是要给默认值的,否则可能会是nil
3.思考一下我在项目中error 的处理方式。一般有问题,都在最底层的服务统一处理,因为一层层的往上抛,感觉日志记录可能重复,详见我的third 包调用 第三方的处理方式。但是我的service中, error 一般都没处理,一般是抛给了 controller 处理,因为我的service 虽然按道理也是公共的,但是调用方其实很少,当时考虑的也不太周到,所以都是交给controller 处理。
error 几种常见需要解决的问题
1.wrap ,我经常要 对错误信息添加,比如对发生错误时候的参数进行记录。
2.判断两个error 是否相等。我们需要注意的是 error 是地址类型 (可以通过 reflect 获取 ),所以两个error 完全相等必须是同一个变量,而不只是 new 里面的内容相等
1 | var UserSexNil = errors.New("未查到该用户信息!") |
go 1.13 之后又很完美的方式解决我上面的蛋疼问题
1 | fmt.Error("%w xxx", errors.New("old error")) // 这个新生成的error 就是old error + 自定义的xx信息, 但此时我们要判断是否是old error 生成,需要调用特殊的方法 |
引申: 看了这个想到了怎么对interface 类型执行,可以
静态文件引入
php 中对于静态文件引入很容易,定义个相对路径就好了,但是我们go 微服务不可以(不是所有的都不可以), 我们go 的执行方式是
1 | exec /app/"$@" -conf /app/config.yaml |
go.mod
indirect , 可能是我平时直接 go get 获取的,项目中用不到
go.sum https://studygolang.com/articles/25658, 这篇文章讲的很好。 go.mod 中只是我们直接import 的文件,go.sum 中存在依赖的依赖。 那串hash 主要用来校验,防止别人修改代码。go 中发布是可以删除tag 修改代码再发布的,这样就会导致你之前依赖的代码可能被别人修改过,而你不知道,这时候go.sum 就会发挥用处。
go.sum 也是个文件,可能存在conflict, 而且他还是类似 log 日志的那种存在。如果冲突,我们可以都保留,或者保留最新的,反正只是个验证作用。
如果实在通不过,难道真的是包作者删除 tag,再重新打包? ~~
一些思考
1.一些公共变量在框架init 的时候初始化,但这个变量应该放在哪?我开始是放在main 文件中,但有个很坑爹的时候,service 对于main 中变量引入不了,一来是因为循环依赖(比如 a 依赖b, b反过来也引入a),我们可以开一个单独的包,定义这个公共变量,并且 包含 自身init 函数
2.今天听大佬分享 公共pool 中对象被耗尽的问题,如何解决? (最近接的案例就是我们的redis pool 被耗尽了)。1.池子里面对象尽可能给多,2.池子里面对象取出来尽快还回去。