最近基于gin尝试写了一套内部使用的框架(如下),过程中有一些总结笔记在此记录。
1、打开连接如MySQL,redis要记得close,否则容易内存泄漏;
2、for循环的时候 要注意 最好显式close。其实当我们通过for循环迭代数据库的时候,当迭代到最后一样数据的时候,会出发一个io.EOF的信号,引发一个错误,同时go会自动调用rows.Close方法释放连接,然后返回false,此时循环将会结束退出。通常你会正常迭代完数据然后退出循环。可是如果并没有正常的循环而因其他错误导致退出了循环。此时rows.Next处理结果集的过程并没有完成,归属于rows的连接不会被释放回到连接池。因此十分有必要正确的处理rows.Close事件。如果没有关闭rows连接,将导致大量的连接并且不会被其他函数重用,就像溢出了一样。最终将导致数据库无法使用。所以为了避免这种情况的发生,最好的办法就是显示的调用rows.Close方法,确保连接释放,又或者使用defer指令在函数退出的时候释放连接,即使连接已经释放了,rows.Close仍然可以调用多次,是无害的。rows.Next循环迭代的时候,因为触发了io.EOF而退出循环。为了检查是否是迭代正常退出还是异常退出,需要检查rows.Err。
3、切片可以理解为不定长数组 ,map可以理解为 key:value 对格式;map声明后不能直接进行赋值,只有初始化后才能进行赋值操作。
4、gin中件间拦截请求,总在路由请求之前执行,即 http拦截器,再到路由逻辑。在注册中间件代码之后的路由才生效中间件规则;
5、全局变量或者 路由之外(main函数)的东西,都会一直保存值不消失(常驻内存),要特别注意内存泄露。启动后就不会重新赋值,而不是每次请求(生命周期) 会重新赋值,注意。实践证明:不是每次请求都重新清空赋值的,特别是全局变量;比如DB全局句柄实例的close不同请求会互相影响。还有一个指针取地址要特别小心共享问题。
6、结构体传地址和传值的区别,在于函数内改变形参值,会不会影响外部的实参。定义指针和形参 用*,函数内部则用& 代表取地址。指针赋值,这点原理和C指针相通。
7、只有大结构体,全局变量适合用指针传值(指针变量其实就是个指向变量的地址)。普通对象指针用多了会发生内存逃逸,导致gc压力过大。 go官方一些规范里面,也是推荐一般情况下使用传值。
8、真实内存占用量就是物理内存占用量减去buffer和cache的占用量。
9、MySQL库自带连接池,使用方不需自行实现。*sql.DB 线程安全,开箱即用,屏弊了底层创建连接的实现
Open 只是创建类,调用一次即可,使用前需要 Ping 确保连接正常。
一定要设置连接池的两个参数 MaxIdle, MaxOpen,否则在极端情况会把 DB 连接打满(未加索引,大事务阻塞)。可选 MaxLifetime,需咨询 DBA,一般 DB 默认8小时,无需设置,如果很短要视情况而定
事务会占用一个连接,尽可能减小事务耗时,打散大事务,否则会将 DB 连接数打满
prepare 会占用一个连接,每次使用完后,一定要 close ,否则同样会将连接数打满
DSN 需要指定时区和对时间字段的支持,否则会出现时间提前8小时的问题
Query, Prepare, Exec 无需业务层重试,底层已经实现
使用var != nil判断实现的懒汉式单例并不是线程安全的,注意GO的单例有多种实现方式。
10、init函数在加载包的时候也就是编译服务启动的时候就已经执行了而不是等执行HTTP请求的时候执行到路由才执行。所有init函数是接收不到query参数来初始化的。
11、redigo 连接池pool:TCP 连接保持, 客户端和服务端各自都保持不断开,可以重用TCP。
12、关于接口:
接口可以等于任何一个实现它的 结构体的实例,然后调用实例 中的 接口方法。比如定义cache接口,用Redis和M实例来各自实现接口方法。比如 redigo包。空接口= 没有方法的接口,接口变量可以等于任何东西,任何东西默认都实现了空接口。已经基础入门。
13、闭包就是对 变量有个全局的 作用。解决 变量作用域问题。
14、go build 打包命令
go build -o test example/example.go 命令啥意思
go build主要用于编译代码
打包命令,格式:go build [-o output] [-i] [build flags] [packages]
其中:-o 指定打包后输出的文件名.exe,文件名后面跟的是需要打包的.go文件
打包方式1:
切换到项目目录下,执行打包命令:
cd $GOPATH/src/myfirstgo
go build
此时生成的打包文件名为项目目录名,这里为myfirstgo
也可以指定输出包名,示例: go build -o abc
或者 go build -o abc main.go
打包方式2:
在其他目录执行以下命令:
go build myfirstgo 此时在当前执行命令目录生成myfirstgo可执行文件
指定输出包名,示例: go build -o xyz myfirstgo 此时在当前执行命令目录生成xyz可执行文件
在非go项目目录下执行”go build 路径+项目名“命令查找的项目目录从$GOPATH/src/开始
15、注意if写法或者遍历时,变量的作用域。
比如如下写法是编译不通过的:
func JsonStr2Map(str string)(map[string]interface{},error){
var dat map[string]interface{}
if err :=json.Unmarshal([]byte(str), &dat);err !=nil {
return nil,err
}
return dat,err
}
正确写法:
func JsonStr2Map(str string)(map[string]interface{},error){
var dat map[string]interface{}
if err :=json.Unmarshal([]byte(str), &dat);err !=nil {
return nil,err
}else {
return dat,err//注意err作用域
}
}
16、json转结构体索引问题
type Email struct {
Email string `json:"email_address"`
}
type EmailsList struct {
IsSchemaConforming bool `json:"isSchemaConforming"`
SchemaVersion int `json:"schemaVersion"`
Emails []Email `json:"unknown.0"`
}
jsonStr := `{"isSchemaConforming":true,"schemaVersion":0,"unknown.0":[{"email_address":"[email protected]"},{"email_address":"[email protected]"}]}`
emails := EmailsList{}
json.Unmarshal([]byte(jsonStr), &emails)
email0 := emails.Emails[0].Email //注意这里emails.Emails["0"].Email 和emails.Emails['0'].Email 都不正确,注意它们的区别
17、注意if语句代码块中变量的作用域为局部的
以下两种写法的区别:
v,ok :=sActivityId.(string)
if !ok {
return nil,errors.New("type err")
}
return v
以下代码编译错误:
if v,ok :=sActivityId.(string);!ok{
return nil,errors.New("type err")
}
return v
18、断言的语法是接口类型的,其他类型无法使用断言语法。数组值为接口类型的字符串,strconv.ParseInt(v,10,64) string转int行传参时必须先将接口类型断言为string。
19、golang中new和make的区别
make 仅用来分配及初始化类型为 slice、map、chan 的数据。new 可分配任意类型的数据(包括结构体).
new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type.
new 分配的空间被清零, make 分配空间后,会进行初始化.
总结
new 的作用是初始化一个指向类型的指针(*T),make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。
func new(Type) *Type 自己实现一个类似 new 的功能:
func newInt() *int {
var i int
return &i
}
someInt := newInt()
我们这个函数的功能跟 someInt := new(int) 一模一样。所以在我们自己定义 new 开头的函数时,出于约定也应该返回类型的指针。
func make(Type, size... IntegerType) Type 内建函数 make 用来为 slice,map 或 chan 类型分配内存和初始化一个对象(注意:只能用在这三种类型上),跟 new 类似,第一个参数也是一个类型而不是一个值,跟 new 不同的是,make 返回类型的引用而不是指针,而返回值也依赖于具体传入的类型,具体说明如下:
Slice: 第二个参数 size 指定了它的长度,它的容量和长度相同。
你可以传入第三个参数来指定不同的容量值,但必须不能比长度值小。
比如 make([]int, 0, 10)
Map: 根据 size 大小来初始化分配内存,不过分配后的 map 长度为 0,如果 size 被忽略了,那么会在初始化分配内存时分配一个小尺寸的内存
Channel: 管道缓冲区依据缓冲区容量被初始化。如果容量为 0 或者忽略容量,管道是没有缓冲区的
20、初始化结构体的三种方法
a:=new(user)
b:=&user{}
c:=user{}
type user struct {
id int `1123`
}
func main() {
a := &user{}
a.id = 111
b := user{}
b.id = 222
c := new(user)
c.id = 333
fmt.Println(a, b, c)
}
运行后结果如下
&{111} {222} &{333}
a:=new(user) <== 创建user变量,并且清零 b:=&user{} <== 返回的是user变量的指针 c:=user{} <== 创建user变量,三种写法的效果完全一样,区别只是 语法糖 的差异而已。
小马不苟同上面这句话,虽然从上面结果看三种写法效果是一样的,但是本质有区别。a := &user{}是取地址,c := new(user)是取地址并清零,b := user{}是拷贝。