环境搭建
编译器安装
1、编译器下载地址
2、打开命令行模式,输入go version
ide安装
ide下载地址
依赖管理
goproxy
1、goproxy代理地址
// 阿里云
https://mirrors.aliyun.com/goproxy
// 微软
https://goproxy.io
// 七牛
https://goproxy.cn
2、ide配置goproxy
gopath
go mod
1、创建包管理项目
- goland-ide新版本,创建go mod项目,File > New > Project > Go
- goland-ide旧版本,创建go mod项目,File > New > Project > Go modules(vgo)
2、实例教程
package mainimport "github.com/gin-gonic/gin"func main() {r := gin.Default()r.GET("/ping", func(c *gin.Context) {c.JSON(200, gin.H{"message": "pong",})})r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
执行go run main.go后,go.mod文件内容
module go-quick-start-guidego 1.14require github.com/gin-gonic/gin v1.7.7
gopath文件夹内容
go-基础-教程
标准库文档
Golang标准库文档
跨平台编译
1、编译
Windows下编译Linux平台的64位可执行程序
SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go
Mac下编译Linux平台的64位可执行程序
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
2、实例教程
背景:业务需求,要处理历史数据,本地写好脚本,然后打包到生产环境运行
(1)go build编译Linux平台的64位可执行程序
(2)登录linux服务器,使用“rz”命令上传Linux平台的64位可执行程序
(3)chmod赋予文件可执行权限,然后运行
常见问题
解决问题
go build打包脚本时,GOOS变量和GOARCH变量没有设置正确,导致在linux服务器上无法运行
数据类型
1、数字类型
2、字符串类型
3、布尔型
4、派生类型:指针、数组、切片、结构体、map、函数、管道、接口
变量-crud
1、变量定义
- 第一种方法:指定变量类型,如果变量没有初始化,则变量默认为零值
// 声明变量
var variable_name type
// 初始化变量
variable_name = value
- 第二种方法:编译器根据值自行判定变量类型
var variable_name = value
- 第三种方法:使用
:=
声明变量并初始化(推荐)
variable_name := value
2、实例教程
package mainimport ("fmt""runtime""unsafe"
)func main() {// 变量定义// 第一种,指定变量类型,如果没有初始化,则变量默认为零值var teacherName stringteacherName = "zhang"fmt.Printf("teacherName:%s\n", teacherName)// 第二种,编译器根据值自行判定变量类型var studentName = "li"fmt.Printf("studentName:%s\n", studentName)// 第三种,使用` :=` 声明变量并初始化parentName := "wang"fmt.Printf("parentName:%s\n", parentName)// 变量占用内存// output: string:16fmt.Printf("string:%v\n", unsafe.Sizeof(teacherName))var num int32// output: int32:4fmt.Printf("int32:%v\n", unsafe.Sizeof(num))var count int// output: cpu:amd64, int:8// 64位cpu下int是8个字节,32位cpu下int是4个字节fmt.Printf("cpu:%v, int:%v\n", runtime.GOARCH, unsafe.Sizeof(count))
}
3、知识点
- 变量已经使用 var 声明过了,再使用
:=
声明变量,就产生编译错误 - 指定变量类型,如果没有初始化,则变量默认为零值
- 整型 int 、浮点型 float 默认值为0
- 布尔类型默认值为false
- 字符串类型默认值为""
- 其他类型默认值为nil
变量-权限
1、基础知识
go语言是根据首字母的大小写来确定是否可以跨包访问。如果首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用
2、实例教程
config.go
package configimport "time"var (// other package can not access this variableteacherName = "zhang"// other package can access this variableStudentName = "li"
)// other package can not access this struct
type config struct {id int64name string
}// other package can access this struct
type Activity struct {Id int64Name stringStartTime *time.TimeEndTime *time.Time
}// other package can not access this function
func getConfig() (c *config) {return
}// other package can access this function
func ReadConfig() (activity *Activity) {return
}
main.go
package mainimport ("fmt""go-quick-start-guide/go-tutorial/go-variable/go-variable-permission/config"
)func main() {fmt.Printf("studentName:%s\n", config.StudentName)fmt.Printf("activity:%+v\n", config.ReadConfig())
}
常量
1、常量定义
- 省略type时,表示编译器根据值自行判定变量类型
const variable_name [type] = value
2、类型定义
类型定义是在底层类型的基础上创建全新的类型,但二者并不能相互赋值,只能类型转换
type NewType BaseType
3、iota
(1)iota只能在常量的表达式中使用
(2)每次 const 出现时,都会让 iota 初始化为0
type RoleType intconst (TeacherRole RoleType = iota + 1 // 1StudentRole // 2ParentRole // 3
)
4、实例教程
package mainimport "fmt"type RoleType intconst (TeacherRole RoleType = iota + 1 // 1StudentRole // 2ParentRole // 3
)func main() {fmt.Printf("teacherRole:%v\n", TeacherRole)fmt.Printf("studentRole:%v\n", StudentRole)fmt.Printf("parentRole:%v\n", ParentRole)var teacherRole int// RoleType是在int类型上定义的新数据类型,但二者并不能相互赋值,只能类型转换teacherRole = int(TeacherRole)fmt.Printf("teacherRole:%v\n", teacherRole)
}
类型转换
1、底层结构相同的两个类型之间互转
(1)type定义的新数据类型和原数据类型互转。
(2)同一数据类型,精度不同。低精度转为高精度是安全的,高精度转为低精度会丢失精度。
package mainimport "fmt"type RoleType intconst (TeacherRole RoleType = iota + 1 // 1StudentRole // 2ParentRole // 3
)func main() {// RoleType是在int类型上定义的新数据类型,但二者并不能相互赋值,只能类型转换var teacherRole intteacherRole = int(TeacherRole)fmt.Printf("teacherRole:%v\n", teacherRole)// 同一数据类型,精度不同timeUnix := time.Now().UnixNano() / 1e6// output: timeUnix:1650252338922,type:int64,int32(timeUnix):984897258,type:int32fmt.Printf("timeUnix:%v,type:%T,int32(timeUnix):%v,type:%T\n", timeUnix, timeUnix, int32(timeUnix), int32(timeUnix))
}
2、底层结构不相同的两个类型之间互转
(1)string与int互相转换:strconv包提供了简单数据类型之间的类型转换功能
package mainimport ("fmt""strconv"
)func main() {// string和int类型互相转换// string转成introomId := "2312"if id, err := strconv.Atoi(roomId); err != nil {fmt.Printf("string to int fail,roomId:%v,err:%v\n", roomId, err)} else {// output: string to int success,id:2312,type:intfmt.Printf("string to int success,id:%v,type:%T\n", id, id)}// string转成int64if id, err := strconv.ParseInt(roomId, 10, 64); err != nil {fmt.Printf("string to int64 fail,roomId:%v,err:%v\n", roomId, err)} else {// output: string to int64 success,id:2312,type:int64fmt.Printf("string to int64 success,id:%v,type:%T\n", id, id)}// int转成stringvar userId int = 125432// output: int to string,id:125432,type:stringfmt.Printf("int to string,id:%v,type:%T\n", strconv.Itoa(userId), strconv.Itoa(userId))// int64转成stringvar cardId int64 = 3454345// output: int to string,id:3454345,type:stringfmt.Printf("int to string,id:%v,type:%T\n", strconv.FormatInt(cardId, 10), strconv.FormatInt(cardId, 10))
}
指针
指针变量指向变量的内存地址
指针变量定义格式
第一种方法:使用“*type”声明指针变量
var variable_name *type
第二种方法:使用“&variable_name”取变量的地址
var variable_name type
ptr := &variable_name
使用“*指针变量”访问对应内存地址所存储的变量值
value := *variable_name
实例教程
代码示例:
package mainimport "fmt"func main() {teacherName := "张三"// 声明指针变量var ptr *string// &表示取一个变量的地址ptr = &teacherNamefmt.Printf("teacherName变量的地址是: %x\n", &teacherName )fmt.Printf("ptr变量的指针地址: %x\n", ptr)// 使用 *指针变量,可取出对应内存地址所存储的变量值fmt.Printf("ptr变量储存的变量值: %s\n", *ptr)
}
输出结果:
teacherName变量的地址是: c0000381f0
ptr变量的指针地址: c0000381f0
ptr变量储存的变量值: 张三
普通指针类型、unsafe.Pointer、uintptr之间的关系
uintptr
:用于指针运算, uintptr 无法持有对象,不可以访问变量值普通指针类型
:指向变量的内存地址,可以访问变量值,但不能进行指针运算unsafe.Pointer
:通用指针类型。起桥梁作用,可以让任意类型的指针实现相互转换。例如,普通指针类型不能直接转换成uintptr,可以把普通指针类型转换成unsafe.Pointer,unsafe.Pointer再转换成uintptr。
数组-crud
1、数组定义
size
:数组元素的个数type
:数组元素的类型
第一种方法:声明数组并初始化
var variable_name = [size]type{element0, element1...elementn}
第二种方法:使用:=
声明数组并初始化
variable_name := [size]type{element0, element1...elementn}
第三种,使用 ...
代替数组的长度,编译器会根据元素个数自行推断数组的长度
variable_name := [...]type{element0, element1...elementn}
2、数组的遍历
index
:数组元素的下标value
:数组元素的下标对应的数值
for index,value := range variable_name {函数体
}
3、实例教程
package mainimport "fmt"func main() {// 声明并初始化数组names := [3]string{"zhang", "li", "wang"}// 遍历数组for i, _ := range names {fmt.Printf("index:%v,value:%v\n", i, names[i])}
}
数组-字节数组
实例教程
package mainimport ("encoding/binary""fmt""sync/atomic""time"
)var orderIdCounter = uint32(time.Now().UnixNano())type OrderId stringfunc main() {// int类型与byte转换时,只能将int类型最低八位二进制的数据转换为字节数据var userId int32 = 2254// | 00000000 | 00000000 | 00001000 | 11001110 |// | value=0 | value=0 | value=8 | value=206|toByte := byte(userId)// output: userId toByte:206fmt.Printf("userId toByte:%v\n", toByte)toByte = byte(userId >> 8)// output: userId >> 8 toByte:8fmt.Printf("userId >> 8 toByte:%v\n", toByte)toByte = byte(userId >> 16)// output: userId >> 16 toByte:0fmt.Printf("userId >> 16 toByte:%v\n", toByte)// 大端模式:高位字节存放在内存的低地址,低位字节存放在内存的高地址var userIdByte [4]bytebinary.BigEndian.PutUint32(userIdByte[:], uint32(userId))// output: Big-Endian userIdByte:[0 0 8 206]fmt.Printf("BigEndian userIdByte:%v\n", userIdByte)// 小端模式:高位字节存放在内存的高地址,低位字节存放在内存的低地址binary.LittleEndian.PutUint32(userIdByte[:], uint32(userId))// output: LittleEndian userIdByte:[206 8 0 0]fmt.Printf("LittleEndian userIdByte:%v\n", userIdByte)orderId := generateOrderId(userId)fmt.Printf("generateOrderId:%x\n", orderId)uId := orderId.getUserId()fmt.Printf("getUserId:%v\n", uId)
}func generateOrderId(userId int32) OrderId {var orderIdByte [12]byte// timestamp, 4 bytes, big endianbinary.BigEndian.PutUint32(orderIdByte[0:4], uint32(time.Now().Unix()))// userId, 4 bytes, big endianbinary.BigEndian.PutUint32(orderIdByte[4:8], uint32(userId))// increment, 4 bytes, big endiani := atomic.AddUint32(&orderIdCounter, 1)binary.BigEndian.PutUint32(orderIdByte[8:12], i)return OrderId(orderIdByte[:])
}func (orderId *OrderId) getUserId() int32 {userIdByte := []byte(*orderId)[4:8]return int32(uint32(userIdByte[0])<<24 | uint32(userIdByte[1])<<16 | uint32(userIdByte[2])<<8 | uint32(userIdByte[3]))
}
切片-crud
数组的长度不可改变,切片是长度可变的数组。切片是引用类型,对应的是引用拷贝。
1、切片底层原理
- Data:指向底层数组的指针
- Len:切片的长度,len() 方法获取切片长度
- Cap:切片的容量, cap() 方法获取切片容量
type Slice struct {Data unsafe.PointerLen intCap int
}
2、切片定义格式
第一种方法:切片只声明而没有初始化,那么这个切片的默认值为nil
var variable_name []type
第二种方法:使用 make 函数来创建切片
- 使用make创建切片时,len是切片的长度,是必填参数,cap是切片的容量,是可选参数
variable_name := make([]type, len, cap)
第三种方法:切片声明并初始化
variable_name :=[]type{element0, element1...elementn}
3、切片追加元素
append() 方法可以为切片追加一个元素
ids := make([]int, 0)
ids = append(ids, 1)
4、访问切片指定位置元素
切片是动态数组,可以通过下标来访问切片中指定元素
index
:切片元素的下标
value := variable_name[index]
5、遍历切片的元素
- index:切片元素的下标
- value:切片元素的下标对应的值
for index,value := range variable_name {函数体
}
for-range遍历切片的原理
参考编译器源代码:https://github.com/golang/gofrontend
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
原理解释:先对要遍历的切片做一个引用拷贝,获取切片长度,然后使用常规for循环进行遍历,如果for-range中接收index和value的话,则会对index和value进行一次赋值
6、切片截断或删除切片元素
(1)切片截断
// 将 slice_name 中从下标 start 到 end-1 下的元素创建为一个新的切片
slice_name := slice_name[start:end]
(2)删除切片元素
ids := make([]int, 0)
// 切片删除元素
// 切片删除第一个元素
ids = ids[1:]
// 切片删除最后一个元素
ids = ids[:len(ids)-1]
// 切片删除中间某个元素
deleteIndex := 4
ids = append(ids[:deleteIndex], ids[deleteIndex+1:]...)
7、实例教程
package mainimport "fmt"func main() {ids := make([]int, 0)// 切片添加元素// 切片添加单个元素ids = append(ids, 1)// 切片添加多个元素ids = append(ids, 2, 3, 4)// 切片添加另一个切片中的元素numbers := []int{5, 6, 7, 8, 9, 10, 11, 12}ids = append(ids, numbers...)// 切片删除元素// 切片删除第一个元素ids = ids[1:]// 切片删除最后一个元素ids = ids[:len(ids)-1]// 切片删除中间某个元素deleteIndex := 4ids = append(ids[:deleteIndex], ids[deleteIndex+1:]...)// 切片修改元素updateIndex := 3updateValue := 20ids[updateIndex] = updateValue// 遍历切片元素for i, v := range ids {fmt.Printf("index:%v,value:%v\n", i, v)}// 切片拷贝userIds := make([]int, len(ids))copy(userIds, ids)fmt.Printf("source data:%v, destination data:%v", ids, userIds)
}
输出结果:
index:0,value:2
index:1,value:3
index:2,value:4
index:3,value:20
index:4,value:7
index:5,value:8
index:6,value:9
index:7,value:10
index:8,value:11
source data:[2 3 4 20 7 8 9 10 11], destination data:[2 3 4 20 7 8 9 10 11]
切片-append
实例教程
package mainimport "fmt"func main() {loadFromDataBase := []int{1, 2, 3, 4, 5, 6}// wrong usageuserIds := make([]int, len(loadFromDataBase))userIds = append(userIds, loadFromDataBase...)// output: userIds:[0 0 0 0 0 0 1 2 3 4 5 6],expected len:6,virtual len:12fmt.Printf("userIds:%v,expected len:%v,virtual len:%v\n", userIds, len(loadFromDataBase), len(userIds))// correct usageids := make([]int, 0)ids = append(ids, loadFromDataBase...)// ids:[1 2 3 4 5 6],expected len:6,virtual len:6fmt.Printf("ids:%v,expected len:%v,virtual len:%v\n", ids, len(loadFromDataBase), len(ids))// correct usageuIds := make([]int, len(loadFromDataBase))for i, _ := range loadFromDataBase {uIds[i] = loadFromDataBase[i]}// ids:[1 2 3 4 5 6],expected len:6,virtual len:6fmt.Printf("uIds:%v,expected len:%v,virtual len:%v\n", uIds, len(loadFromDataBase), len(uIds))
}
思考一下
提问:将[]int{1, 2, 3, 4, 5, 6}逐个添加到userIds切片中,明明只有6个元素,怎么到userIds切片就变成12个元素了,到底是哪个环节出问题了?
解答:userIds := make([]int, len(loadFromDataBase)),申请6个元素大小的内存空间并初始化为默认值。append方法是往userIds len长度的后面追加元素,即userIds[6] = 1、userIds[7] = 2,而不是往dataList的第一个位置追加元素,不正确使用make方法和append方法,可能会导致内存加倍。
正确用法
(1)ids := make([]int, 0),然后使用append方法追加元素
(2)uIds := make([]int, len(loadFromDataBase)),然后for循环依次赋值
切片-反射
实例教程
package mainimport ("fmt""reflect"
)func main() {names := []string{"zhang", "li", "wang"}v := reflect.ValueOf(names)t := reflect.TypeOf(names)if t.Kind() == reflect.Slice {if v.IsNil() {return}// slice valuefmt.Printf("names:%v\n", v.Interface())// traverse slice elementfor i := 0; i < v.Len(); i++ {fmt.Printf("index:%v,value:%v\n", i, v.Index(i))}}
}
切片-指针运算
实例教程
package mainimport ("fmt""unsafe"
)type people struct {Name string
}func main() {names := []string{"zhang", "li", "wang"}// output: names element type:*stringfmt.Printf("names element type:%T\n", &names[0])for i := 0; i < len(names); i++ {value := (*string)(unsafe.Pointer(uintptr(unsafe.Pointer(&names[0])) + uintptr(i)*unsafe.Sizeof(names[0])))fmt.Printf("index:%v,value:%v\n", i, *value)}peoples := make([]*people, 0)peoples = append(peoples, &people{Name: "zhang"}, &people{Name: "li"}, &people{Name: "wang"})// output: peoples element type:**main.peoplefmt.Printf("peoples element type:%T\n", &peoples[0])for i := 0; i < len(peoples); i++ {value := (**people)(unsafe.Pointer(uintptr(unsafe.Pointer(&peoples[0])) + uintptr(i)*unsafe.Sizeof(peoples[0])))// *value will panicfmt.Printf("index:%v,value:%+v\n", i, **value)}
}
切片-内存泄漏
实例教程
package mainimport ("fmt""runtime"
)var peoples = make([]*people, 0)type people struct {Name string
}func main() {peoples = append(peoples, &people{Name: "zhang"}, &people{Name: "li"}, &people{Name: "wang"})// output: peoples len:3,cap:4fmt.Printf("peoples len:%v,cap:%v\n", len(peoples), cap(peoples))// 切片是全局变量时,执行删除操作后,被删除的元素在底层数组还是存在的,只是对切片peoples不可见,这就导致内存的泄漏deleteIndex := 2deleteAddr := &peoples[deleteIndex]fmt.Printf("delete data::%+v\n", peoples[2])// peoples[deleteIndex] = nil can solve memory leakpeoples = append(peoples[:deleteIndex], peoples[deleteIndex+1:]...)runtime.GC()fmt.Printf("access delete data:%+v\n", **deleteAddr)
}
切片-打印输出
实例教程
package mainimport ("fmt""time"
)// Order 订单信息
type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量CreateTime time.Time // 创建时间UpdateTime time.Time // 修改时间
}func main() {orders := make([]*Order, 0)orders = append(orders, &Order{UserId: 12,ProductId: 34,Count: 2,CreateTime: time.Now(),UpdateTime: time.Now(),}, &Order{UserId: 12,ProductId: 34,Count: 2,CreateTime: time.Now(),UpdateTime: time.Now(),})// 打印输出切片结构体指针时,只有一种方法,那就是遍历切片结构体指针,逐个打印输出printSliceData(orders)
}func printSliceData(orders []*Order) {for _, v := range orders {fmt.Printf("value:%+v\n", v)}
}
切片-函数参数
实例教程
package mainimport "fmt"func main() {ids := []int{1, 2, 3, 4, 5, 6}fmt.Printf("ids:%v\n", ids)sliceOp(ids)// output: after append element 7,ids:[1 2 3 4 5 6]fmt.Printf("after sliceOp,append element 7,ids:%v\n", ids)sliceOpByPointer(&ids)fmt.Printf("after sliceOpByPointer,append element 8,ids:%v\n", ids)
}func sliceOp(data []int) {data = append(data, 7)
}func sliceOpByPointer(data *[]int) {*data = append(*data, 8)
}
思考一下
提问:sliceOp函数中,往切片追加元素7,函数返回后,切片ids却找不到元素7,切片是引用拷贝,没问题啊?
解答:切片有三个字段:指向底层数组的指针Data字段、切片的长度Len字段、切片的容量Cap字段。Len字段和Cap字段是int类型的,对应值拷贝。在sliceOp函数中append元素,即修改切片data的Len字段和Cap字段,但是不会影响到切片ids的Len字段和Cap字段。
正确用法
函数参数由[]int改为*[]int,函数内部修改了Len字段和Cap字段,也会影响到原先切片的Len字段和Cap字段
切片-扩容
实例教程
package mainimport "fmt"func main() {ids := []int{1, 2, 3, 4, 5}userIds := idsfmt.Printf("before append element,ids:%v,len:%v,cap:%v\n", ids, len(ids), cap(ids))fmt.Printf("before append element,userIds:%v,len:%v,cap:%v\n", userIds, len(userIds), cap(userIds))ids = append(ids, 6)// 超过切片容量时,当原切片长度小于1024时,新切片的容量会直接翻倍// 切片扩容,就会创建新的数组,故切片ids和切片userIds不再共用同一个底层数组fmt.Printf("after append element,ids:%v,len:%v,cap:%v\n", ids, len(ids), cap(ids))fmt.Printf("after append element,userIds:%v,len:%v,cap:%v\n", userIds, len(userIds), cap(userIds))// 超过切片容量时,当原切片的容量大于等于1024时,新切片的容量会增加25%idList := make([]int, 0)for i := 0; i < 1024; i++ {idList = append(idList, i)}fmt.Printf("before append element,idList len:%v,cap:%v\n", len(idList), cap(idList))idList = append(idList, 1024)fmt.Printf("after append element,idList len:%v,cap:%v\n", len(idList), cap(idList))
}
知识点
超过切片容量时,当原切片长度小于1024时,新切片容量会直接翻倍,当原切片的容量大于等于1024时,新切片的容量会增加25%。切片扩容,就会创建新的数组,不会再共用原先切片的底层数组。
结构体-crud
1、结构体定义
type structName struct {member1 type...membern type
}
2、结构体变量
方法一:初始化结构体变量,并赋值
variableName := structName { key1: value1, key2: value2..., keyn: valuen}
方法二:new()初始化结构体变量,new()方法返回值是结构体指针
variableName := new(structName)
3、实例教程
package mainimport ("fmt""time"
)type Activity struct {Name string // 活动名称Blacklist []int64 // 黑名单用户StartTime *time.Time // 活动开始时间EndTime *time.Time // 活动结束时间UserDetails map[int]*UserInfo // 参与用户信息
}type UserInfo struct {Name string // 用户名称Level int64 // 用户等级
}func main() {// 初始化结构体变量并赋值activity := &Activity{Name: "活动",Blacklist: []int64{1, 2, 3, 4},}fmt.Printf("activity:%+v\n", activity)// new()初始化结构体成员为默认值,返回值是结构体指针info := new(Activity)info.Name = "活动"// nil切片可以使用appendinfo.Blacklist = append(info.Blacklist, 2, 3)// map=nil时直接使用会panicif info.UserDetails == nil {info.UserDetails = make(map[int]*UserInfo)}info.UserDetails[3] = &UserInfo{Name: "名字",Level: 2,}fmt.Printf("info:%+v\n", info)// 函数内部定义结构体并初始化lotteryActivity := struct {name stringrate float32}{"活动", 0.6}fmt.Printf("lotteryActivity:%+v\n", lotteryActivity)
}
4、知识点
new()初始化结构体成员为默认值,返回结构体指针。结构体内部含有切片成员时,nil切片可以直接使用append方法。结构体内部含有map成员时,必须使用make()为map成员分配内存才可以使用。
5、小技巧
使用go tools的go fmt file格式化结构体,即结构体成员名称左对齐、类型左对齐、注释左对齐
type Activity struct {Name string // 活动名称Blacklist []int64 // 黑名单用户StartTime *time.Time // 活动开始时间EndTime *time.Time // 活动结束时间UserDetails map[int]*UserInfo // 参与用户信息
}
结构体-嵌入
实例教程
package mainimport ("encoding/json""fmt"
)type Activity struct {Id int64 // 活动idName string // 活动名称Url string // 活动背景图Desc string // 活动介绍
}type LotteryActivity struct {ActivityRate float32 // 抽中概率PrizeId int64 // 奖品idCount int64 // 奖品数量
}func main() {// go has no inheritance, it is just compositionlotteryActivity := &LotteryActivity{Activity: Activity{Name: "活动名称",Desc: "活动介绍",},Rate: 0.3,PrizeId: 14,Count: 1,}// output: lotteryActivity:&{Activity:{Id:0 Name:活动名称 Url: Desc:活动介绍} Rate:0.3 PrizeId:14 Count:1}fmt.Printf("lotteryActivity:%+v\n", lotteryActivity)lottery, _ := json.Marshal(lotteryActivity)// output: lotteryActivity to json:{"Id":0,"Name":"活动名称","Url":"","Desc":"活动介绍","Rate":0.3,"PrizeId":14,"Count":1}fmt.Printf("lotteryActivity to json:%v\n", string(lottery))
}
结构体-标签
1、实例教程
package mainimport ("encoding/json""fmt""time"
)// LotteryActivity omitempty作用是在结构体转为json时,当该字段的值为该字段类型的零值时,忽略该字段
type LotteryActivity struct {ActivityId int64 `json:"activity_id"` // 活动idPrizeId int64 `json:"prize_id"` // 奖品idLotterySpendCoins int64 `json:"lottery_spend_coins"` // 抽奖花费金币数LotteryWinningRate float32 `json:"lottery_winning_rate"` // 抽中的概率CreateTime *time.Time `json:"create_time,omitempty"` // 创建时间UpdateTime *time.Time `json:"update_time,omitempty"` // 修改时间
}func main() {lotteryActivity := &LotteryActivity{ActivityId: 12,PrizeId: 13,LotterySpendCoins: 100,LotteryWinningRate: 0.3,}fmt.Printf("lotteryActivity:%+v\n", lotteryActivity)lottery, _ := json.Marshal(lotteryActivity)// output: lotteryActivity to json:{"activity_id":12,"prize_id":13,"lottery_spend_coins":100,"lottery_winning_rate":0.3}fmt.Printf("lotteryActivity to json:%s\n", string(lottery))
}
2、知识点
结构体字段命名,常采用驼峰式命名,如ActivityId。前后端交互、数据库数据存储,常采用下划线式命名,如activity_id。使用json tag标签即可实现驼峰式命名和下划线式命名相互转换。json标签中,omitempty作用是在结构体转为json时,当该字段的值为该字段类型的零值时,忽略该字段。
结构体-反射
实例教程
package mainimport ("fmt""reflect"
)// LotteryActivity 抽奖活动
type LotteryActivity struct {ActivityId int64 `json:"activity_id"` // 活动idPrizeId int64 `json:"prize_id"` // 奖品idLotterySpendCoins int64 `json:"lottery_spend_coins"` // 抽奖花费金币数LotteryWinningRate float32 `json:"lottery_winning_rate"` // 抽中的概率
}func main() {lotteryActivity := &LotteryActivity{ActivityId: 12,PrizeId: 23,LotterySpendCoins: 245,LotteryWinningRate: 0.2,}fmt.Printf("lotteryActivity:%+v\n", lotteryActivity)structReflectHandle(lotteryActivity)
}func structReflectHandle(reflectData interface{}) {v := reflect.ValueOf(reflectData)t := reflect.TypeOf(reflectData)if v.Kind() == reflect.Ptr {v = v.Elem()t = t.Elem()}if v.Kind() == reflect.Struct {for i := 0; i < v.NumField(); i++ {fv := v.Field(i)ft := t.Field(i).Tagfmt.Printf("index:%v,value:%v,tag:%s\n", i, fv.Interface(), ft.Get("json"))}}
}
结构体-指针运算
实例教程
package mainimport ("fmt""time""unsafe"
)type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量CreateTime time.Time // 创建时间UpdateTime time.Time // 修改时间
}func main() {order := &Order{UserId: 12,ProductId: 34,Count: 2,}fmt.Printf("order:%+v\n", order)fmt.Printf("UserId:%v\n", *(*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(order)) + unsafe.Offsetof(order.UserId))))fmt.Printf("ProductId:%v\n", *(*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(order)) + unsafe.Offsetof(order.ProductId))))fmt.Printf("Count:%v\n", *(*int64)(unsafe.Pointer(uintptr(unsafe.Pointer(order)) + unsafe.Offsetof(order.Count))))
}
结构体-空结构体
实例教程
package mainimport ("fmt""unsafe"
)func main() {// struct{}表示空结构体,不包含任何成员变量// struct{}{}表示这个变量的类型是struct{},其值是{},struct{}{}变量不占用内存// output: struct{}{}:0fmt.Printf("struct{}{}:%v\n", unsafe.Sizeof(struct{}{}))// go语言没有内置函数set,但是可以通过内置函数map来实现userIds := []int64{12, 23, 12, 35, 34, 65, 36}set := make(map[int64]struct{})for _, v := range userIds {set[v] = struct{}{}}fmt.Printf("set:%v\n", set)
}
结构体-方法
实例教程
package mainimport "fmt"type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量
}type OrderService struct {
}func (OrderService) createOrder(order *Order) {fmt.Printf("order:%+v", order)
}func (OrderService) payOrder(order *Order) {fmt.Printf("order:%+v", order)
}func main() {order := &Order{UserId: 12,ProductId: 234,Count: 1,}orderService := new(OrderService)orderService.createOrder(order)
}
map-crud
1、map定义
方法一:定义map变量,默认值为nil,但nil map不能直接使用
var variable_name map[key_type]value_type
方法二:使用make()定义map变量并分配内存空间
variable_name := make(map[key_type]value_type)
2、增删改查
(1)增加或更新key-value
variable_name["key"] = value
(2)删除key
delete(variable_name, "key")
(3)查询key-value
// ok=true,表示map集合中有这个key
val, ok := variable_name["key"]
(4)遍历
for i, v := range variable_name {fmt.Printf("key:%v,value:%+v\n", i, v)
}
3、实例教程
package mainimport "fmt"type Users struct {Id int64Name stringLevel int64Pic stringDesc string
}func main() {details := make(map[int64]*Users)details[2345] = &Users{Id: 2345,Name: "zhang",Level: 2,}details[4567] = &Users{Id: 4567,Name: "li",Level: 1,}if val, ok := details[2345]; ok {fmt.Printf("userId:2345,detail:%+v\n", val)}for i, v := range details {fmt.Printf("key:%v,value:%+v\n", i, v)}
}
map-容量
1、为什么map集合容量总是2的n次幂?
hash冲突解决-链地址法:取模运算,即hash(key) % capacity
当capacity是2的n次幂时,hash(key) % capacity = hash(key) & (capacity)
例如,hash(key)=18,capacity=16,hash(key) % capacity = 2,10010 & 01111 = 00010(十进制=2)
在计算机中直接求余效率不如位运算,当map集合容量总是2的n次幂,可以加快hash计算
函数-crud
1、函数定义
- go语言函数允许有多个返回值
func function_name( [parameter list] ) ( [return_types] ){
}
2、实例教程
package mainimport ("fmt"
)type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量
}func main() {orders, err := getOrders()if err != nil {fmt.Printf("getOrders fail, err:%s", err.Error())} else {printSliceData(orders)}
}func getOrders() (orders []*Order, err error) {orders = make([]*Order, 0)orders = append(orders, &Order{UserId: 12,ProductId: 14,Count: 3,})orders = append(orders, &Order{UserId: 15,ProductId: 17,Count: 3,})return
}func printSliceData(orderList []*Order) {for _, v := range orderList {fmt.Printf("order:%+v\n", v)}
}
函数-测试函数
1、测试函数
(1)文件命名规范为xxx_test.go
(2)测试函数
- 性能测试函数
func BenchmarkXxx(b *testing.B) {for i := 0; i < b.N; i++ {}
}
- 单元测试函数
func TestXxx(t *testing.T) {
}
(3)执行测试函数
- 执行单元测试函数:go test -v
- 执行性能测试函数:go test -bench="."
2、实例教程
package go_func_testimport ("encoding/json"jsoniter "github.com/json-iterator/go""testing""time"
)type Activity struct {Name string `json:"name"` // 活动名称Desc string `json:"desc"` // 活动介绍Pic string `json:"pic"` // 活动背景图BlackList []int64 `json:"black_list"` // 黑名单用户StartTime *time.Time `json:"start_time"` // 活动开始时间EndTime *time.Time `json:"end_time"` // 活动结束时间
}var (activityStruct = &Activity{Name: "活动名称",Desc: "活动介绍",Pic: "活动背景图",BlackList: []int64{2, 3, 4, 5},}activityJson = []byte(`{"name":"活动名称", "desc":"活动介绍","pic":"活动背景图","black_list":[2, 3, 4, 5]}`)
)// 使用 json-iterator 序列化库
func TestJsoniterUnmarshal(t *testing.T) {activity := new(Activity)err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(activityJson, activity)if err != nil {t.Errorf("jsonIterator.Unmarshal fail,err:%v\n", err)} else {t.Logf("activity:%+v\n", activity)}
}// 使用 json-iterator 序列化库
func BenchmarkJsoniterMarshal(b *testing.B) {b.ResetTimer()for i := 0; i < b.N; i++ {_, err := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(activityStruct)if err != nil {b.Errorf("jsonIterator.Marshal fail,err:%v\n", err)}}
}// 使用 json-iterator 序列化库
func BenchmarkJsoniterUnmarshal(b *testing.B) {activity := new(Activity)b.ResetTimer()for i := 0; i < b.N; i++ {err := jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(activityJson, activity)if err != nil {b.Errorf("jsonIterator.Unmarshal fail,err:%v\n", err)}}
}// 使用 goland-json 序列化库
func TestJsonUnmarshal(t *testing.T) {activity := new(Activity)err := json.Unmarshal(activityJson, activity)if err != nil {t.Errorf("jsonIterator.Unmarshal fail,err:%v\n", err)} else {t.Logf("activity:%+v\n", activity)}
}// 使用 goland-json 序列化库
func BenchmarkJsonMarshal(b *testing.B) {b.ResetTimer()for i := 0; i < b.N; i++ {_, err := json.Marshal(activityStruct)if err != nil {b.Errorf("json.Marshal fail,err:%v\n", err)}}
}// 使用 goland-json 序列化库
func BenchmarkJsonUnmarshal(b *testing.B) {activity := new(Activity)b.ResetTimer()for i := 0; i < b.N; i++ {err := json.Unmarshal(activityJson, activity)if err != nil {b.Errorf("json.Unmarshal fail,err:%v\n", err)}}
}
执行单元测试函数
PS G:\goland\go-quick-start-guide\go-tutorial\go-func\go-func-test> go test -v
=== RUN TestJsoniterUnmarshalTestJsoniterUnmarshal: json_test.go:36: activity:&{Name:活动名称 Desc:活动介绍 Pic:活动背景图 BlackList:[2 3 4 5] StartTime:<nil> EndTime:<nil>}
--- PASS: TestJsoniterUnmarshal (0.00s)
=== RUN TestJsonUnmarshalTestJsonUnmarshal: json_test.go:70: activity:&{Name:活动名称 Desc:活动介绍 Pic:活动背景图 BlackList:[2 3 4 5] StartTime:<nil> EndTime:<nil>}
--- PASS: TestJsonUnmarshal (0.00s)
PASS
ok go-quick-start-guide/go-tutorial/go-func/go-func-test 0.798s
执行性能测试函数
PS G:\goland\go-quick-start-guide\go-tutorial\go-func\go-func-test> go test -bench="."
goos: windows
goarch: amd64
pkg: go-quick-start-guide/go-tutorial/go-func/go-func-test
BenchmarkJsoniterMarshal-4 395286 2958 ns/op
BenchmarkJsoniterUnmarshal-4 500860 2452 ns/op
BenchmarkJsonMarshal-4 387691 2864 ns/op
BenchmarkJsonUnmarshal-4 100155 14236 ns/op
PASS
ok go-quick-start-guide/go-tutorial/go-func/go-func-test 6.814s
分析性能测试函数输出结果
(1)json-iterator 序列化性能和 goland-json 差不多
(2)json-iterator 反序列化性能远远超过 goland-json
函数-defer
1、基础教程
(1)defer语句执行时机:会在函数最后执行,但在return之前
在java语言中,try-catch-finally语句可以处理异常错误,即finally语句内的代码一定会执行的。在go语言中,不管函数内的代码发生各种异常,defer语句内的代码一定会执行的。
(2)defer语句应用场景:释放锁、关闭数据库连接、关闭文件流、错误恢复
2、实例教程
package mainimport ("fmt"
)type OrderStatus intconst (paymentUnpaid OrderStatus = iota + 1 // 未支付paymentInProgress // 支付中paymentSuccess // 支付成功paymentFailed // 支付失败
)type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量Status OrderStatus // 订单支付状态
}func main() {// 多个defer语句,按后进先出的方式执行multipleDefer()// defer、return、返回值三者的执行顺序:先给返回值赋值,后执行defer语句,最后return退出函数order := executionOrder()fmt.Printf("executionOrder:%+v\n", order)
}// 多个defer语句,按后进先出的方式执行
func multipleDefer() {// 延迟释放锁defer releaseLock()// 延迟关闭数据库连接defer closeDatabaseConnection()// 延迟关闭文件流defer closeFileStream()
}// defer、return、返回值三者的执行顺序:先给返回值赋值,后执行defer语句,最后return退出函数
func executionOrder() (order *Order) {order = &Order{UserId: 12,ProductId: 15,Count: 2,}defer func() {// 后执行defer语句,因为order是结构体指针,这个status = 3会直接影响到返回值,故此时返回值中status = 3order.Status = paymentSuccessfmt.Printf("updateOrderStatus:%+v\n", order)}()// 先给返回值赋值,此时返回值order字段中status = 0return order
}// 释放锁
func releaseLock() {fmt.Println("releaseLock")
}// 关闭数据库连接
func closeDatabaseConnection() {fmt.Println("closeDatabaseConnection")
}// 关闭文件流
func closeFileStream() {fmt.Println("closeFileStream")
}
函数-panic/recover
1、基础教程
(1)panic:可用 err error 返回程序执行过程中异常信息,但是有些异常信息,我们希望程序应当终止。例如,程序启动过程中,如果http监听绑定端口失败,这个程序应当panic,即终止进程。
(2)recover:数组越界、nil map put/get data等问题,会导致panic的,这个panic会一直往上抛出,如果没有哪个地方捕获这个异常,即整个进程会挂掉的。我们希望由于用户编程导致的意外panic,不要影响程序的运行,即不要挂掉进程,就需要用到recover错误恢复,recover的使用必须写在defer语句中。
2、实例教程
package mainimport ("fmt""net/http""runtime/debug""time"
)type Activity struct {Name string // 活动名称Blacklist []int64 // 黑名单用户StartTime *time.Time // 活动开始时间EndTime *time.Time // 活动结束时间UserDetails map[int]*UserInfo // 参与用户信息
}type UserInfo struct {Name string // 用户名称Level int64 // 用户等级
}func main() {// defer+recoveractivityHandle()// panic when http.ListenAndServe errerr := http.ListenAndServe("127.0.0.1:8088", nil)if err != nil {panic(err)}
}func activityHandle() (activity *Activity) {defer func() {if err := recover(); err != nil {fmt.Printf("err:%s\n", string(debug.Stack()))}}()activity = &Activity{Name: "活动名称",Blacklist: []int64{1, 3, 4, 5},}// nil map put data will panicactivity.UserDetails[12] = &UserInfo{Name: "用户名字",Level: 1,}return
}
输出结果
err:goroutine 1 [running]:
runtime/debug.Stack(0x0, 0x0, 0x0) G:/goland/go-sdk/go1.14/src/runtime/debug/stack.go:24 +0xa8
main.main.func1() G:/goland/go-quick-start-guide/go-tutorial/go-func/go-func-recover/main.go:25 +0x6a
panic(0x4e50c0, 0x5110e0) G:/goland/go-sdk/go1.14/src/runtime/panic.go:973 +0x3dc
main.activityHandle(0xc0000d0000) G:/goland/go-quick-start-guide/go-tutorial/go-func/go-func-recover/main.go:37 +0x1c9
main.main()G:/goland/go-quick-start-guide/go-tutorial/go-func/go-func-recover/main.go:28 +0x51
函数-闭包
1、基础知识
(1)匿名函数:没有函数名的函数
- 第一种:定义匿名函数,然后调用
// 匿名函数
sumFunc := func(a, b int) int {return a + b
}
// 调用匿名函数
fmt.Printf("sum:%v", sumFunc(1, 3))
- 第二种:定义匿名函数并调用
sum := func(a, b int) int {return a + b}(1, 3)
fmt.Printf("sum:%v\n", sum)
(2)闭包:匿名函数+上下文
函数getIncId返回值是一个函数,没有函数名,即返回值是一个匿名函数。匿名函数内部又和外部变量i有联系,即变量i是该匿名函数的上下文。
func getIncId() func() int {i := 0return func() int {i++return i}
}
匿名函数nextId依次调用输出1、2、3,匿名函数的上下文即变量i,在匿名函数的作用相当于全局变量。匿名函数nextId2依次调用输出1、2、3,而不是在匿名函数nextId的基础上输出4、5、6。匿名函数nextId和匿名函数nextId2都会维护自己的上下文即变量i。
// 闭包 = 匿名函数 + 上下文
nextId := getIncId()
fmt.Println(nextId()) // output: 1
fmt.Println(nextId()) // output: 2
fmt.Println(nextId()) // output: 3
nextId2 := getIncId()
fmt.Println(nextId2()) // output: 1
(3)闭包的作用:实现包装功能,常用作middlware
功能:在充值RechargeHandler之前校验accessToken
如何实现:实现一个包装函数,比如GetHttpHandler函数,统一在包装函数内部实现公共功能,比如校验用户ip、校验用户accessToken、记录业务HttpHandler函数耗时等,业务HttpHandler函数作为参数形式传给包装函数即可。
type WrapHttpFunc struct {HttpFunc func(http.ResponseWriter, *http.Request)CheckAccessToken bool
}func RechargeHandler(w http.ResponseWriter, r *http.Request) {
}func CheckAccessToken(w http.ResponseWriter, r *http.Request) (ok bool, err error) {return
}func GetHttpHandler(wrapHttpFunc *WrapHttpFunc) func(w http.ResponseWriter, r *http.Request) {return func(w http.ResponseWriter, r *http.Request) {if wrapHttpFunc.CheckAccessToken {ok, err := CheckAccessToken(w, r)if !ok {fmt.Printf("CheckAccessToken err:%s", err.Error())w.Write([]byte(err.Error()))}}wrapHttpFunc.HttpFunc(w, r)}
}func main() {// wrapping functions and creating middlewarehttp.HandleFunc("/recharge", GetHttpHandler(&WrapHttpFunc{HttpFunc: RechargeHandler,CheckAccessToken: true,}))if err := http.ListenAndServe(":8088", nil); err != nil {panic(err)}
}
2、实例教程
package mainimport ("fmt""net/http"
)type WrapHttpFunc struct {HttpFunc func(http.ResponseWriter, *http.Request)CheckAccessToken bool
}func getIncId() func() int {i := 0return func() int {i++return i}
}func main() {// 匿名函数sumFunc := func(a, b int) int {return a + b}// 调用匿名函数fmt.Printf("sum:%v\n", sumFunc(1, 3))// 闭包 = 匿名函数 + 上下文nextId := getIncId()fmt.Println(nextId()) // output: 1fmt.Println(nextId()) // output: 2fmt.Println(nextId()) // output: 3nextId2 := getIncId()fmt.Println(nextId2()) // output: 1// wrapping functions and creating middlewarehttp.HandleFunc("/recharge", GetHttpHandler(&WrapHttpFunc{HttpFunc: RechargeHandler,CheckAccessToken: true,}))if err := http.ListenAndServe(":8088", nil); err != nil {panic(err)}
}func RechargeHandler(w http.ResponseWriter, r *http.Request) {
}func CheckAccessToken(w http.ResponseWriter, r *http.Request) (ok bool, err error) {return
}func GetHttpHandler(wrapHttpFunc *WrapHttpFunc) func(w http.ResponseWriter, r *http.Request) {return func(w http.ResponseWriter, r *http.Request) {if wrapHttpFunc.CheckAccessToken {ok, err := CheckAccessToken(w, r)if !ok {fmt.Printf("CheckAccessToken err:%s", err.Error())w.Write([]byte(err.Error()))}}wrapHttpFunc.HttpFunc(w, r)}
}
包-导入
1、点操作(尽量不要用)
某个包导入之后,在调用这个包的函数时,可以省略包名
package mainimport (. "fmt"
)func main() {Println("point operation")
}
2、别名操作
给包起个别名
package mainimport (jsoniter "github.com/json-iterator/go"
)type ResponseBody struct {Code int `json:"code"`Message interface{} `json:"message,omitempty"`Body interface{} `json:"body,omitempty"`
}func main() {jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(&ResponseBody{Code: 123})
}
3、下划线操作
不直接使用包里的函数,而是调用该包里面的init函数
config.go
package configimport "fmt"func init() {fmt.Println("config init")
}
main.go
package mainimport (_ "go-quick-start-guide/go-tutorial/go-package/underscore/config"
)// output: config init
func main() {
}
接口-crud
1、接口定义
type interfaceName interface {methodName1 [returnType]...methodNamen [returnType]
}
2、实现接口
type interfaceName interface {methodName1 [returnType]
}type structName struct {}func (variableName structName) methodName1 [returnType] {}
3、实例教程
package mainimport "fmt"type Order struct {UserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量
}type OrderService interface {// 创建订单createOrder(order *Order) (err error)// 支付订单payOrder(order *Order) (err error)// 回滚订单rollbackOrder(order *Order) (err error)
}type TaoBaoOrderService struct {
}func (TaoBaoOrderService) createOrder(order *Order) (err error) {fmt.Printf("taoBao createOrder:%+v\n", order)return
}func (TaoBaoOrderService) payOrder(order *Order) (err error) {fmt.Printf("taoBao payOrder:%+v\n", order)return
}func (TaoBaoOrderService) rollbackOrder(order *Order) (err error) {fmt.Printf("taoBao rollbackOrder:%+v\n", order)return
}type JingDongOrderService struct {
}func (JingDongOrderService) createOrder(order *Order) (err error) {fmt.Printf("jingDong createOrder:%+v\n", order)return
}func (JingDongOrderService) payOrder(order *Order) (err error) {fmt.Printf("jingDong payOrder:%+v\n", order)return
}func (JingDongOrderService) rollbackOrder(order *Order) (err error) {fmt.Printf("jingDong rollbackOrder:%+v\n", order)return
}func main() {order := &Order{UserId: 12,ProductId: 234,Count: 1,}var orderService OrderServiceorderService = new(TaoBaoOrderService)orderService.createOrder(order)orderService = new(JingDongOrderService)orderService.createOrder(order)
}
接口-空接口
1、基础知识
(1)空接口是一个没有任何方法的接口,go语言中任何数据类型都实现了空接口。空接口作为函数的参数,可以接受任意类型的参数。
package mainimport "fmt"func main() {userId := 2343// output: type:int,data:2343print(userId)userName := "zhang"// output: type:string,data:zhangprint(userName)
}func print(data interface{}) {fmt.Printf("type:%T,data:%v\n", data, data)
}
(2)类型断言:v, ok := x.(T),将接口类型的值(x),转成类型(T)
package mainimport "fmt"type RechargeActivity struct {
}type LotteryActivity struct {
}func main() {activityConfig := make(map[int]interface{})activityConfig[123] = &RechargeActivity{}if rechargeActivity, ok := activityConfig[123].(*RechargeActivity); ok {fmt.Printf("rechargeActivity:%+v\n", rechargeActivity)}activityConfig[456] = &LotteryActivity{}if lotteryActivity, ok := activityConfig[456].(*LotteryActivity); ok {fmt.Printf("rechargeActivity:%+v\n", lotteryActivity)}
}
协程-crud
1、开启协程
go funtionName(params)
积分抽奖:扣除积分 -> 抽奖 -> 异步派发礼物
package mainfunc main() {pointsDraw()
}// 积分抽奖
func pointsDraw() {deductPoints()lottery()// 异步派发礼物go giftExclusion()
}// 扣除积分
func deductPoints() {
}// 抽奖
func lottery() {
}// 礼物派发
func giftExclusion() {
}
(1)一个程序启动时,主函数运行在main goroutine中。main函数结束时即main goroutine结束时,所有的goroutine都会结束。
只输出main goroutine over,并没有输出log print,因为main goroutine结束时,所有的goroutine都会结束
package mainimport ("fmt""time"
)func main() {go print("log print")fmt.Println("main goroutine over")
}func print(log string) {time.Sleep(1 * time.Second)fmt.Println(log)
}
协程-多协程同步
1、等待多个协程-sync.WaitGroup
(1)主协程通过Add方法设置等待协程的数量
(2)每个等待协程运行结束时调用Done方法
(3)主协程调用Wait方法阻塞程序,直到所有等待协程都完成任务
package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroup// 主协程通过Add方法设置等待协程的数量wg.Add(3)// 每个等待协程运行结束时调用Done方法go queryMetricsA(&wg)go queryMetricsB(&wg)go queryMetricsC(&wg)// 主协程调用Wait方法阻塞程序,直到所有等待协程都完成任务wg.Wait()combineMetrics()
}// 查询指标A
func queryMetricsA(wg *sync.WaitGroup) {defer wg.Done()fmt.Println("queryMetricsA")
}// 查询指标B
func queryMetricsB(wg *sync.WaitGroup) {defer wg.Done()fmt.Println("queryMetricsB")
}// 查询指标C
func queryMetricsC(wg *sync.WaitGroup) {defer wg.Done()fmt.Println("queryMetricsC")
}// 组合指标数据
func combineMetrics() {fmt.Println("combineMetrics")
}
管道-crud
1、创建管道
(1)创建无缓冲管道
无缓冲管道:往 channel 写入数据的 goroutine 会被阻塞住,直到有其他 goroutine 消费了 channel 里的数据。
channelName := make(chan type)
(2)创建有缓冲管道
有缓冲管道:当 channel 里的元素总数不大于这个可缓冲容量,往 channel 写入数据的 goroutine 就不会被阻塞住。
channelName := make(chan type, size)
2、写入数据
channelName <- value
(1)向已关闭的 channel 中写入数据会 panic
package main// panic: send on closed channel
func main() {data := make(chan int)close(data)data <- 123
}
3、读取数据
// 第一种
value := <- channelName
// 第二种,ok为false时表示管道已关闭
value, ok := <- channelName
(1)从已关闭的 channel 读取消息,若消息均已读出,则会读到类型的零值
package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroupwg.Add(2)data := make(chan int)go write(data, 123, &wg)// output: channel open,read channel:123go read(data, &wg)wg.Wait()close(data)wg.Add(1)// output: channel closed,read channel:0go read(data, &wg)wg.Wait()
}func write(data chan int, value int, wg *sync.WaitGroup) {defer wg.Done()data <- value
}func read(data chan int, wg *sync.WaitGroup) {defer wg.Done()if value, ok := <-data; ok {fmt.Printf("channel open,read channel:%v\n", value)} else {fmt.Printf("channel closed,read channel:%v\n", value)}
}
4、关闭管道
close(channelName)
(1)重复关闭同一个 channel 会 panic
package main// panic: close of closed channel
func main() {data := make(chan int)close(data)close(data)
}
5、range 遍历
for-range会一直从 channel 中读取数据,直到 channel closed 循环才会结束
package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroupwg.Add(2)data := make(chan int, 10)go writeMsg(&wg, data, 123, 456, 789)go readMsg(&wg, data)wg.Wait()
}func writeMsg(wg *sync.WaitGroup, data chan int, values ...int) {defer wg.Done()for i, _ := range values {data <- values[i]}close(data)
}func readMsg(wg *sync.WaitGroup, data chan int) {defer wg.Done()for v := range data {fmt.Printf("channel open,read channel:%v\n", v)}
}
管道-select
1、使用规则
(1)每个 case 表达式中都只能包含操作管道的表达式。
(2)如果有多个 case 表达式都可以运行,select 会随机选出一个执行。
(3)如果没有 case 表达式可以运行,若有 default 子句,则执行该语句。若没有 default 子句,select 将阻塞,直到某个 case 可以运行。
package mainimport "fmt"func main() {ch1 := make(chan int, 1)ch1 <- 123ch2 := make(chan int, 1)ch2 <- 456select {case v1 := <-ch1:fmt.Printf("read from ch1,data:%v\n", v1)case v2 := <-ch2:fmt.Printf("read from ch2,data:%v\n", v2)default:fmt.Println("default")}
}
2、超时控制
函数 getConfig 内部起个 goroutine 去异步获取抽奖活动配置信息,函数 getConfig 会返回一个管道给调用方。“获取活动配置信息”可能因为某些情况导致耗时很长,故加入超时控制。函数 getConfigWithTimeOut 使用 select 语句去监听“数据管道”和“超时管道”。当超时后,直接返回空数据即可。
package mainimport ("errors""fmt""time"
)type LotteryActivity struct {ActivityId int64 `json:"activity_id"` // 活动idPrizeId int64 `json:"prize_id"` // 奖品idLotterySpendCoins int64 `json:"lottery_spend_coins"` // 抽奖花费金币数LotteryWinningRate float32 `json:"lottery_winning_rate"` // 抽中的概率CreateTime *time.Time `json:"create_time,omitempty"` // 创建时间UpdateTime *time.Time `json:"update_time,omitempty"` // 修改时间
}func main() {lotteryActivity, err := getConfigWithTimeOut(1 * time.Second)if err != nil {fmt.Printf("getConfigWithTimeOut err:%s\n", err.Error())} else {fmt.Printf("getConfigWithTimeOut value:%+v\n", lotteryActivity)}
}func getConfigWithTimeOut(timeout time.Duration) (lotteryActivity *LotteryActivity, err error) {config := getConfig()select {case lotteryActivity = <-config:returncase <-time.After(timeout):return nil, errors.New("timeout")}return
}func getConfig() (config chan *LotteryActivity) {config = make(chan *LotteryActivity, 1)go func() {time.Sleep(2 * time.Second)config <- &LotteryActivity{ActivityId: 123,PrizeId: 456,}}()return
}
管道-协程池
并发-sync.Map
1、map并发问题
多协程同时 read 和 write ,会出现 fatal error: concurrent map read and map write
package main// fatal error: concurrent map read and map write
func main() {data := make(map[int]int)// readgo func() {for {_ = data[123]}}()// writego func() {for {data[456] = 456}}()// block main goroutinefor {}
}
2、sync.Map使用
(1)初始化:nil map 直接使用会 panic ,而 sync.Map 无须初始化,直接声明即可。
(2)crud:Store 方法存储、Load 方法获取、Delete 方法删除、Range 方法遍历。Range 方法回调函数会返回键值对 ,回调函数返回值为 true 时表示继续遍历,为 false 时表示停止遍历。
package mainimport ("fmt""sync"
)func main() {var data sync.Map// writedata.Store(123, 123)data.Store(456, 456)// readif v, ok := data.Load(456); ok {fmt.Printf("read value:%v\n", v)}// for-rangedata.Range(func(key, value interface{}) bool {fmt.Printf("key:%v,value:%v\n", key, value)return true})// deletedata.Delete(123)
}
并发-sync.Once
sync.Once 能够保证函数只执行一次
1、读取配置文件
(1)文件目录树
拓展知识点:Windows下如何使用tree命令生成目录树
go-concurrency-sync-once├─config│ config.go│ config.toml│└─mainmain.go
(2)编码开发
config.toml:配置 mongo 和 etcd 数据库
[mongo]
url = "mongodb://username:password@127.0.0.1:27017"
maxPoolSize = 50
[etcd]
endpoints = ["localhost:2379"]
dialTimeout = 3
config.go:toml 配置文件,可用 toml.DecodeFile 去读取
package configimport ("fmt""github.com/BurntSushi/toml""sync"
)var (conf *Configonce sync.Once
)type Config struct {Mongo *MongoConfigEtcd *EtcdConfig
}type MongoConfig struct {Url stringMaxPoolSize int64
}type EtcdConfig struct {Endpoints []stringDialTimeout int
}func GetConfig() *Config {once.Do(func() {conf = new(Config)if _, err := toml.DecodeFile("../config/config.toml", conf); err != nil {fmt.Printf("decode config file fail, err:%s", err.Error())} else {fmt.Printf("mongo config:%+v,etcd config:%+v\n", conf.Mongo, conf.Etcd)}})return conf
}
main.go:起10个协程去读取配置文件,验证 toml.DecodeFile 被执行的次数
package mainimport ("go-quick-start-guide/go-tutorial/go-concurrency/go-concurrency-sync-once/config""sync"
)func main() {var wg sync.WaitGroupwg.Add(10)for i := 0; i < 10; i++ {go func() {defer wg.Done()config.GetConfig()}()}wg.Wait()
}
(3)运行验证
toml.DecodeFile 读取配置文件的路径是 "../config/config.toml",运行代码时工作目录必须是main包,否则报错 open ../config/config.toml: The system cannot find the path specified
toml.DecodeFile("../config/config.toml", conf)
第一种方法:go run main.go,默认的工作目录是 main.go 所在的包
PS G:\goland\go-quick-start-guide\go-tutorial\go-concurrency\go-concurrency-sync-once\main> go run main.go
mongo config:&{Url:mongodb://username:password@127.0.0.1:27017 MaxPoolSize:50},etcd config:&{Endpoints:[localhost:2379] DialTimeout:3}
第二种方法:配置 working directory 为 main 包路径
三个点...作用
(1)函数变长参数
package mainimport "fmt"func main() {print(123, 456, 789)
}func print(ids ...interface{}) {for i, _ := range ids {fmt.Println(ids[i])}
}
(2)将切片打散成一个个元素
package mainfunc main() {id := make([]int, 0)userIds := []int{123, 456, 789}id = append(id, userIds...)
}
(3)使用...代替数组的长度,编译器会根据元素个数自行推断数组的长度
package mainimport "fmt"func main() {ids := [...]int{12, 34, 56, 78, 90, 13, 15, 17, 19, 24, 26, 28, 30, 32}// output: type:[14]int,len:14fmt.Printf("type:%T,len:%v", ids, len(ids))
}
go-基础-实战
快速排序
1、基本原理
(1)核心思想
每次排序都会选一个基准数,小于基准数的放在左子序列,大于等于基准数的放在右子序列。
原始序列:{13, 15, 8, 54, 23}
step1:随机选一个基准数15,则其左子序列{13, 8},右子序列{54, 23}
step2.1:序列{13, 8}随机选一个基准数8,则其左子序列{},右子序列{13}
step2.2:序列{54, 23}随机选一个基准数23,则其左子序列{},右子序列{54}
当子序列包含的元素个数小于等于1时停止循环,排序工作已经完成
(2)编码思路
如何将待排序序列重新组合成“左子序列 + 基准数 + 右子序列”?
随机从待排序序列中选一个基准数,然后 for 循环遍历待排序序列,如果当前元素的值小于基准数,就将该元素放在左子序列。那究竟是放在左子序列的哪个位置上?这肯定涉及到元素交换,所以要维护一个索引 i,如果当前元素的值小于基准数,就和索引 i 位置上的元素互换位置。
待排序序列:{13, 67, 25, 18, 45, 23, 35, 30},固定选最后一个元素即30为基准数,索引 i 初始值为0,索引 j 为 for 循环遍历当前元素所在的索引。
step1:遍历第一个元素13,比基准数30小,与索引 i 元素互换位置,然后 i++、j++
step2:遍历第二个元素67,比基准数30大,i 保持不变、j++
step3:遍历第三个元素25,比基准数30小,与索引 i 元素互换位置,然后 i++、j++
step4:遍历第四个元素18,比基准数30小,与索引 i 元素互换位置,然后 i++、j++
step5:遍历第五个元素45,比基准数30大,i 保持不变、j++
step6:遍历第六个元素23,比基准数30小,与索引 i 元素互换位置,然后 i++、j++
step7:遍历第七个元素35,比基准数30大,i 保持不变、j++
step8:遍历到基准数时,p 与 i 元素互换
最终结果:索引 i 元素值为30,左子序列小于基准数30,右子序列大于等于基准数30。
序列{13, 15, 8, 54, 23, 25, 17, 11, 78, 89, 67, 56, 54, 34, 97, 15}经过第一轮排序后就变成
{13, 8, 11, 15, 23, 25, 17, 15, 78, 89, 67, 56, 54, 34, 97, 54},其中基准数15所在的索引是3。
比基准数15(索引为3)小的都在左子序列,符合预期。
package mainimport "fmt"func main() {data := []int{13, 15, 8, 54, 23, 25, 17, 11, 78, 89, 67, 56, 54, 34, 97, 15}fmt.Printf("before sort:%v\n", data)sort(data)// after sort:[13 8 11 15 23 25 17 15 78 89 67 56 54 34 97 54]fmt.Printf("after sort:%v\n", data)
}func sort(data []int) {if len(data) <= 1 {return}i := 0for j := 0; j <= len(data)-1; j++ {if data[j] < data[len(data)-1] {temp := data[i]data[i] = data[j]data[j] = tempi++}}temp := data[len(data)-1]data[len(data)-1] = data[i]data[i] = temp// i value:3fmt.Printf("i value:%v\n", i)
}
代码实现中固定选最后一个元素为基准数,那如何实现随机选一个元素为基准数?
思路:rand.Seed 在 “0-len(data)-1”之间随机产生一个值,然后将该位置上的元素和最后一个元素交换位置。
package mainimport ("fmt""math/rand""time"
)func main() {data := []int{13, 15, 8, 54, 23, 25, 17, 11, 78, 89, 67, 56, 54, 34, 97, 15}fmt.Printf("before sort:%v\n", data)random(data)sort(data)// after sort:[13 15 8 15 23 25 17 11 34 54 67 56 54 78 97 89]fmt.Printf("after sort:%v\n", data)
}func sort(data []int) {if len(data) <= 1 {return}i := 0for j := 0; j <= len(data)-1; j++ {if data[j] < data[len(data)-1] {temp := data[i]data[i] = data[j]data[j] = tempi++}}temp := data[len(data)-1]data[len(data)-1] = data[i]data[i] = temp// i value:9fmt.Printf("i value:%v\n", i)
}func random(data []int) {rand.Seed(time.Now().UnixNano())p := rand.Intn(len(data) - 1)fmt.Printf("random value:%v\n", p)temp := data[len(data)-1]data[len(data)-1] = data[p]data[p] = temp
}
2、代码实现
package mainimport ("fmt""math/rand""time"
)func main() {data := []int{13, 15, 8, 54, 23, 25, 17, 11, 78, 89, 67, 56, 54, 34, 97, 15}fmt.Printf("before sort:%v\n", data)quickSort(data, 0, len(data)-1)fmt.Printf("after sort:%v\n", data)
}func randomizedPartition(data []int, low, high int) int {rand.Seed(time.Now().UnixNano())p := rand.Intn(high-low) + lowtemp := data[high]data[high] = data[p]data[p] = tempreturn partition(data, low, high)
}func partition(data []int, low, high int) int {pivot := data[high]i := lowfor j := low; j < high; j++ {if data[j] <= pivot {temp := data[i]data[i] = data[j]data[j] = tempi++}}data[high] = data[i]data[i] = pivotreturn i
}func quickSort(data []int, low, high int) {if high > low {p := randomizedPartition(data, low, high)// quickSort(data, low, p) incorrect, will cause stack overflowquickSort(data, low, p-1)quickSort(data, p+1, high)}
}
3、时间复杂度
平均时间复杂度:O( nlog_{2}n )
最好时间复杂度:O( nlog_{2}n )
最坏时间复杂度:O( n^{2} ),选的基准数只能将序列分为一个元素与其他元素两部分,这时的快速排序退化为冒泡排序
最坏时间复杂度情况:
(1)分区函数每次选取的基准数为序列最小元素。
(2)分区函数每次选取的基准数为序列最大元素。
具体案例:序列已经正序或逆序排好,选的基准数每次都是序列第一个元素或最后一个元素。
4、稳定性
(1)什么是排序算法的稳定性?
大小相同的两个值在排序之前和排序之后的先后顺序不变
序列{13, 67, 25, 67,18},排序之后能保证原序列第一个67一定在原序列第二个67的前面,就是稳定的排序算法。
(2)排序算法稳定性的作用?
A{V1:500, V2:300}、B{V1:400, V2:300}、C{V1:300, V2:200}
需求:先按V1降序排序,再按V2降序排序
预期排序结果:A{V1:500, V2:300}、B{V1:400, V2:300}、C{V1:300, V2:200}
使用快速排序算法进行排序(先说结论,快速排序是不稳定的)
先按V1降序排序:A{V1:500, V2:300}、B{V1:400, V2:300}、C{V1:300, V2:200}
再按V2降序排序,有两种可能结果,因为A和B的V2值相等。
第一种:A{V1:500, V2:300}、B{V1:400, V2:300}、C{V1:300, V2:200}
第二种:B{V1:400, V2:300}、A{V1:500, V2:300}、C{V1:300, V2:200}
(3)快速排序算法是不稳定的
待排序序列:{15, 13, 15},选最后一个元素即15为基准数,索引 i 初始值为0,索引 j 为 for 循环遍历当前元素所在的索引。
step1:遍历第一个元素15,大于等于基准数15,i 保持不变、j++
step2:遍历第一个元素13,比基准数15小,与索引 i 元素互换位置,然后 i++、j++
step3:遍历到基准数时,p 与 i 元素互换
最终结果:大小相同的两个元素15在排序之前和排序之后的先后顺序已经发生变化
堆排序
1、基本原理
(1)堆的存储
什么是完全二叉树?
满足两个条件:(1)前n-1层为满的(2)最后一层从左往右是连续的
案例分析:不是完全二叉树,当节点13移动到节点17的左子节点时才是完全二叉树。
堆是完全二叉树,可以用数组去存储二叉树节点。假设二叉树中某个节点在数组中的索引值为 n,那其左子节点在数组中的索引值为 2n+1,右子节点在数组中的索引值为 2n+2。
如何推导?
完全二叉树,第 l 层第一个节点索引值为 2^{l-1}-1 ,最后一个节点索引值为 2^{l}-2
左子节点在数组中的索引值 = 父节点在数组中的索引值 + 同一层父节点后面的元素个数 + 同一层父节点前面的元素个数 \times2 + 1
同一层父节点后面的元素个数 = 2^{l}-2-n
同一层父节点前面的元素个数 \times2 = 2\times[n-(2^{l-1}-1)]
故左子节点索引值 = n+(2^{l}-2-n)+2\times[n-(2^{l-1}-1)]+1=2n+1
(2)堆的性质
最大堆:每个节点的值都大于或等于它的左右子节点的值。
最小堆:每个节点的值都小于或等于它的左右子节点的值。
案例分析:节点40小于右子节点45,故二叉树不是最大堆。
(3)堆排序
思考:堆排序是如何将待排序序列有序化的?
第一步,将待排序序列构造成最大堆,取出最大值,与数组的倒数第一个元素交换位置。第二步,再次构造成最大堆,取出最大值,与数组的倒数第二个元素交换位置,以此类推完成接下来的操作。
第一个目标:如何将无序序列构造成最大堆?
案例分析:待排序序列{25, 12, 45, 18, 26, 36, 56, 6, 32}
从最后一个非叶子节点开始调整,如果子节点的值比该节点大,就交换位置。
step1:对非叶子节点18调整,右子节点32比它大,故交换位置。
step2:对非叶子节点45调整,右子节点56比它大,故交换位置。
step3:对非叶子节点12调整,左子节点32比它大,故交换位置。交换位置后就发现节点12的右子节点比它大,这不符合最大堆的性质,所以对节点12继续调整。
交换位置后,对节点12继续调整。
step4:对非叶子节点25调整,右子节点56比它大,故交换位置。交换位置后就发现节点25的右子节点比它大,这不符合最大堆的性质,所以对节点25继续调整。
交换位置后,对节点25继续调整。
总结:从最后一个非叶子节点开始调整,一直到根节点。当发生元素位置交换后,记得判断子节点局部是否满足最大堆的性质,如果不满足就递归从上到下调整到满足为止。
第二个目标:从最大堆中取出最大值后,如何再次构造成最大堆?
第一次从最大堆中取出最大值后,就和数组的倒数第一个元素交换位置,交换位置后如何再次构造成最大堆?
step1:最大值56和数组倒数第一个元素交换位置
step2:根节点即节点12从上到下递归调整到满足最大堆性质。
右子节点45比节点12大,故交换位置。
节点12的左子节点比它大,故交换位置。
总结:从最大堆取出最大值并与数组中倒数第n个元素交换位置后,将根节点从上到下调整到满足最大堆性质为止。
2、代码实现
package mainimport "fmt"func main() {data := []int{13, 15, 8, 54, 23, 25, 17, 11, 78, 89, 67, 56, 54, 34, 97, 15}fmt.Printf("before sort:%v\n", data)heapSort(data)// after sort:[8 11 13 15 15 17 23 25 34 54 54 56 67 78 89 97]fmt.Printf("after sort:%v\n", data)
}func heapSort(data []int) {for i := len(data)/2 - 1; i >= 0; i-- {constructHeap(data, i, len(data))}// 97 89 56 78 67 54 34 15 54 23 15 25 13 8 17 11fmt.Printf("constructHeap:%v\n", data)for i := 0; i < len(data)-1; i++ {swapAndAdjustHeap(data, len(data)-i)}
}func constructHeap(data []int, parent int, size int) {left := 2*parent + 1right := 2*parent + 2if left >= size {return}temp := data[left]index := leftif right < size && data[left] < data[right] {temp = data[right]index = right}if data[parent] < temp {data[index] = data[parent]data[parent] = tempconstructHeap(data, index, size)}
}func swapAndAdjustHeap(data []int, size int) {temp := data[size-1]data[size-1] = data[0]data[0] = tempconstructHeap(data, 0, size-1)
}
go-进阶-教程
context-WithTimeout
1、基础知识
(1)创建超时上下文
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Second)
(2)超时则取消执行
defer cancel()
验证 defer cancel() 是否能超时取消执行
创建订单函数 createOrder 超时控制方式调用 rpcCreateOrder ,函数 rpcCreateOrder 是个死循环,每隔一秒钟就打印日志。运行代码,打开浏览器访问 http://localhost:8080/createOrder ,查看 goland 控制台日志输出。创建订单函数 createOrder 超时后,运行 rpcCreateOrder 函数的 goroutine 还在继续打印输出日志,并没有取消执行。
分析原因:调用 cancel() 函数,仅仅是把管道关闭了,即 ctx.Done() 能够读取到数据。如果想要超时取消 goroutine 的执行,应该在 goroutine 中校验 context 是否取消了,如果是就 return。
package mainimport ("context""fmt""net/http""time"
)type Order struct {OrderId int64 // 订单idUserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量CreateTime time.Time // 创建时间UpdateTime time.Time // 修改时间
}func main() {http.HandleFunc("/createOrder", createOrder)if err := http.ListenAndServe(":8080", nil); err != nil {panic(err)}
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {var errCh = make(chan error)ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)defer cancel()order := new(Order)go func() {err := rpcCreateOrder(ctx, order)select {case errCh <- err:default:return}}()select {case <-ctx.Done():fmt.Printf("createOrder ctx err: %s\n", ctx.Err())w.Write([]byte("createOrder ctx err"))case err := <-errCh:fmt.Printf("createOrdererr: %s\n", err.Error())if err != nil {w.Write([]byte("createOrder err"))} else {w.Write([]byte("createOrder value"))}}return
}// infinite loop
func rpcCreateOrder(ctx context.Context, order *Order) (err error) {for i := 0; ; i++ {time.Sleep(1 * time.Second)fmt.Printf("count:%v,rpcCreateOrder\n", i)}return
}
2、超时控制
(1)第一种风格:在被调用方实现超时控制
获取用户订单函数 getOrder 调用 rpcGetOrder 函数,rpcGetOrder 函数侧做超时控制
(2)第二种风格:在调用方实现超时控制
创建订单函数 createOrder 调用 rpcCreateOrder 函数,createOrder 函数侧做超时控制
package mainimport ("context""fmt""time"
)type Order struct {OrderId int64 // 订单idUserId int64 // 用户idProductId int64 // 商品idCount int64 // 购买的商品数量CreateTime time.Time // 创建时间UpdateTime time.Time // 修改时间
}func main() {// 获取用户订单信息getOrder(15)// 创建订单createOrder(&Order{})
}// 获取用户订单信息
func getOrder(orderId int64) {ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()order, err := rpcGetOrder(ctx, orderId)if err != nil {fmt.Printf("rpcGetOrders fail,err:%s\n", err.Error())return}fmt.Printf("rpcGetOrders res:%+v\n", order)return
}// 创建订单
func createOrder(order *Order) {var errCh = make(chan error)ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)defer cancel()go func() {err := rpcCreateOrder(ctx, order)select {case errCh <- err:default:return}}()select {case <-ctx.Done():fmt.Printf("createOrder ctx err: %s\n", ctx.Err())returncase err := <-errCh:fmt.Printf("createOrder res:%v\n", err)return}return
}// rpc 获取用户订单信息
func rpcGetOrder(ctx context.Context, orderId int64) (order *Order, err error) {ch := make(chan *Order)go func() {order = &Order{OrderId: orderId,UserId: 12,ProductId: 34,Count: 2,}select {case ch <- order:default:return}}()select {case <-ctx.Done():return nil, ctx.Err()case result := <-ch:return result, nil}return
}// rpc 创建订单
func rpcCreateOrder(ctx context.Context, order *Order) (err error) {// 订单id幂等校验// 商品库存校验time.Sleep(2 * time.Second)return
}
定时任务-cron
1、cron表达式
Field name | Mandatory? | Allowed values | Allowed special characters
---------- | ---------- | -------------- | --------------------------
Seconds | Yes | 0-59 | * / , -
Minutes | Yes | 0-59 | * / , -
Hours | Yes | 0-23 | * / , -
Day of month | Yes | 1-31 | * / , - ?
Month | Yes | 1-12 or JAN-DEC | * / , -
Day of week | Yes | 0-6 or SUN-SAT | * / , - ?
(1)* :表示匹配该域的任意值。假如在 Minutes 域使用, 即表示每分钟都会触发一次
(2)/:表示起始时间开始触发,然后每隔固定时间触发一次。假如在 Minutes 域使用5/20,则表示5分钟触发一次,而25(5+20),45(5+20+20)分钟分别触发一次
(3),:表示列出枚举值。假如在 Minutes 域使用5,20,则表示在5和20分钟分别触发一次
(4)-:表示范围。假如在 Minutes 域使用5-20,表示从5分到20分钟每分钟触发一次
2、实例教程
每隔1分钟执行一次,正确的cron表达式是"0 */1 * * * ?"
如果写成"* */1 * * * ?",能实现每隔1分钟执行一次吗?
1分钟0秒匹配这个表达式,1分钟1秒也能匹配这个表达式,所以实现不了每隔1分钟执行一次
package mainimport ("github.com/robfig/cron""log""net/http"
)func main() {runJob()mux := http.NewServeMux()if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func runJob() {c := cron.New()// 每隔5秒执行一次c.AddFunc("*/5 * * * * ?", func() {log.Println("execute every 5 seconds")})// 每隔1分钟执行一次c.AddFunc("0 */1 * * * ?", func() {log.Println("execute every 1 minute")})// 每天1点执行一次c.AddFunc("0 0 1 * * ?", func() {log.Println("execute once a day at 1:00 a.m.")})c.Start()
}
http-crud
1、注册处理器
第一种方法(推荐)
func HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
函数格式为 func(http.ResponseWriter, *http.Request)
package mainimport "net/http"func main() {http.HandleFunc("/createOrder", createOrder)
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {
}
第二种方法
func Handle(pattern string, handler Handler)type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
自定义结构体,实现 Handler 接口
package mainimport ("net/http"
)type GetOrderHandle struct {
}func main() {http.Handle("/getOrder", &GetOrderHandle{})
}func (getOrderHandle *GetOrderHandle) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
2、启动服务
handler参数为 nil 时,会使用 DefaultServeMux 。
func ListenAndServe(addr string, handler Handler) error
(1)第一种方法:使用 DefaultServeMux (尽量不要用)
package mainimport "net/http"func main() {http.HandleFunc("/createOrder", createOrder)if err := http.ListenAndServe(":8080", nil); err != nil {panic(err)}
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {
}
尽量不要用 DefaultServeMux 的理由
DefaultServeMux 存储在全局变量中,而且首字母是大写,所以任何包都可以访问它并注册路由,包括您的应用程序导入的任何第三方包。 如果其中一个第三方软件包遭到破坏,就可以使用 DefaultServeMux 将恶意处理程序暴露给 Web。
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMuxvar defaultServeMux ServeMux
(2)第二种方法:使用 http.NewServeMux() (推荐)
package mainimport "net/http"func main() {mux := http.NewServeMux()mux.HandleFunc("/createOrder", createOrder)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {
}
推荐使用 http.NewServeMux() 的理由
mux 是 main 包下 main 函数的一个局部变量
3、获取请求参数
(1)获取 post 请求参数
POST http://localhost:8080/createOrder
{"order_id":123,"user_id":456
}
GetPostBody:校验 r.Method 、ioutil.ReadAll 读取字节流、json-iterator 反序列化
package mainimport ("errors""fmt"jsoniter "github.com/json-iterator/go""io/ioutil""net/http""strings"
)type Order struct {OrderId int64 `json:"order_id"` // 订单idUserId int64 `json:"user_id"` // 用户idProductId int64 `json:"product_id"` // 商品idCount int64 `json:"count"` // 购买的商品数量
}func main() {mux := http.NewServeMux()mux.HandleFunc("/createOrder", createOrder)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {order := new(Order)err := GetPostBody(r, order)if err != nil {fmt.Printf("getPostBody err:%s\n", err.Error())w.Write([]byte(err.Error()))return}fmt.Printf("getPostBody value:%v\n", order)res, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(order)w.Write(res)return
}func GetPostBody(r *http.Request, params interface{}) (err error) {if strings.ToLower(r.Method) != "post" {return errors.New("http method err")}bodyByte, err := ioutil.ReadAll(r.Body)if err != nil {return}return jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(bodyByte, params)
}
(2)获取 get 请求参数
http://localhost:8080/getPageList?page=1&size=10
第一种方法
GetUrlParams 函数通过 r.URL.Query() 把参数保存在 map[string]string。
第二种方法(推荐)
调用 r.ParseForm() 后通过 r.Form.Get 获取对应的数据。当缺少某个必填参数时,返回报错。
package mainimport ("fmt"jsoniter "github.com/json-iterator/go""net/http""strconv""strings"
)type PageReq struct {Page int `json:"page"` // 第几页Size int `json:"size"` // 一页多少条
}func main() {mux := http.NewServeMux()mux.HandleFunc("/getPageList", getPageList)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func getPageList(w http.ResponseWriter, r *http.Request) {if strings.ToLower(r.Method) != "get" {w.Write([]byte("http method err"))return}// 第一种方法result := GetUrlParams(r)fmt.Printf("getUrlParams:%v\n", result)// 第二种方法params := new(PageReq)r.ParseForm()pageString := r.Form.Get("page")sizeString := r.Form.Get("size")if page, err := strconv.Atoi(pageString); err == nil {params.Page = page} else {w.Write([]byte("missing required parameter page"))return}if size, err := strconv.Atoi(sizeString); err == nil {params.Size = size} else {w.Write([]byte("missing required parameter size"))return}fmt.Printf("params:%+v\n", params)res, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(result)w.Write(res)return
}func GetUrlParams(r *http.Request) (result map[string]string) {result = make(map[string]string)keys := r.URL.Query()for k, v := range keys {result[k] = v[0]}return
}
4、请求响应
状态码、提示信息、数据
type ResponseBody struct {Code int `json:"code"`Message interface{} `json:"message,omitempty"`Body interface{} `json:"body,omitempty"`
}
WriteResponseBody 函数通过 w.Write 写入 ResponseBody 序列化后的字节数组
package mainimport (jsoniter "github.com/json-iterator/go""net/http"
)const (OK = 0
)type ResponseBody struct {Code int `json:"code"`Message interface{} `json:"message,omitempty"`Body interface{} `json:"body,omitempty"`
}type Order struct {OrderId int64 `json:"order_id"` // 订单idUserId int64 `json:"user_id"` // 用户idProductId int64 `json:"product_id"` // 商品idCount int64 `json:"count"` // 购买的商品数量
}func main() {mux := http.NewServeMux()mux.HandleFunc("/getPageList", getPageList)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func getPageList(w http.ResponseWriter, r *http.Request) {WriteSuccessResponseBody(w, &Order{OrderId: 123,UserId: 456,ProductId: 789,Count: 2,})
}func WriteResponseBody(w http.ResponseWriter, code int, message, body interface{}) {responseBody := &ResponseBody{Code: code,Message: message,Body: body,}resByte, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(responseBody)w.Write(resByte)
}func WriteSuccessResponseBody(w http.ResponseWriter, body interface{}) {WriteResponseBody(w, OK, "OK", body)
}
5、http middware
GetHttpHandler 函数,统一在包装函数内实现公共功能,比如校验用户 ip、校验用户 accessToken、记录业务 HttpHandler 函数耗时等,业务 HttpHandler 函数作为参数形式传给包装函数。
package mainimport ("fmt""net/http"
)type WrapHttpFunc struct {HttpFunc func(http.ResponseWriter, *http.Request)CheckAccessToken bool
}func main() {mux := http.NewServeMux()mux.HandleFunc("/recharge", GetHttpHandler(&WrapHttpFunc{HttpFunc: RechargeHandler,CheckAccessToken: true,}))if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func RechargeHandler(w http.ResponseWriter, r *http.Request) {
}func CheckAccessToken(w http.ResponseWriter, r *http.Request) (ok bool, err error) {return
}func GetHttpHandler(wrapHttpFunc *WrapHttpFunc) func(w http.ResponseWriter, r *http.Request) {return func(w http.ResponseWriter, r *http.Request) {if wrapHttpFunc.CheckAccessToken {ok, err := CheckAccessToken(w, r)if !ok {fmt.Printf("CheckAccessToken err:%s", err.Error())w.Write([]byte(err.Error()))}}wrapHttpFunc.HttpFunc(w, r)}
}
性能优化-pprof
1、基础知识
运行代码,浏览器访问 http://localhost:8080/debug/pprof/
package mainimport ("net/http""net/http/pprof"
)func main() {mux := http.NewServeMux()// register pprof handlersmux.HandleFunc("/debug/pprof/", pprof.Index)mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)mux.HandleFunc("/debug/pprof/profile", pprof.Profile)mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)mux.HandleFunc("/debug/pprof/trace", pprof.Trace)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}
2、cpu分析
生成 cpu profile 文件,启动 pprof web ui
D:\goland\code\go-quick-start-guide>go tool pprof -http=:8081 http://localhost:8080/debug/pprof/profile?seconds=30
Fetching profile over HTTP from http://localhost:8080/debug/pprof/profile?seconds=30
Saved profile in C:\Users\Administrator\pprof\pprof.samples.cpu.002.pb.gz
Serving web UI on http://localhost:8081
常见问题
PS D:\goland\code\go-quick-start-guide> go tool pprof -http=:8081 http://localhost:8080/debug/pprof/profile?seconds=30
Fetching profile over HTTP from http://localhost:8080/debug/pprof/profile?seconds=30
Saved profile in C:\Users\Administrator\pprof\pprof.samples.cpu.001.pb.gz
Serving web UI on http://localhost:8081
Failed to execute dot. Is Graphviz installed?
exec: "dot": executable file not found in %PATH%
解决问题:下载安装 graphviz ,手动配置环境变量或者在安装过程中勾选“add Graphviz to the system PATH for all users”
验证 graphviz 是否安装成功,goland idea terminal 输入“dot -version”
PS D:\goland\code\go-quick-start-guide> dot -version
dot : 无法将“dot”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。
所在位置 行:1 字符: 1
+ dot -version
+ ~~~+ CategoryInfo : ObjectNotFound: (dot:String) [], CommandNotFoundException+ FullyQualifiedErrorId : CommandNotFoundException
解决问题:goland idea terminal 默认用的是 powershell ,更换 powershell 为 cmd
更换步骤:File -> Settings -> Enter 'Terminal' in search -> Shell path
3、自动profile
背景:有人值守的情况下,如果 cpu 飙升,那可以通过指定路径手动下载 cpu profile 文件,然后用工具对 cpu profile 文件进行分析。那无人值守的情况下如何应对?半夜出现 cpu 飙升,如何保存现场,即如何产生 cpu profile 文件?
思路1:持续性采样,即指定间隔时间内会产生一个 cpu profile 文件。优化思路就是自动 cpu profile 开关接入配置中心。有人值守的情况下,关闭自动 cpu profile 功能,无人值守的情况下,打开自动 cpu profile 功能。
package mainimport ("github.com/robfig/cron""log""net/http""net/http/pprof""os"runtimePprof "runtime/pprof""time"
)func main() {runJob()mux := http.NewServeMux()// register pprof handlersmux.HandleFunc("/debug/pprof/", pprof.Index)mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)mux.HandleFunc("/debug/pprof/profile", pprof.Profile)mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)mux.HandleFunc("/debug/pprof/trace", pprof.Trace)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func runJob() {c := cron.New()// 每隔5分钟执行一次c.AddFunc("0 */5 * * * ?", func() {ContinueProfile()})// 每隔2秒执行一次c.AddFunc("*/2 * * * * ?", func() {bigMap()})c.Start()
}func ContinueProfile() {path := time.Now().Format("20060102150405") + ".pprof"log.Printf("profile path:%s\n", path)fp, err := os.Create(path)defer fp.Close()if err != nil {log.Printf("os.Create err:%s", err.Error())return}runtimePprof.StartCPUProfile(fp)<-time.After(30 * time.Second)defer runtimePprof.StopCPUProfile()
}func bigMap() {data := make(map[int]int)for i := 0; i < 100000; i++ {data[i] = i}
}
对指定间隔时间自动生成的 cpu profile 文件进行分析
D:\goland\code\go-quick-start-guide\go-tutorial\go-http\go-http-pprof\continue_profile>go tool pprof 20220501224200.pprof
Type: cpu
Time: May 1, 2022 at 10:42pm (CST)
Duration: 221.32ms, Total samples = 0
No samples were found with the default sample value type.
Try "sample_index" command to analyze different sample values.
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 0, 0% of 0 totalflat flat% sum% cum cum%
(pprof)
出现问题:Duration: 221.32ms, Total samples = 0
分析问题:手动下载 cpu profile 时,设置seconds=30,即 Duration: 30s 左右。现在指定时间间隔自动下载 cpu profile 的 Duration 值不符合预期。
解决问题:增加代码 <-time.After(30 * time.Second)
runtimePprof.StartCPUProfile(fp)
<-time.After(30 * time.Second)
defer runtimePprof.StopCPUProfile()
最终结果:go tool pprof xxx.pprof ,可增加“-http=:8081”选项,自动打开 web ui。
D:\goland\code\go-quick-start-guide\go-tutorial\go-http\go-http-pprof\continue_profile>go tool pprof 20220502105800.pprof
Type: cpu
Time: May 2, 2022 at 10:58am (CST)
Duration: 30.10s, Total samples = 110ms ( 0.37%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 110ms, 100% of 110ms total
Showing top 10 nodes out of 34flat flat% sum% cum cum%30ms 27.27% 27.27% 30ms 27.27% runtime.stdcall110ms 9.09% 36.36% 20ms 18.18% github.com/robfig/cron.(*Cron).run10ms 9.09% 45.45% 10ms 9.09% runtime.evacuate_fast6410ms 9.09% 54.55% 10ms 9.09% runtime.netpoll10ms 9.09% 63.64% 10ms 9.09% runtime.releasep10ms 9.09% 72.73% 10ms 9.09% runtime.runqempty10ms 9.09% 81.82% 10ms 9.09% runtime.runtimer10ms 9.09% 90.91% 10ms 9.09% runtime.selectgo10ms 9.09% 100% 10ms 9.09% runtime/pprof.(*profMap).lookup0 0% 100% 10ms 9.09% github.com/robfig/cron.(*Cron).runWithRecovery
(pprof)
思路2:条件性采样,即超过阈值后触发采样动作,可使用第三方开源的自动采样库 Holmes
package mainimport ("mosn.io/holmes""net/http""net/http/pprof""time"
)func main() {ConditionalProfile()mux := http.NewServeMux()// register pprof handlersmux.HandleFunc("/debug/pprof/", pprof.Index)mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)mux.HandleFunc("/debug/pprof/profile", pprof.Profile)mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)mux.HandleFunc("/debug/pprof/trace", pprof.Trace)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}func ConditionalProfile() {h, _ := holmes.New(holmes.WithCollectInterval("5s"),holmes.WithDumpPath("/tmp"),holmes.WithCPUDump(20, 25, 80, time.Minute),holmes.WithCPUMax(90),)h.EnableCPUDump()// start the metrics collect and dump looph.Start()
}
日志-seelog
1、配置文件-seelog.xml
format="%Date(2006-01-02 15:04:05.999) [%LEV] %File:%Line:%FuncShort %Msg%n" 配置日志输出格式。
2022-04-29 21:43:08.774 [INF] main.go:54:createOrder getPostBody value:&{OrderId:123 UserId:456 ProductId:0 Count:0}
datepattern="2006010215",表示一个日志文件保存一个小时的日志。maxrolls="168",表示最多可以有168个日志文件存在。综合两个配置,即保留七天的日志文件。
<seelog type="asynctimer" minlevel="info" maxlevel="error" asyncinterval="1000"><outputs formatid="main"><console /><rollingfile type="date" filename="../../logs/main.log" datepattern="2006010215" maxrolls="168" fullname="true"/></outputs><formats><format id="main" format="%Date(2006-01-02 15:04:05.999) [%LEV] %File:%Line:%FuncShort %Msg%n"/></formats>
</seelog>
2、读取配置
package mainimport (log "github.com/cihub/seelog"
)func main() {defer log.Flush()logger, err := log.LoggerFromConfigAsFile("../config/seelog.xml")if err != nil {panic(err.Error())}log.ReplaceLogger(logger)
}
3、实例教程
package mainimport ("errors"log "github.com/cihub/seelog"jsoniter "github.com/json-iterator/go""io/ioutil""net/http""strings"
)const (OK = 0ERR0R = 1
)type ResponseBody struct {Code int `json:"code"`Message interface{} `json:"message,omitempty"`Body interface{} `json:"body,omitempty"`
}type Order struct {OrderId int64 `json:"order_id"` // 订单idUserId int64 `json:"user_id"` // 用户idProductId int64 `json:"product_id"` // 商品idCount int64 `json:"count"` // 购买的商品数量
}func main() {defer log.Flush()logger, err := log.LoggerFromConfigAsFile("../config/seelog.xml")if err != nil {panic(err.Error())}log.ReplaceLogger(logger)mux := http.NewServeMux()mux.HandleFunc("/createOrder", createOrder)if err := http.ListenAndServe(":8080", mux); err != nil {panic(err)}
}// 创建订单
func createOrder(w http.ResponseWriter, r *http.Request) {order := new(Order)err := GetPostBody(r, order)if err != nil {log.Errorf("getPostBody err:%s\n", err.Error())WriteResponseBody(w, ERR0R, err.Error(), err.Error())return}log.Infof("getPostBody value:%+v\n", order)WriteSuccessResponseBody(w, order)
}func GetPostBody(r *http.Request, params interface{}) (err error) {if strings.ToLower(r.Method) != "post" {return errors.New("http method err")}bodyByte, err := ioutil.ReadAll(r.Body)if err != nil {return}return jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal(bodyByte, params)
}func WriteResponseBody(w http.ResponseWriter, code int, message, body interface{}) {responseBody := &ResponseBody{Code: code,Message: message,Body: body,}resByte, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(responseBody)w.Write(resByte)
}func WriteSuccessResponseBody(w http.ResponseWriter, body interface{}) {WriteResponseBody(w, OK, "OK", body)
}
配置文件-toml
1、基础知识
(1)表是键值对的集合,格式:[table]
[database] 配置数据库,ports = [ 8000, 8001, 8002 ] 配置多个端口,即数组格式。
[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true
[database] 对应的结构体,当结构体中的成员名称和 [database] 中属性名称不一致时,比如 ConnMax 和 connection_max,可用 toml 标签实现映射。
type TomlConfig struct {Database *DatabaseConfig
}type DatabaseConfig struct {Server stringPorts []intConnMax int `toml:"connection_max"`Enabled bool
}
(2)子表:[table.childtable] 格式通过点分隔键来定义的表中的子表
[servers][servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"
[servers] 对应的结构体,使用 map 集合保存。
type TomlConfig struct {Servers map[string]*ServerConfig
}type ServerConfig struct {IP stringDC string
}
2、实例教程
config.toml
# https://github.com/toml-lang/tomltitle = "TOML Example"[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates[database]
server = "192.168.1.1"
ports = [ 8000, 8001, 8002 ]
connection_max = 5000
enabled = true[servers]# Indentation (tabs and/or spaces) is allowed but not required
[servers.alpha]
ip = "10.0.0.1"
dc = "eqdc10"[servers.beta]
ip = "10.0.0.2"
dc = "eqdc10"[clients]
data = [ ["gamma", "delta"], [1, 2] ]# Line breaks are OK when inside arrays
hosts = ["alpha","omega"
]
config.go
package configimport ("fmt""github.com/BurntSushi/toml""sync""time"
)var (conf *TomlConfigonce sync.Once
)type TomlConfig struct {Title stringOwner *OwnerConfigDatabase *DatabaseConfigServers map[string]*ServerConfigClients *ClientsConfig
}type OwnerConfig struct {Name stringDOB time.Time
}type DatabaseConfig struct {Server stringPorts []intConnMax int `toml:"connection_max"`Enabled bool
}type ServerConfig struct {IP stringDC string
}type ClientsConfig struct {Data [][]interface{}Hosts []string
}func GetConfig() *TomlConfig {once.Do(func() {conf = new(TomlConfig)if _, err := toml.DecodeFile("../config/config.toml", conf); err != nil {fmt.Printf("decode config file fail, err:%s\n", err.Error())}})return conf
}
main.go
package mainimport ("fmt""go-quick-start-guide/go-tutorial/go-toml/config"
)func main() {conf := config.GetConfig()fmt.Printf("config:%+v", conf)
}