工作中问题收集
善于提问,敢于质疑,勤于思考/反思,笨而多总结。避免夜郎自大,闭门造车
1.once.do实现单例实现问题
go
错误用例:
func main() {
NewOpenApi(1)
NewOpenApi(2)
}
var once sync.Once // 错误写法,只是外部在声明一份,而不在方法中声明
// 这个地方有个坑,只在外部声明一个统一的once,两个实例方法都使用这个once的时候,就会发现只有一个once被实例化成功,另一个实例会报未初始化,所以需要在子方法内容单独声明 sync.once
type ScrmOpenApiV2 struct {
appId string `json:"app_id"`
}
var instance *OpenApiV2
var instance2 *OpenApiV2
func NewOpenApi(t int) *OpenApiV2 {
if t == 1 {
return onceFunc()
}
return newOnceFunc()
}
func onceFunc() *OpenApiV2 {
if instance != nil {
return instance
}
//var once sync.Once // 正确写法
once.Do(func() {
instance = new(OpenApiV2)
instance.appId = "100"
})
return instance
}
func newOnceFunc() *OpenApiV2 {
//var once sync.Once // 正确写法
once.Do(func() {
instance2 = new(OpenApiV2)
instance2.appId = "200"
})
return instance2
}
2.for 循环里面使用sync.waitgroup的问题,不能使用值类型的sync.WaitGroup作为参数,使用指针
go
两种goroutine 写法比较:
package main
import (
"fmt"
"sync"
"time"
)
type IsOnlineItem struct {
UserID string `json:"user_id"`
IsOnline int `json:"is_online"`
}
func useWaitGroupBug() {
var wg = new(sync.WaitGroup)
lock := sync.Mutex{}
list := make([]IsOnlineItem, 0)
userIDs := []string{"01", "02", "03"}
for _, userID := range userIDs {
wg.Add(1)
go func(userIDs string) {
defer func() {
if err := recover(); err != nil {
fmt.Printf("IsOnline panic err:%+v", err)
}
wg.Done()
}()
lock.Lock()
list = append(list, IsOnlineItem{
UserID: wxUserID,
IsOnline: 1,
})
lock.Unlock()
}(userID)
}
wg.Wait()
fmt.Printf("%v\n", list)
}
func main() {
useWaitGroupBug()
// 第二种写法,使用指针*sync.WaitGroup
num := 3
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go process(i, &wg)
}
wg.Wait()
fmt.Println("All go routines finished executing")
}
func process(i int, wg *sync.WaitGroup) {
fmt.Println("started Goroutine ", i)
time.Sleep(2 * time.Second)
fmt.Printf("Goroutine %d ended\n", i)
wg.Done()
}
// 有一点需要特别注意的是process()中使用指针类型的*sync.WaitGroup作为参数,
// 这里不能使用值类型的sync.WaitGroup作为参数,因为这意味着每个goroutine都拷贝一份wg,每个goroutine都使用自己的wg。
// 这显然是不合理的,这3个goroutine应该共享一个wg,才能知道这3个goroutine都完成了。实际上,如果使用值类型的参数,main goroutine将会永久阻塞而导致产生死锁。
3.json.UnMarsh() float64字段类型时精确缺失是因为当 JSON 中存在一个比较大的数字时,它会被解析成 float64 类型,就有可能会出现科学计数法的形式。
- 返回的结构体内容data 定义为interface{}类型,在原数据遇到长度较大的整形时,转换为了float64类型出现精确缺失
go
package main
import (
"encoding/json"
"fmt"
)
var dataStr = `{"code":100,"data":{"id":1473954384332480512}}`
type BasicResp struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
type MyData struct {
ID int `json:"id"`
}
func main() {
var resp BasicResp
err := json.Unmarshal([]byte(dataStr), &resp)
fmt.Printf("resp:%v\n", resp)
// 实际上在这出错了,类型已经转换为float64类型,resp:{100 map[id:1.4739543843324805e+18]}
b, err := json.Marshal(&resp.Data)
fmt.Printf("err:%v,b:%v\n", err, string(b)) // err:<nil>,b:{"id":1473954384332480500}
// 业务层只关心data内容
var md MyData
err = json.Unmarshal(b, &md)
fmt.Printf("err:%+v\n", err)
fmt.Println("md", md) // md {1473954384332480500}
}
- 解决办法除了重新返回值为具体结构类型外还可以使用json.NewDecoder()类型解码
go
package main
import (
"encoding/json"
"fmt"
)
var dataStr = `{"code":100,"data":{"id":1473954384332480512}}`
type BasicResp struct {
Code int `json:"code"`
Data interface{} `json:"data"`
}
type MyData struct {
ID int `json:"id"`
}
func main() {
var resp BasicResp
d := json.NewDecoder(bytes.NewReader([]byte(dataStr)))
d.UseNumber()
d.Decode(&resp)
fmt.Printf("resp:%v\n", resp) //resp:{100 map[id:1473954384332480512]}
bf := bytes.Buffer{}
err := json.NewEncoder(&bf).Encode(&resp.Data)
b := bf.Bytes()
fmt.Printf("err:%v,b:%v\n", err, string(b)) // err:<nil>,b:{"id":id:1473954384332480512}
// 业务层只关心data内容
var md MyData
err = json.Unmarshal(b, &md)
fmt.Printf("err:%+v\n", err)
fmt.Println("md", md) // md {id:1473954384332480512}
}
参考资料:
- 1.json数据解码的两种方法NewDecoder与Unmarshal blog.csdn.net/qq_42346574...
css
a、json.NewDecoder用于http连接与socket连接的读取与写入,或者文件读取;
b、json.Unmarshal用于直接是byte的输入。
- 2.json.unmarsh()遇到的问题 www.cnblogs.com/xinliangcod...
4.局部变量赋值问题
go
package main
import (
"context"
"errors"
"fmt"
"go.etcd.io/etcd/clientv3"
"time"
)
var cli *clientv3.Client
func conn() {
/* 解决办法:
var err error
cli,err = clientv3.New
*/
cli, err := clientv3.New(
clientv3.Config{
Endpoints: []string{"172.17.0.1:2379"},
DialTimeout: 5 * time.Second,
})
defer cli.Close()
if err != nil {
fmt.Println(errors.New("connect failed"))
}
fmt.Println("connect Etcd success")
}
func mustInit() error {
if cli == nil {
return errors.New("config is not init")
}
return nil
}
fun main(){
if err := mustInit(); err != nil {
return err
}
}
为啥总是cli指针总是nil值呢?因为在cli, err := clientv3.New这一行,使用了:赋值,相当于重新给cli分配了值,前面的定义无效了,去掉:就好了
5.項目使用gorm
请求日志出現了 mysql invalid connection错误,经查证是mysql 连接池中的连接被服务器单方面关闭了,而程序却不知道,依然使用这个连接,所以会出现这个错误。
sql
可以检查mysql 设置的连接超时时间:
SHOW VARIABLES LIKE '%timeout%';
可以看到连接的超时关闭时间为 60 s。客户端没有限制超时关闭时间,这样如果一个连接超过 60s 没有使用,服务器会关闭这个连接,而客户端并不知道连接已经被关闭,再通过这个连接去查询数据时就失败了。
高峰时期用户很多,每个连接在达到 60s 之前就被复用了,然后连接的超时时间就会重置,所以用户多的时候并不容易出现这个问题。
解决办法就是为客户端的连接池设置一个更短的生存时间。
db.DB().SetConnMaxLifetime(59 * time.Second)
6.并发写入 map 导致的致命错误,recover无法捕获错误
go
func main() {
m := make(map[int]string)
for i := 0; i < 10; i++ {
go func() {
defer func() {
if e := recover(); e != nil {
log.Printf("recover: %v", e)
}
}()
m[i] = "Go 语言编程之旅:一起用 Go 做项目"
}()
}
fmt.Println(1234)
}
go
debug/panic.go #gosetup
/private/var/folders/k0/6j_zdmmx43zctycxk58s8tw00000gn/T/GoLand/___2go_build_go_notes_debug__4_
fatal error: concurrent map writes
fatal error: concurrent map writes
goroutine 18 [running]:
runtime.throw(0x10cbf29, 0x15)
/usr/local/go/src/runtime/panic.go:617 +0x72 fp=0xc00003ef60 sp=0xc00003ef30 pc=0x1028192
runtime.mapassign_fast64(0x10add20, 0xc000096000, 0x7, 0x0)
/usr/local/go/src/runtime/map_fast64.go:176 +0x344 fp=0xc00003efa0 sp=0xc00003ef60 pc=0x100efe4
main.main.func1(0xc000096000, 0xc000098000)
附录 B:Goroutine 与 panic、recover 的小问题 | Go 语言编程之旅 (eddycjy.com)
7.Gin框架,http包 body参数只能读取一次,中间件读取body后,handle中无法再读
由于默认http.Request.Body类型为io.ReadCloser
类型,即只能读一次,读完后直接close掉,后续流程无法继续读取http.request是readcloser。我们将http.request readAll的时候讲无法再次读取http.request里面的信息。
go
// GetRawData return stream data.
func (c *Context) GetRawData() ([]byte, error) {
return ioutil.ReadAll(c.Request.Body)
}
...
Body io.ReadCloser
....
// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
Reader
Closer
}
...
解决办法: 新建缓冲区并替换原有Request.body
ini
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
go
var bodyBytes []byte // 我们需要的body内容
// 从原有Request.Body读取
bodyBytes, err := ioutil.ReadAll(c.Request.Body)
if err != nil {
return 0, nil, fmt.Errorf("Invalid request body")
}
// 新建缓冲区并替换原有Request.body
c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// 当前函数可以使用body内容
_ := bodyBytes
httputil 标准包其实有相应的解决方法
go
import ("net/http/httputil")
...
httpRequest, _ := httputil.DumpRequest(c.Request, false)
...
func DumpRequest(req *http.Request, body bool) ([]byte, error) {
var err error
save := req.Body
if !body || req.Body == nil {
req.Body = nil
} else {
save, req.Body, err = drainBody(req.Body)
if err != nil {
return nil, err
}
}
...
}
func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) {
if b == nil || b == http.NoBody {
// No copying needed. Preserve the magic sentinel meaning of NoBody.
return http.NoBody, http.NoBody, nil
}
var buf bytes.Buffer
if _, err = buf.ReadFrom(b); err != nil {
return nil, b, err
}
if err = b.Close(); err != nil {
return nil, b, err
}
return io.NopCloser(&buf), io.NopCloser(bytes.NewReader(buf.Bytes())), nil
}
看下drainBody方法其实就是这个解决方案
8.go 编译报错 pkg is not in GOROOT
go
go build -tags "etcd " -ldflags '-X "toolsapi/version.TAG=" -X "toolsapi/version.VERSION=e843c692016c7bc5892f797a59f7e0dc672af4aa" -X "toolsapi/version.AUTHOR=peanut" -X "toolsapi/version.BUILD_INFO=fix logmiddleware" -X "toolsapi/version.BUILD_DATE=2022-02-15 17:20:40"' -gcflags "-N" -o ./bin/toolsapi ./cmd/toolsapi
cmd/toolsapi/main.go:13:2: package toolsapi/app/router is not in GOROOT (/usr/local/go/src/toolsapi/app/router)
cmd/toolsapi/main.go:14:2: package toolsapi/pkg/bootstrap is not in GOROOT (/usr/local/go/src/toolsapi/pkg/bootstrap)
原因在于创建go.mod 项目文件是toolsapi,go mod init时候生成是
arduino
module my_tools/toolsapi
改为
arduino
module toolsapi
9.调用酷狗音乐服务时,客户端轮训http请求用go封装请求第三方接口暴露出的路由接口 接口大量超时报错:context deadline exceeded (Client.Timeout exceeded while awaiting headers)
http client超时如果是接口请求qps过高,负载过高,此种解决办法就是扩容,加机器 完善本地逻辑,同时排查网络问题,
markdown
对话
a:我们这边用的云服务器是 aws 宁夏区。但现在通过 dig+trace 测试发现酷狗的 ns 上 dns 解析记录把我们这边的 ip 划分到了境外。导致现在服务请求[thirdssomdelay.kugou.com](https://thirdssomdelay.kugou.com/)有很大的延迟,同时也丢包严重。您看您这边能对这方面优化吗?
b:操作: dig thirdssomdelay.kugou.com +short
在宁夏区的 EC2 实例上使用`dig`命令对`thirdssomdelay.kugou.com`进行 DNS 解析的结果
- `kgnop2.kugou.com.`
- `kgnop.hong.kong.kugou.com.`
- `110.242.91.7`
- `110.242.91.9`
- `110.242.9.8`,这些是解析得到的域名和 IP 地址,其中`kgnop.hong.kong.kugou.com.`表明可能与香港的节点有关,而后面的 IP 地址属于`103.243.93.0/24`网段。
因为机器使用的aws 服务,发现dns解析的时候会先到香港然后跑到美国,绕了一圈太平洋 可以使用net/http/httpstace 判断dns解析和tcp建立连接详情信息来获取消耗的时间