[TOC]
Golang踩坑
内存溢出
- GC回收时,无法实现100%的回收
- 有goroutine泄漏,zombie goroutine没有结束,这个时候在这个goroutine上分配的内存对象将一直被这个僵尸goroutine引用着,进而导致gc无法回收这类对象,内存泄漏。
- 有生命周期和程序一样长的的数据结构意外的挂住了本该释放的对象,虽然goroutine已经退出了,但是这些对象并没有从这类数据结构中删除,导致对象一直被引用,无法被回收。
- 总上面的几条,在编码时最好不要写一些对GC不友好的代码.否则内存溢出是必然的,只是早晚的问题.
- 资源句柄未释放导致继续频繁创建
- 死锁
- sync.pool不正确的使用了slice
- goroutine 的阻塞
- goroutine 无限制生成
- 大struct被频繁创建,函数传参最好用指针
- 大量的使用+拼接超大字符串
- 频繁的进行json操作(原版json解析性能不好,可以用easyjson)
指针
Go语言中的指针
Go语言中不存在指针操作,只需要记住两个符号
&:取地址
*:根据地址取值
取地址操作符&和根据地址取值操作符,是一对互补的操作
- 对变量进行取地址操作,可以获得变量的指针
- 指针变量的值是指针
- 对指针变量进行取值操作,可以获得指针变量指向的原变量的值
new和make
Go语言中对于引用类型的变量,我们在使用的时候需,不仅要声明,还要分配内存空间,否则我们的值没有办法存储,而对于值类型的变量就不需要分配内存,因为我们在声明的时候,已经分配好了内存,要分配内存就引出了new和make
make也是用来分配内存的,但区别与new,它只用于slice,map和chan的内存创建。而他返回的类型,就是三个类型的本身,而不是指针,因为三种类型已经是引用类型了,没必要返回指针
package main
import "fmt"
func main() {
a := 1
fmt.Println(&a)
b := new(int)
fmt.Printf("%v--%d\n", b, *b)
slice := make([]int, 0)
fmt.Printf("%v\n", slice)
}
————————————————
版权声明:本文为CSDN博主「我只要一发」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_44865158/article/details/114531019
io.Reader/io.Writer
作者:P_Chou
链接://www.greatytc.com/p/758c4e2b4ab8
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原生包中的io.Reader/Writer
是比较常用的接口,他们分别需要实现Read和Write,也就是说只要实现了他们方法的对象就可以使用.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
围绕io.Reader/Writer
,有几个常用的实现:
- net.Conn, os.Stdin, os.File: 网络、标准输入输出、文件的流读取
- strings.Reader: 把字符串抽象成Reader
- bytes.Reader: 把
[]byte
抽象成Reader - bytes.Buffer: 把
[]byte
抽象成Reader和Writer - bufio.Reader/Writer: 抽象成带缓冲的流读取(比如按行读写)
数据类型
引用类型与值类型
map, slice, channel,指针,函数, 都是引用类型, 其他均为值类型
引用类型尤其需要注意的是并发问题
可以比较类型
bool、整数、浮点数、复数、字符串、指针、Channel、接口都是可比较的,包含可比较元素的 struct 和数组,这俩也是可比较的,而 slice、map、函数值都是不可比较的。
goroutine
- 要限制协程数量,尤其是不能让协程无限制的自由生成.否则高并发时会起到反作用,甚至内存溢出,程序崩溃不说还会因为内存被打满而影响到同一台机器上的其他守护进程.所以一般都会采用协程池的概念
string
通过+和fmt.Sprintf
进行字符串拼接性能太差了在大字符串拼接时会导致内存暴增,推荐strings.Builder
:
var sBuilder strings.Builder
sBuilder.WriteString("aaa")
sBuilder.WriteString("bbb")
sBuilder.WriteString("ccc")
log.Println(sBuilder.String())
slice
操作 | 含义 |
---|---|
s[n] | 切片s中索引位置为n的项 |
s[:] | 从切片s的索引位置0到len(s)-1处所获得的切片 |
s[start:] | 从切片s的索引位置start到len(s)-1处所获得的切片 |
s[:end] | 从切片s的索引位置0到end处所获得的切片,len=end |
s[start:end] | 从切片s的索引位置start到end处所获得的切片,len=end-start |
s[start:end : max] | 从切片s的索引位置start到end处所获得的切片,len=end-start,cap=max-start |
len(s) | 切片s的长度,总是<=cap(s) |
cap(s) | 切片s的容量,总是>=len(s) |
老生常谈:
- 对slice切片是复制操,原slice不变len和cap还是原来的长度,
- 切片的新slice的len是从start到end, 而cap是从start一直到原slice的cap的最后.
- 切片的新slice的end如果超过了原len的长度,但是没有超过cap的长度是没问题的,不存在的元素会被置为0,如果超过cap则会panic.
- 在对一个slice进行append时,如果超过了cap长度,则会将其自动扩展为原cap长度的两倍.
- cap未变化时slice是对数组的引用并且append会修改被引用数组的值,但是append操作导致cap变化后会复制被引用的数组然后切断引用关系.
说上面这些,主要是为了记录下面这个案例
- 假设当有新的数据进来时不断的对一个slice进行append
- 然后将数据通过切片取出
- ...如此循环往复
此时要特别注意,千万不能因为append让这个slice的cap无限制的增长.尤其是在sync.pool中要特别注意.
map
- 未初始化
- 并发读写
- 并发锁
解决 map 并发 panic 的两个方法:加锁和分片
chan
会 panic 的情况,总共有 3 种:
- close 为 nil 的 chan;
- send 已经 close 的 chan;
- close 已经 close 的 chan。
Context
- 从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
- Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
- key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
- 常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
sync.Map
sync.Map 并不是用来替换内建的 map 类型的,它只能被应用在一些特殊的场景里。虽然是官方标准,反而是不常用的
官方的文档中指出,在以下两个场景中使用 sync.Map,会比使用 map+RWMutex 的方式,性能要好得多:
- 只会增长的缓存系统中,一个 key 只写入一次而被读很多次;
- 多个 goroutine 为不相交的键集读、写和重写键值对。
没有 Len 查询 sync.Map 的包含项目数量的方法
sync.mutex
- lock与unlock必须成对出现
- 不可复制
- 不可重入
- 死锁
sync.WaitGroup
计数器设置为负值会报错
不期望的 Add 时机会报错,简单来说就是不要在goroutine中add
前一个 Wait 还没结束就重用 WaitGroup会报错
sync.Once
- 死锁
- 未初始化
sync.Pool
- 不适用与网络连接数据库连接等等,因为会无通知的自动回收
- 不可使用后再复制使用
- 内存泄漏
- 内存浪费
Go 1.13 之前的 sync.Pool 的实现有 2 大问题:
- 每次 GC 都会回收创建的对象。
- 如果缓存元素数量太多,就会导致 STW 耗时变长;缓存元素都被回收后,会导致 Get 命中率下降,Get 方法不得不新创建很多对象。
- 底层实现使用了 Mutex,对这个锁并发请求竞争激烈的时候,会导致性能的下降。
- 在 Go 1.13 中,sync.Pool 做了大量的优化。前几讲中我提到过,提高并发程序性能的优化点是尽量不要使用锁,如果不得已使用了锁,就把锁 Go 的粒度降到最低。Go 对 Pool 的优化就是避免使用锁,同时将加锁的 queue 改成 lock-free 的 queue 的实现,给即将移除的元素再多一次“复活”的机会。
json
json.Marshal 默认 escapeHtml 为true,会转义 <、>、&
bytes, err := json.Marshal(data)
if err != nil {
r.err = err
return ""
}
return string(bytes)
改成
bf := bytes.NewBuffer([]byte{})
jsonEncoder := json.NewEncoder(bf)
jsonEncoder.SetEscapeHTML(false)
err := jsonEncoder.Encode(data)
if err != nil {
return ""
}
return bf.String()
json.Marshal会将JSON中的字符串里的\n
转移为\\n
DB
Error 1040: Too many connections
一出现这个情况就我的第一感觉就是,这是一个MySQL的错误
- 设置的
SetMaxOpenConns
币MySQL的max_connections
的大,导致部分连接无法连接.
果然,临时设置set global max_connections=500;
解决.
正确的方式,将go的SetMaxOpenConns
不能大于MySQL的max_connections
.
driver: bad connection
在开发时没有这个情况,开发机MySQL 5.6.44,预发布为 5.7.27;用的是xorm
一出现这个情况就我的第一感觉就是
- 设置的
timeout
比MySQL的connect_timeout
大了.长时间连接不上被MySQL提前断开了. - 设置的
readTimeout
比MySQL的net_read_timeout
或net_write_timeout
大了.慢的时候这边没断开,MySQL就断开了. - 设置
SetConnMaxLifetime
MySQL大 - 而一般MySQL的
wait_timeout
等都很长,轻易是不会出现这个问题的. - 再不然就是某些5.7的特性导致了
总之不论是那种原因,在配置时任何设置一定要主要不能让go的设置比MySQL的大.
我一般使用的是连接池,连接池一直维持着和MySQL的长连接.有的连接因为一些原因被MySQL断开了,但是可能依然会在连接池中.
假设此时从连接池中拿一个连接,而此连接正是被MySQL强制断开的则会出现该问题.
这是相关的MySQL配置
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.27 |
+-----------+
1 row in set (0.00 sec)
mysql> show variables like "%timeout%";
+-----------------------------+----------+
| Variable_name | Value |
+-----------------------------+----------+
| connect_timeout | 10 |
| delayed_insert_timeout | 300 |
| have_statement_timeout | YES |
| innodb_flush_log_at_timeout | 1 |
| innodb_lock_wait_timeout | 50 |
| innodb_rollback_on_timeout | OFF |
| interactive_timeout | 28800 |
| lock_wait_timeout | 31536000 |
| net_read_timeout | 30 |
| net_write_timeout | 60 |
| rpl_stop_slave_timeout | 31536000 |
| slave_net_timeout | 60 |
| wait_timeout | 28800 |
+-----------------------------+----------+
13 rows in set (0.01 sec)
mysql> show variables like "%conn%";
+-----------------------------------------------+-----------------+
| Variable_name | Value |
+-----------------------------------------------+-----------------+
| character_set_connection | utf8 |
| collation_connection | utf8_general_ci |
| connect_timeout | 10 |
| disconnect_on_expired_password | ON |
| init_connect | |
| max_connect_errors | 100 |
| max_connections | 151 |
| max_user_connections | 0 |
| performance_schema_session_connect_attrs_size | 512 |
+-----------------------------------------------+-----------------+
9 rows in set (0.00 sec)
解决方案
- 设置
SetConnMaxLifetime
到一个合理的值,让连接由go来关闭防止一个连接长时间存在最终被MySQL给关闭了.
这是追踪到的源码
// maxBadConnRetries is the number of maximum retries if the driver returns
// driver.ErrBadConn to signal a broken connection before forcing a new
// connection to be opened.
const maxBadConnRetries = 2
// ErrBadConn should be returned by a driver to signal to the sql
// package that a driver.Conn is in a bad state (such as the server
// having earlier closed the connection) and the sql package should
// retry on a new connection.
//
// To prevent duplicate operations, ErrBadConn should NOT be returned
// if there's a possibility that the database server might have
// performed the operation. Even if the server sends back an error,
// you shouldn't return ErrBadConn.
var ErrBadConn = errors.New("driver: bad connection")
// ExecContext executes a query without returning any rows.
// The args are for any placeholder parameters in the query.
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error) {
var res Result
var err error
for i := 0; i < maxBadConnRetries; i++ {
res, err = db.exec(ctx, query, args, cachedOrNewConn)
if err != driver.ErrBadConn {
break
}
}
if err == driver.ErrBadConn {
return db.exec(ctx, query, args, alwaysNewConn)
}
return res, err
}