快速入门go语言

环境搭建

编译器安装

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/768428.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯G431RBT6——定时器中使用led冲突以及led与lcd冲突等一系列问题

本文是解决 同时在 定时器中点灯 与 LCD屏幕显示 冲突异常的问题 我们大家都知道&#xff0c;G431RBT6开发板上led与lcd是冲突的&#xff0c;所以在lcd.c文件中的这三个函数中 void LCD_WriteReg(u8 LCD_Reg, u16 LCD_RegValue) void LCD_WriteRAM_Prepare(void) void LCD_Wr…

vue+Echarts实现多设备状态甘特图

目录 1.效果图 2.代码 3.注意事项 Apache ECharts ECharts官网&#xff0c;可在“快速上手”处查看详细安装方法 1.效果图 可鼠标滚轮图表和拉动下方蓝色的条条调节时间细节哦 &#xff08;注&#xff1a;最后一个设备没有数据&#xff0c;所以不显示任何矩形&#xff09;…

【】(综合练习)博客系统

在之前的学些中&#xff0c;我们掌握了Spring框架和MyBatis的基本使用&#xff0c;接下来 我们就要结合之前我们所学的知识&#xff0c;做出一个项目出来 1.前期准备 当我们接触到一个项目时&#xff0c;我们需要对其作出准备&#xff0c;那么正规的准备是怎么样的呢 1.了解需求…

深入探索C语言动态内存分配:释放你的程序潜力

&#x1f308;大家好&#xff01;我是Kevin&#xff0c;蠢蠢大一幼崽&#xff0c;很高兴你们可以来阅读我的博客&#xff01; &#x1f31f;我热衷于分享&#x1f58a;学习经验&#xff0c;&#x1f3eb;多彩生活&#xff0c;精彩足球赛事⚽ &#x1f31f;感谢大家的支持&#…

[激光原理与应用-80]:PLC通信协议之-OMRON欧姆龙FINS TCP通信协议详细解析

目录 一、OMRON欧姆龙 PLC简介 1.1 OMRON欧姆龙 PLC 1.2 OMRON欧姆龙 PLC通信协议简介 1.3 通信架构 二、欧姆龙FINS协议简介 2.1 简介 2.2 协议分层 2.3 OMRON&#xff08;欧姆龙&#xff09;FINS协议的本质 2.4 OMRON&#xff08;欧姆龙&#xff09;FINS&#xff08…

边缘计算【智能+安全检测】系列教程-- Jeton Agx Orin 基础环境搭建

1 .前期准备 Jetson Agx Orin 比Jetson Agx Orin Xavier的算力要高&#xff0c;性能要好通常用来做自动驾驶的AI推理&#xff0c;具体外观如下图 1.刷机软件sdkmanager&#xff1a;下载链接 NVIDIA账号需要注册&#xff0c;正常一步一步往下走就行。在ubuntu18以上的系统安…

Verilog刷题笔记45

题目&#xff1a;Given the finite state machine circuit as shown, assume that the D flip-flops are initially reset to zero before the machine begins. Build this circuit. 解题&#xff1a; module top_module (input clk,input x,output z ); wire [2:0]size;dtou…

第一篇:概述、 目录、适用范围及术语 --- IAB/MRC《增强现实(AR)广告(效果)测量指南1.0 》

第一篇&#xff1a;概述、目录、适用范围及术语 - IAB与MRC及《增强现实广告效果测量指南1.0》 --- 我为什么要翻译美国IAB科技公司系列标准 ​​​​​​​​​​​​​​ 翻译计划 第一篇概述—IAB与MRC及《增强现实广告效果测量指南》之目录、适用范围及术语第二篇广告效…

MySQL B树 和B+数据的区别

MySQL B树 和B数据的区别 树分类&#xff1a;二叉树完全二叉树满二叉树堆红黑树 B树B树 首先我们搞清楚数据结构中的 树这个概念 树 分类&#xff1a; 二叉树 限定每个节点的子节点最多只有2个子节点&#xff0c;且有左右之分。完全二叉树 满二叉树 堆 红黑树 B树 B树 …

上位机图像处理和嵌入式模块部署(qmacvisual之ROI设定)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 ROI&#xff0c;全称是region of interest&#xff0c;也就是感兴趣区域。这里面一般分成两种情况&#xff0c;一种是所有的算法都依赖于这个ROI&a…

Jenkins中使用Generic Webhook Trigger插件实现持续集成

项目环境 宝塔Linux面板DockerJenkinsgitee 目的 实现每次push推送dev分支到gitee上&#xff0c;Jenkins自动构建项目&#xff1b;push其它分支时&#xff0c;不运行。 实现方法 1.在Jenkins上安装Generic Webhook Trigger插件 在“系统设置–插件管理–可选插件”界面搜…

第390场 LeetCode 周赛题解

A 每个字符最多出现两次的最长子字符串 滑动窗口&#xff1a;枚举窗口的左边界&#xff0c;尽可能右移窗口的右边界。 (当然也可以暴力枚举) class Solution { public:int maximumLengthSubstring(string s) {vector<int> cnt(26);int res 0;for (int l 0, r -1, n s…

构建以太网交换网络——(生成树实验)

实验介绍 关于本实验 以太网交换网络中为了进行链路备份&#xff0c;提高网络可靠性&#xff0c;通常会使用冗余链路。但是使用冗余链路会在交换网络上产生环路&#xff0c;引发广播风暴以及MAC地址表不稳定等故障现象&#xff0c;从而导致用户通信质量较差&#xff0c;甚至…

C语言 数组指针 指针数组

指针数组 什么是指针数组&#xff0c;他是一个数组&#xff0c;数组的元素是指针。但是指针也有多种数据类型&#xff0c;有数组指针、函数指针、整形指针、字符串指针。 现在我就使用函数指针来写代码&#xff0c;也就是函数指针数组的应用代码&#xff1a; #include <s…

Vscode按键占用问题解决

Vscode按键占用 在使用vscode的过程中&#xff0c;官方按键 Ctrl . 按键可以提示修复代码中的问题&#xff0c;但是发现按了没有反应。 解决问题 首先确认vscode中是否设置了这个按键&#xff0c;默认设置了的系统输入法中是否有按键冲突了&#xff0c;打开输入法设置检查 …

[iOS]GCD(一)

[iOS]GCD(一) 文章目录 [iOS]GCD(一)GCD的概要GCD的APIDispatch Queuedispatch_queue_createMain Dispatch_set_target_queuedispatch_afterDispatch Groupdispatch_barrier_asyncdispatch_applydispatch_applydispatch_suspend/dispatch_resumeDispatch Semaphoredispatch_onc…

MQ高级篇---消息可靠性

MQ的一些常见问题 后面内容基于springboot 2.3.9.RELEASE 消息可靠性 生产者确认机制 在publisher微服务中application.yml中添加 spring:rabbitmq:publisher-confirm-type: correlatedpublisher-returns: truetemplate:mandatory: true每个RabbitTemplate只能配置一个Return…

【计算机网络】启程

&#x1f4dd;本文介绍 本文为计算机网路系列的开始篇&#xff0c;会介绍一下使用的书籍和自己做的思维导图。 &#x1f44b;作者简介&#xff1a;一个正在积极探索的本科生 &#x1f4f1;联系方式&#xff1a;943641266(QQ) &#x1f6aa;Github地址&#xff1a;https://githu…

Rust下载安装、卸载、版本切换、创建项目(包含指定版本的)

先声名一下&#xff0c;下面所说的版本号为xxxxx-x86_64-unknown-linux-gnu中xxxxx的部分。 下载安装 下载最新版本的Rust&#xff1a; curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh info: downloading installer重启shell 或者 按照提示 执行命令让环境变…

codeTop:二叉树最大深度

属于二叉树遍历的变形&#xff0c;这里使用DFS的递归方式 最大深度就是 max(左子树最大深度 ,右子树最大深度) 1&#xff0c; 而计算左/右子树最大深度可以用相同的算法。 public int maxDepth(TreeNode root) { if(root null){return 0; } int lh maxDepth(root.left); in…