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

相关推荐
midsummer_woo1 小时前
基于spring boot的医院挂号就诊系统(源码+论文)
java·spring boot·后端
Olrookie2 小时前
若依前后端分离版学习笔记(三)——表结构介绍
笔记·后端·mysql
一条GO2 小时前
ORM中实现SaaS的数据与库的隔离
后端
京茶吉鹿2 小时前
"if else" 堆成山?这招让你的代码优雅起飞!
java·后端
长安不见2 小时前
从 NPE 到高内聚:Spring 构造器注入的真正价值
后端
你我约定有三2 小时前
RabbitMQ--消息丢失问题及解决
java·开发语言·分布式·后端·rabbitmq·ruby
程序视点3 小时前
望言OCR 2025终极评测:免费版VS专业版全方位对比(含免费下载)
前端·后端·github
rannn_1113 小时前
Java学习|黑马笔记|Day23】网络编程、反射、动态代理
java·笔记·后端·学习
一杯科技拿铁3 小时前
Go 的时间包:理解单调时间与挂钟时间
开发语言·后端·golang