Go笔记

go 笔记

基础

变量

Copy
var 变量名 类型 = 表达式

其中 “类型” 或 “= 表达式” 两个部分可以省略其中的一个。如果省略的是类型信息，那么将根据

初始化表达式来推导变量的类型信息。如果初始化表达式被省略，那么将用零值初始化该变

量。数值类型变量对应的零值是 0，布尔类型变量对应的零值是 false，字符串类型对应的零

值是空字符串，接口或引用类型（包括 slice、指针、map、chan 和函数）变量对应的零值是

nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

make、new

Copy
i := 0
u := user{}

二者都是内存的分配（堆上），但是 make 只用于 slice、map 以及 channel 的初始化（非零值）；而 new 用于类型的内存分配，并且内存置为零

make 返回的还是这三个引用类型本身；而 new 返回的是指向类型的指针

new 不常用，通常都是采用短语句声明以及结构体的字面量达到我们的目的

range

Go 语言中 range 关键字用于 for 循环中迭代数组 (array)、切片 (slice)、通道 (channel) 或集合 (map) 的元素。在数组和切片中它返回元素的索引和索引对应的值，在集合中返回 key-value 对

切片

Copy
//创建一个初始元素长度为5的数组切片，元素初始值为0：
mySlice1 := make([]int, 5)
//创建一个初始元素长度为5的数组切片，元素初始值为0，并预留10个元素的存储空间：
mySlice2 := make([]int, 5, 10)
//切片字面量创建长度为5容量为5的切片,需要注意的是 [ ] 里面不要写数组的容量，因为如果写了个数以后就是数组了，而不是切片了。
mySlice3 := []int{10,20,30,40,50}

函数

函数语法格式：

Copy
func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}

func：函数由 func 开始声明

funcName：函数名称，函数名和参数列表一起构成了函数签名

小写：同一个包内可调用，不同包不可调用

大写：不同包内可调用（通过 import 包）

parametername type：参数列表

output type：返回值，Go 语言支持一个函数返回多个值

值传递

引用传递

引用传递本质引用传递本质上也是值传递，只不过这份值是一个指针（地址）

Go 语言中 slice，map 和 channel 这三种类型的实现机制类似指针， 所以可以直接传递，而不用取地址后传递指针。

defer

函数中添加 defer 语句，defer 是采用后进先出模式

Copy
func ReadWrite() bool {
file.Open("file")
defer file.Close()
}

踩坑点：

Copy
func testDefer() (r int) {
x := 1
defer func(xPtr *int) {
*xPtr++
}(&x)
return x
}

return x 不是一条原子指令，实际上 x=r def return

defer 是后进先出，协程遇到 panic 时，遍历本协程的 defer 链表，并执行 defer，如果没有遇到 recover，遍历完本协程的 defer 链表后，向 stderr 跑出 panic 信息

方法

Go 语言中同时有函数和方法。一个方法就是一个包含了 接受者 的函数，接受者可以是命名类型或者结构体类型的一个值或者是一个指针

继承

Copy
type Human struct {
Name string
Age int
Phone string
}

type Employee struct {
Human
Salary int
Currency string
}

接口

Copy
// 定义一个接口 Men
type Men interface {
SayHi()
Sing(lyrics string)
}

interface 可以被任意的对象实现

一个对象可以实现任意多个 interface

任意的类型都实现了空的 interface

一个 struct 如果实现了一个 interface，那么一个 interface 类型的变量 i，是可以被赋值为 struct 类型的，我们也可以从这个变量 i 中取出这个 struct 类型的值

错误

一个函数或方法返回一个错误，必须是函数返回的最后一个值

处理错误的方法：返回的错误与 nil 进行比较，nil 值表示没有发生错误

goroutine

封装 main 函数的 goroutine 称为主 goroutine

设定每一个 goroutine 所能申请的栈空间的最大值

创建一个特殊的 defer 语句，用于 goroutine 退出时做必要的善后处理

启动专用于在后台清扫内存垃圾的 goroutine，并设置 gc 可用的标识

执行 main 包中的 init 函数

执行 main 函数

检查 goroutine 是否引发运行 panic

最后主 goroutine 会结束自己以及当前进程的运行

channel

使用 channel 多重返回值

使用 for range 简化语法

对一个关闭的 channel 再发送值就会导致 panic

对一个关闭的 channel 进行接收会一直获取值直到 channel 为空

对一个关闭的并且没有值的 channel 执行接收操作会得到对应类型的零值

关闭一个已经关闭的 channel 会导致 panic

无缓冲 channel (make (chan int))，必须有接收者

关闭 channel

m 个接收者，1 个发送者，发送者通过关闭数据通道

Copy
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)

// ...
const MaxRandomNumber = 100000
const NumReceivers = 100

wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)

// 发送者
go func() {
for {
if value := rand.Intn(MaxRandomNumber); value == 0 {
// 唯一的发送者可以安全地关闭通道。
close(dataCh)
return
} else {
dataCh <- value
}
}
}()

// 接收者
for i := 0; i < NumReceivers; i++ {
go func() {
defer wgReceivers.Done()

// 接收数据直到 dataCh 被关闭或者
// dataCh 的数据缓存队列是空的。
for value := range dataCh {
log.Println(value)
}
}()
}

1 个接收者，n 个发送者，接收者通过关闭一个信号通道

Copy
package main

import (
"time"
"math/rand"
"sync"
"log"
)

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)

// ...
const MaxRandomNumber = 100000
const NumSenders = 1000

wgReceivers := sync.WaitGroup{}
wgReceivers.Add(1)

// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh 是一个信号通道。
// 它的发送者是 dataCh 的接收者。
// 它的接收者是 dataCh 的发送者。

// 发送者
for i := 0; i < NumSenders; i++ {
go func() {
for {
// 第一个 select 是为了尽可能早的尝试退出 goroutine。
// 事实上，在这个特殊的例子中，这不是必要的，所以它能省略。
select {
case <- stopCh:
return
default:
}

// 即使 stopCh 已经关闭，如果发送给 dataCh 没有阻塞，那么在第二个 select 中第一个分支可能会在一些循环中不会执行。
// 但是在这里例子中是可接受的， 所以上面的第一个 select 代码块可以被省略。
select {
case <- stopCh:
return
case dataCh <- rand.Intn(MaxRandomNumber):
}
}
}()
}

// 接收者
go func() {
defer wgReceivers.Done()

for value := range dataCh {
if value == MaxRandomNumber-1 {
// dataCh 通道的接收者也是 stopCh 通道的发送者。
// 在这里关闭停止通道是安全的。.
close(stopCh)
return
}

log.Println(value)
}
}()

// ...
wgReceivers.Wait()
}

m 个接收者，n 个发送者，一个通过通知一个主持人关闭一个信号通道

Copy
package main

import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)

func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)

// ...
const MaxRandomNumber = 100000
const NumReceivers = 10
const NumSenders = 1000

wgReceivers := sync.WaitGroup{}
wgReceivers.Add(NumReceivers)

// ...
dataCh := make(chan int, 100)
stopCh := make(chan struct{})
// stopCh 是一个信号通道。
// 它的发送者是下面的主持人 goroutine。
// 它的接收者是 dataCh的所有发送者和接收者。
toStop := make(chan string, 1)
// toStop 通道通常用来通知主持人去关闭信号通道( stopCh )。
// 它的发送者是 dataCh的任意发送者和接收者。
// 它的接收者是下面的主持人 goroutine

var stoppedBy string

// 主持人
go func() {
stoppedBy = <-toStop
close(stopCh)
}()

// 发送者
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(MaxRandomNumber)
if value == 0 {
// 这里，一个用于通知主持人关闭信号通道的技巧。
select {
case toStop <- "sender#" + id:
default:
}
return
}

// 这里的第一个 select 是为了尽可能早的尝试退出 goroutine。
//这个 select 代码块有 1 个接受行为 的 case 和 一个将作为 Go 编译器的 try-receive 操作进行特别优化的默认分支。
select {
case <- stopCh:
return
default:
}

// 即使 stopCh 被关闭， 如果 发送到 dataCh 的操作没有阻塞，那么第二个 select 的第一个分支可能会在一些循环（和在理论上永远）
// 不会执行
// 这就是为什么上面的第一个 select 代码块是必须的。
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}

// 接收者
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()

for {
// 和发送者 goroutine 一样， 这里的第一个 select 是为了尽可能早的尝试退出 这个 goroutine。
// 这个 select 代码块有 1 个发送行为 的 case 和 一个将作为 Go 编译器的 try-send 操作进行特别优化的默认分支。
select {
case <- stopCh:
return
default:
}

// 即使 stopCh 被关闭， 如果从 dataCh 接受数据不会阻塞，那么第二个 select 的第一分支可能会在一些循环（和理论上永远）
// 不会被执行到
// 这就是为什么第一个 select 代码块是必要的。
select {
case <- stopCh:
return
case value := <-dataCh:
if value == MaxRandomNumber-1 {
// 同样的技巧用于通知主持人去关闭信号通道。
select {
case toStop <- "receiver#" + id:
default:
}
return
}

log.Println(value)
}
}
}(strconv.Itoa(i))
}

// ...
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}

json

首先查找 tag 名字（关于 json tag 的解释参看下一节）为 Field 的字段

然后查找名字为 Field 的字段

最后再找名字为 Field 等大小写不敏感的匹配字段。

如果都没有找到，就直接忽略这个 key，也不会报错。这对于要从众多数据中只选择部分来使用非常方便。

reflect

Copy
var i int = 3
type test struct{}
var t test
v := reflect.ValueOf(i)
v2 := reflect.ValueOf(&t).Elem()

reflect.ValueOf ()：返回值

reflect.ValueOf ().Elem ()：传入必须是指针类型

struct

struct {}{}：空结构体的作用

因为空结构体不占据内存空间，因此被广泛作为各种场景下的占位符使用。一是节省资源，二是空结构体本身就具备很强的语义，即这里不需要任何值，仅作为占位符。

Go 语言标准库没有提供 Set 的实现，通常使用 map 来代替。事实上，对于集合来说，只需要 map 的键，而不需要值。即使是将值设置为 bool 类型，也会多占据 1 个字节，那假设 map 中有一百万条数据，就会浪费 1MB 的空间。

因此呢，将 map 作为集合 (Set) 使用时，可以将值类型定义为空结构体，仅作为占位符使用即可。

go 内存申请

程序向操作系统申请一块内存 (堆和栈)

申请到栈内存好处：函数返回直接释放，不会引起垃圾回收，对性能没有影响

申请到堆内存：引起垃圾回收

Copy
// 实例1：栈
func F() {
temp := make([]int, 0, 20)
...
}
// 实例2：堆
func F() []int{
a := make([]int, 0, 20)
return a
}
// 实例3
func F() {
a := make([]int, 0, 20) // 栈
b := make([]int, 0, 20000) // 容量比较大,堆
l := 20
c := make([]int, 0, l) // 不定长,堆
}

go init

import --> const --> var --> init()

每个 package 中每个 init () 函数都会被调用，且顺序固定

对同一个 go 文件的 init () 调用顺序是从上到下的
对同一个 package 中不同文件是按文件名字符串比较 “从小到大” 顺序调用各文件中的 init () 函数，对于
对不同的 package，如果不相互依赖的话，按照 main 包中 "先 import 的后调用" 的顺序调用其包中的 init ()
如果 package 存在依赖，则先调用最早被依赖的 package 中的 init ()

unsafe.Pointer

大多数指针类型会写成*T，表示是 “一个指向 T 类型变量的指针”。unsafe.Pointer 是特别定义的一种指针类型（译注：类似 C 语言中的void*类型的指针），它可以包含任意类型变量的地址

Sizeof 接受任意类型的值 (表达式)，返回其占用的字节数，这和 c 语言里面不同，c 语言里面 sizeof 函数的参数是类型，而这里是一个值，比如一个变量

Offsetof：返回结构体成员在内存中的位置距离结构体起始处的字节数，所传参数必须是结构体的成员（结构体指针指向的地址就是结构体起始处的地址，即第一个成员的内存地址）

flag 库

使用flag库的一般步骤：

定义一些全局变量存储选项的值，如这里的intflag/boolflag/stringflag；

在init方法中使用flag.TypeVar方法定义选项，这里的Type可以为基本类型Int/Uint/Float64/Bool，还可以是时间间隔time.Duration。定义时传入变量的地址、选项名、默认值和帮助信息；

在main方法中调用flag.Parse从os.Args[1:]中解析选项。因为os.Args[0]为可执行程序路径，会被剔除。

go -ini 库

context 库

创建 context：

context.Backgroud()

context.TODO

With 系列函数

Copy
// 取消控制
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 超时控制
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 携带数据
func WithValue(parent Context, key, val interface{}) Context

sync 库

sync.map：

sync.map.Range () 遍历求长度

sync.pool：缓存对象

在 Put 之前重置，在 Get 之后重置

sync.RWMutex：读写锁

如果设置了写锁，其他读线程和写线程就无法获得锁。这时候和互斥锁的作用是一样的

如果设置了读锁，其他写线程不能拿到锁，但是其他读线程可以拿到锁

sync.Cod：

sync.Cond典型的使用場景是生产 - 消費者模式，多個 goroutine等待某個事件發生， 单個goroutine 通知某個事件已發生。

etcd

etcd 是一个高可用强一致性的键值仓库在很多分布式系统架构中得到了广泛的应用，其最经典的使用场景就是服务发现

一个强一致性、高可用的服务存储目录

一种注册服务和监控服务健康状态的机制

一种查找和连接服务的机制

流程：

从 Leader （主节点）流向 Follower；

用户可以对 etcd 集群中的所有节点进行读写

micro

go 语言构建微服务的基础框架

例子：

服务端

Copy
service guild {
//查询
rpc Find(FindRequest) returns (FindResponse){}
//查询玩家数据
rpc FindUser(FindUserRequest) returns (FindUserResponse){}
}

guild 服务：

main.go

Copy
func main() {
reg := etcd.NewRegistry(func(op *registry.Options) {
op.Addrs = settings.GetEtcdSysAddr()
})
uuid := utils.GenUUID()

s := grpc.NewService(
micro.Server(grpc2.NewServer(func(options *server.Options) {
options.Id = uuid
})),
micro.Name(fmt.Sprintf("com.jsw.server.guild.%d", settings.GetSetId())),
micro.Registry(reg),
micro.RegisterInterval(time.Second*2),
micro.RegisterTTL(time.Second*5),
micro.WrapHandler(LogHandler),
)
// GuildHandler对象必须实现.proto文件定义的这些接口函数
registerErr := guildProto.RegisterGuildHandler(s.Server(), &handler.GuildHandler{})
s.Init()
s.Run()
}

guild.go

Copy
type GuildHandler struct {
}
func (m *GuildHandler) Find(ctx context.Context, request *guildProto.FindRequest, response *guildProto.FindResponse) error {}
func (m *GuildHandler) FindUser(ctx context.Context, request *guildProto.FindUserRequest, response *guildProto.FindUserResponse) error {}

game 服务：

Copy
guildServer = guildProto.NewGuildService(fmt.Sprintf("com.jsw.server.guild.%d", setId), ser.Client())

gorm

Copy
// 获取第一条记录（主键升序）
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// 获取一条记录，没有指定排序字段
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// 获取最后一条记录（主键降序）
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

go protobuf

.proto 文件定义 go_package = "xxx/proto/game;com_jsw_server_game" ，；前面是生成.pb.go 文件路径，；后面是别名

Copy
//go:generate protoc --proto_path=. --proto_path=../ --proto_path=../public --proto_path=../game --gofast_out=. --micro_out=. common.proto

grpc

Copy
message SimpleRequest {
string name = 1;
}
message SimpleResponse {
string message = 1;
}

service SimpleService {
rpc Get(SimpleRequest) returns (SimpleResponse) {} // 简单模式
rpc GetUser(SimpleRequest) return (stream SimpleResponse) {} // 服务端数据流模式
rpc GetUser(stream SimpleRequest) return (SimpleResponse) {} // 客户端数据流模式
rpc GetUser(stream GetUserRequest) returns (stream GetUserResponse) {} // 双端模式
}

四种模式：

简单 RPC

Copy
// c
resopnse, err := client.Get(contect.Background(), &pb.SimpleRequest{Name: "hello"})
// s
func (s *Simple) Get(ctx context.Context, req *pb.SimpleRequest) (resp *pb.SimpleResponse, error) {
return resp
}

服务端数据流

Copy
// c
user, err := client.GetUser(context.Background(), &pb.SimpleRequest{Name: "hello"})
for {
resp, err := user.Recv() // 调用Recv去接收
if err == io.EOF {
break
}
...
}
// s
func (s *ServerSide) GetUser(req *pb.GetUserRequest, stream pb.ServerSide_GetUserServer) error {
...
for {
stream.Send(&pb.SimpleResponse{}) // 调用Send去回复
}
return nil
}

客户端数据流

Copy
// c
user, err := client.GetUser(context.Background())
for {
err := user.Send(&pb.SimpleRequest{Name: "hello"})
}
// s
func (c *ClientSide) GetUser(stream pb.ClientSide_GetUserServer) error {
recv, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&pb.GetUserResponse{})
}
}

双向数据流

Copy
// c
user, err := client.GetUser(context.Background())
err := user.Send(&pb.SimpleRequest{Name: "hello"})
err = user.CloseSend()
// s
func (b *BidirectionalService) GetUser(stream pb.BidirectionalService_GetUserServer) (err error) {
req, err := stream.Recv()
err = stream.Send(&pb.SimpleRequest{Name: "hello"})
}

go cli

cli是一个用于构建命令行程序的库，创建一个 cli.App 结构对象

Copy
func main() {
app := &cli.App{
Name: "hello",
Usage: "hello world example",
Action: func(c *cli.Context) error {
fmt.Println("hello world")
return nil
},
}

err := app.Run(os.Args)
}

Name 和 Usage 都显示在帮助中

Action 是调用该命令行程序实际执行的函数