快速入门go语言

news/2024/4/28 13:53:13/文章来源:https://blog.csdn.net/xhw79/article/details/137000114

环境搭建

编译器安装

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.luyixian.cn/news_show_1027038.aspx

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

相关文章

蚂蚁庄园今日答案

蚂蚁庄园是一款爱心公益游戏&#xff0c;用户可以通过喂养小鸡&#xff0c;产生鸡蛋&#xff0c;并通过捐赠鸡蛋参与公益项目。用户每日完成答题就可以领取鸡饲料&#xff0c;使用鸡饲料喂鸡之后&#xff0c;会可以获得鸡蛋&#xff0c;可以通过鸡蛋来进行爱心捐赠。其中&#…

UG NX二次开发(C#)-通过曲线组生成NURBS曲面

文章目录 1、前言2、UG NX中通过曲线组生成NURBS曲面的操作3、采用NXOpen C#方法的源代码1、前言 在UG NX中,曲线、曲面的操作使用比较多,对于创建NURBS曲面,可以通过曲线组来生成,本文以NXOpen C#的方法实现通过曲线组生成NURBS曲面的功能。对于UG NX二次开发感兴趣或者有…

windows上打开redis服务闪退问题处理

方法1&#xff1a;在windows上面打开redis服务时&#xff0c;弹窗闪退可能是6379端口占用&#xff0c;可以用以下命令查看&#xff1a; netstat -aon | findstr 6379 如果端口被占用可以用这个命令解决&#xff1a; taskkill /f /pid 进程号 方法2&#xff1a; 可以使用…

力扣热门算法题 112. 路径总和,115. 不同的子序列,120. 三角形最小路径和

112. 路径总和&#xff0c;115. 不同的子序列&#xff0c;120. 三角形最小路径和&#xff0c;每题做详细思路梳理&#xff0c;配套Python&Java双语代码&#xff0c; 2024.03.25 可通过leetcode所有测试用例。 目录 112. 路径总和 解题思路 完整代码 Java Python 115…

机器学习——元学习

元学习&#xff08;Meta Learning&#xff09;是一种机器学习方法&#xff0c;旨在使模型能够学习如何学习。它涉及到在学习过程中自动化地学习和优化学习算法或模型的能力。元学习的目标是使模型能够从有限的训练样本中快速适应新任务或新环境。 在传统的机器学习中&#xff…

数据结构算法系列----贪心算法

目录 一、什么是贪心 1、定义&#xff1a; 2、举例&#xff1a; 二、例题 完整代码&#xff1a; 一、什么是贪心 1、定义&#xff1a; 贪心算法是一种在每一步选择中都采取当前状态下最优决策的算法。在贪心算法中&#xff0c;通过 局部最优 解来达到全局最优解。贪心算法…

Mysql数据库:事务管理

目录 一、Mysql事务的概述 1、Mysql事务的概念 2、事务的ACID四大特性 3、事务之间的相互影响 4、事务的四种隔离级别 5、MySQL与Oracle自动提交事务的区别 6、事务隔离级别的作用范围 二、Mysql事务相关操作 1、查询和设置事务隔离级别 1.1 全局级事务隔离级别 1.1…

量子计算+运营优化!IonQ 和 德国DESY 合作提升机场登机口调度效率

内容来源&#xff1a;量子前哨&#xff08;ID&#xff1a;Qforepost&#xff09; 编辑丨慕一 编译/排版丨 沛贤 深度好文&#xff1a;1200字丨8分钟阅读 3月14日&#xff0c;量子计算公司IonQ宣布了与德国电子同步加速器&#xff08;DESY&#xff0c;德国的大型粒子物理学研…

【正点原子FreeRTOS学习笔记】————(2)FreeRTOS的任务创建和删除

这里写目录标题 一、任务创建和删除的API函数&#xff08;熟悉&#xff09;二、任务创建和删除&#xff08;动态方法&#xff09;&#xff08;掌握&#xff09;三、任务创建和删除&#xff08;静态方法&#xff09;&#xff08;掌握&#xff09; 一、任务创建和删除的API函数&a…

数据结构:初识树和二叉树

目前主流的方式是左孩子右兄弟表示法 我们的文件系统就是一个树 以上就是树的概念&#xff0c;我们今天还要来学习一种从树演变的重要的结构&#xff1a;二叉树 顾名思义二叉树就是一个结点最多有两个子树。 其中我们还要了解满二叉树和完全二叉树的概念 注意我们的完全二叉…

Java项目:78 springboot学生宿舍管理系统的设计与开发

作者主页&#xff1a;舒克日记 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 系统的角色&#xff1a;管理员、宿管、学生 管理员管理宿管员&#xff0c;管理学生&#xff0c;修改密码&#xff0c;维护个人信息。 宿管员管理公寓…

微服务高级篇(三):分布式缓存+Redis集群

文章目录 一、单点Redis的问题及解决方案二、Redis持久化2.1 单机安装Redis2.2 RDB持久化2.3 AOF持久化2.4 RDB和AOF对比 三、Redis主从3.1 搭建Redis主从架构3.1.1 集群结构3.1.2 准备实例和配置3.1.3 启动3.1.4 开启主从关系3.1.5 测试 3.2 数据同步3.2.1 全量同步【建立连接…

盏燕生物科技将出席2024第七届燕窝天然滋补品博览会

参展企业介绍 深圳市盏燕生物科技有限公司&#xff0c;办公室地址位于中国第一个经济特区&#xff0c;鹏城深圳&#xff0c;深圳市龙岗区平湖街道禾花社区富安大道18号亚钢工贸大楼1栋1017A&#xff0c;我公司主要提供一般经营项目是&#xff1a;初级农产品、海产品、化妆品、…

批量添加时,两个选择框为一组,不能选择一模一样的值,将不符合条件的值禁止设为禁止点击

效果展示&#xff1a; 完整代码如下&#xff1a; <template><div class"container"><div v-for"item in arr"><el-select v-model"item.name" placeholder"请选择" change"changeBox"><el-opti…

el-card设置内边距

el-card设置内边距 :deep(.el-card .el-card__body) {padding: 5px; }

Spring中常用的注解及使用规则

文章目录 前言一、Spring注解是什么&#xff1f;二、使用步骤1.注解使用 总结 前言 提示&#xff1a;这里可以添加本文要记录的大概内容&#xff1a; 在学习Spring中&#xff0c;我们汇经常用到注解来简化我们的工作&#xff0c;接下来将为大家简单介绍一下常用的注解 提示&a…

如何实现无公网IP及服务器实现公网环境企业微信网页应用开发调试

文章目录 1. Windows安装Cpolar2. 创建Cpolar域名3. 创建企业微信应用4. 定义回调本地接口5. 回调和可信域名接口校验6. 设置固定Cpolar域名7. 使用固定域名校验 企业微信开发者在应用的开发测试阶段&#xff0c;应用服务通常是部署在开发环境&#xff0c;在有数据回调的开发场…

蓝桥杯真题Day40 倒计时19天 纯练题!

蓝桥杯第十三届省赛真题-统计子矩阵 题目描述 给定一个 N M 的矩阵 A&#xff0c;请你统计有多少个子矩阵 (最小 1 1&#xff0c;最大 N M) 满足子矩阵中所有数的和不超过给定的整数 K? 输入格式 第一行包含三个整数 N, M 和 K. 之后 N 行每行包含 M 个整数&#xf…

java日志技术——Logback日志框架安装及概述

前言&#xff1a; 整理下学习笔记&#xff0c;打好基础&#xff0c;daydayup!!! 日志 什么是日志 程序中的日志&#xff0c;通常就是一个文件&#xff0c;里面记录的是程序运行过程中的各种信息&#xff0c;通过日志可以进行操作分析&#xff0c;bug定位等 记录日志的方案 程…

【爬虫基础】第6讲 opener的使用

在爬虫中&#xff0c;opener是一个用来发送HTTP请求的对象。它可以用来模拟浏览器发送请求&#xff0c;包括设置请求头、处理Cookie等操作。使用opener可以实现一些高级功能&#xff0c;如模拟登录、处理验证码等。 方法1&#xff1a; from urllib.request import Request,bu…