Bugs

工作中问题收集

善于提问,敢于质疑,勤于思考/反思,笨而多总结。避免夜郎自大,闭门造车

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}
}

参考资料:

css 复制代码
a、json.NewDecoder用于http连接与socket连接的读取与写入,或者文件读取;
b、json.Unmarshal用于直接是byte的输入。

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方法其实就是这个解决方案

blog.csdn.net/impressionw...

blog.csdn.net/testapl/art...

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建立连接详情信息来获取消耗的时间

相关推荐
yuuki23323329 分钟前
【C语言】文件操作(附源码与图片)
c语言·后端
IT_陈寒33 分钟前
Python+AI实战:用LangChain构建智能问答系统的5个核心技巧
前端·人工智能·后端
无名之辈J1 小时前
系统崩溃(OOM)
后端
码农刚子1 小时前
ASP.NET Core Blazor简介和快速入门 二(组件基础)
javascript·后端
间彧1 小时前
Java ConcurrentHashMap如何合理指定初始容量
后端
catchadmin1 小时前
PHP8.5 的新 URI 扩展
开发语言·后端·php
少妇的美梦1 小时前
Maven Profile 教程
后端·maven
白衣鸽子1 小时前
RPO 与 RTO:分布式系统容灾的双子星
后端·架构
Jagger_2 小时前
SOLID原则与设计模式关系详解
后端
间彧2 小时前
Java: HashMap底层源码实现详解
后端