Open wangbo123855842 opened 3 years ago
Wire 是一个轻巧的 Golang 依赖注入工具。它由 Go Cloud 团队开发,通过自动生成代码的方式在编译期完成依赖注入。
作为一个代码生成工具, Wire 可以生成 Go 源码并在编译期完成依赖注入。Wire 生成的代码与手写无异。
go get github.com/google/wire/cmd/wire
wire.go 中,调用各个结构体(对象)的初始化方法,比如
wire.Build(
controller.NewTenantController,
service.NewTenantService,
usecase.NewTenantUseCase,
)
Controller 的初始化方法 NewTenantController 里面,需要两个成员变量 TenantService 和 TenantUseCase
type tenantController struct {
service.TenantService
usecase.TenantUseCase
}
func NewTenantController(ts service.TenantService, tu usecase.TenantUseCase) TenantController {
return &tenantController{TenantService: ts, TenantUseCase: tu}
}
有了这些代码以后,运行 wire 命令将生成 wire_gen.go 文件,其中保存了 injector 函数的真正实现。
tenantService := service.NewTenantService(xxx)
tenantUseCase := usecase.NewTenantUseCase(xxx)
tenantController := controller.NewTenantController(tenantService, tenantUseCase)
要触发“生成”动作有两种方式:go generate 或 wire 。 前者仅在 wire_gen.go 已存在的情况下有效,而后者在任何时候都有可以调用。
在 Go 的标准库中并没有提供对 Sessoin 的实现。我们需要自己来实现 Session。
实现 Session 主要需要考虑以下几点
实现的过程,参考 Go 语言中使用 Session
在项目中我们通常可能会使用 database/sql
连接MySQL数据库。
sqlx 可以认为是 Go 语言内置 database/sql
的超集,它在优秀的内置 database/sql 基础上提供了一组扩展。
sqlx 设计和 database/sql 使用方法是一样的。包含有4中主要的 handle types
sqlx.DB 和sql.DB相似,表示数据库。
sqlx.Tx 和sql.Tx相似,表示transacion。
sqlx.Stmt 和sql.Stmt相似,表示prepared statement。
sqlx.NamedStmt 表示prepared statement(支持named parameters)
所有的 handler types 都提供了对 database/sql 的兼容,意味着当你调用 sqlx.DB.Query 时,可以直接替换为 sql.DB.Query。 这就使得 sqlx 可以很容易的加入到已有的数据库项目中。
此外,sqlx 还有两个 cursor 类型
sqlx.Rows 和sql.Rows类似,Queryx返回。
sqlx.Row 和sql.Row类似,QueryRowx返回。
你可以通过 Open 创建一个 sqlx.DB 或 **通过 NewDb 从已存在的 sql.DB 中创建一个新的 sqlx.DB。
var db *sqlx.DB
// exactly the same as the built-in
db = sqlx.Open("sqlite3", ":memory:")
// from a pre-existing sql.DB; note the required driverName
db = sqlx.NewDb(sql.Open("sqlite3", ":memory:"), "sqlite3")
// 创建一个连接,测试是否通过
err = db.Ping()
调用 connect,打开一个 DB 的同时连接 DB。这个函数打开一个新的 DB 并尝试 Ping。
db, err := sqlx.Connect("mysql", "xxx")
if err != nil {
log.Fatalln(err)
}
db.SetMaxIdleConns(0) // 设置理想最大连接数
取得结果的时候,使用 rows.StructScan。
var survey model.Survey
const q = `SELECT tenant_id, survey_id, status, created_at, created_by, updated_at, updated_by FROM survey WHERE survey_id = ? AND tenant_id = ?`
if err := r.db.QueryRowxContext(ctx, q, surveyID, tenantID).StructScan(&survey); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, isxerr.New(err, isxerr.ResourceNotFound, "survey not found")
}
return nil, err
}
return &survey, nil
查询多条
var questions []*model.SurveyQuestion
const q = `SELECT tenant_id, survey_id, question_id, description, question_type, answer_required, question_order FROM survey_question WHERE survey_id = ? AND tenant_id = ? ORDER BY question_order`
if err := r.db.SelectContext(ctx, &questions, q, surveyID, tenantID); err != nil {
return nil, err
}
return questions, nil
sql.Tx 的 ExecContext方法来执行 insert,update,delete
var tx *sql.Tx
const q = `INSERT INTO survey (tenant_id, survey_id, status, created_by, updated_by) VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE status = ?, updated_by = ?`
if _, err := tx.ExecContext(ctx, q, tenantID, survey.SurveyID, survey.Status, userID, userID,
survey.Status, userID,
); err != nil {
return err
}
return nil
golang 标准库 testing 包为 Go 代码支持了自动化测试。使用 go test 命令来执行。
函数测试定义
func TestXxx(*testing.T)
学习路线
前言
Go 是一门编译型,具有静态类型和类 C 语言语法的语言,并且有垃圾回收(GC)机制。 静态类型意味着变量必须是特定的类型。(如:int, string, bool, [] byte 等等),这可以通过在声明变量的时候,指定变量的类型来实现,或者让编译器自行推断变量的类型。
Go 被设计成代码在工作区内运行。 工作区是一个文件夹,这个文件夹由 bin ,pkg,以及 src 子文件夹组成的。
环境变量
运行一个go程序
执行命令
go run 命令已经包含了编译和运行。 它使用一个临时目录来构建程序,执行完然后清理掉临时目录。 在 go 中程序入口必须是 main 函数,并且在 main 包内。
只是编译代码
变量
Go语言的基本类型
当一个变量被声明之后,系统自动赋予它该类型的零值。所有的内存在 Go 中都是经过初始化的。
Go语言的变量声明的标准格式
批量声明
简短格式 除 var 关键字外,还可使用更加简短的变量定义和初始化语法
需要注意的是,简短模式有以下限制
由于使用了
:=
,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。短变量声明的形式在开发中很常用,比如
匿名变量
匿名变量的特点是一个下画线
_
,_
本身就是一个特殊的标识符,被称为空白标识符。Go语言变量的作用域
字符串
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。
字符串拼接 使用 + 符号。
len()
函数。utf8.RuneCountInString()
函数Go 语言的字符串是不可变的。修改字符串时,可以将字符串转换为 []byte 进行修改。 []byte 和 string 可以通过强制类型转换互转。
Go 语言的字符串无法直接修改每一个字符元素,只能通过重新构造新的字符串并赋值给原来的字符串变量实现。 例如
comma := strings.Index(tracer,",") pos := strings.Index(tracer[comma:], "g")
字符串索引比较常用的有如下几种方法 strings.Index:正向搜索子字符串 strings.LastIndex:反向搜索子字符串
Go 语言的标准库自带了 Base64 编码算法 使用 encoding/base64
Go语言的字符有以下两种
数据类型转换
指针
&
操作符,可以获得这个变量的指针变量。*
操作符,可以获得指针变量指向的原变量的值。创建指针的另一种方法 new() 函数,比如
new() 函数可以创建一个对应类型的指针,创建过程会分配内存,被创建的指针指向默认值。
常量和const关键字
常量的值必须是能够在编译时就能够确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
iota 常量生成器
在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1。
类型别名
比如
将 NewInt 定义为 int 类型,这是常见的定义类型的方法,通过 type 关键字的定义,NewInt 会形成一种新的类型,NewInt 本身依然具备 int 类型的特性。 将 IntAlias 设置为 int 的一个别名,使用 IntAlias 与 int 等效。
控制语句
if 还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断
Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构 如果要实现 while
另外,break,continue 可以结合标签
for range 结构是Go语言特有的一种的迭代结构,在许多情况下都非常有用,for range 可以遍历数组、切片、字符串、map 及通道(channel),for range 语法上类似于其它语言中的 foreach 语句 格式
另外,val 是一个值拷贝,修改这个值,不会影响原数组,原切片等。如果需要修改原数组的数据,可以使用
数组变量[key] = xx
Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行
如果想跨越 case 的话,手动使用 fallthrough
Go语言中 goto 语句通过标签进行代码间的无条件跳转,同时 goto 语句在快速跳出循环、避免重复退出上也有一定的帮助
数组
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。 因为数组的长度是固定的,所以在Go语言中很少直接使用数组。 和数组对应的类型是 Slice(切片),Slice 是可以增长和收缩的动态序列,功能也更灵活。
默认情况下,数组的每个元素都会被初始化为元素类型对应的零值
在数组的定义中,如果在数组长度的位置出现 ... 省略号,则表示数组的长度是根据初始化值的个数来计算
切片
切片 slice 是对数组的一个连续片段的引用,所以切片是一个引用类型。
切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。
如果需要动态地创建一个切片,可以使用 make() 内建函数
其中 Type 是指切片的元素类型,size 指的是为这个类型初始化多少个元素,cap 为预分配的元素数量。 cap的值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题。 比如
Go语言的内建函数 append() 可以为切片动态添加元素。
使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行扩容,此时新切片的cap会发生改变
Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中。 如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
比如
Go语言中删除切片元素的本质是,以被删除元素为分界点,将前后两个部分的内存重新连接起来。
map
映射可以和切片一样,使用 make 方法来创建。
我们使用 len 方法类获取映射的键的数量
使用 delete 方法来删除一个键对应的值,另外,Go语言中并没有为 map 提供任何清空所有元素的方法。
映射是动态变化的。然而我们可以通过传递第二个参数到 make 方法来设置一个初始大小
Go 还可以这样定义和初始化
函数 Func
Go语言支持多返回值。 Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,比如
声明
我们可以像这样使用最后一个
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中
...type 格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数 从内部实现机理上来说,类型...type本质上是一个数组切片,也就是[]type
args切片是一个值拷贝,更改args切片不影响原切片,跟直接传递一个切片是不一样的。
任意类型的可变参数 如果你希望传任意类型,可以指定类型为 interface{} 用 interface{} 传递任意类型数据是Go语言的惯例用法,使用 interface{} 仍然是类型安全的
Go语言自带了 testing 测试包,可以进行自动化的单元测试 要开始一个单元测试,需要准备一个 go 源码文件,在命名文件时文件名必须以
_test.go
结尾demo.go
demo_test.go
执行测试命令,运行结果
还可以进行性能测试
运行
覆盖率测试 执行测试命令
另外,GO 语言的函数,不允许方法重载。
异常
Go语言是不支持 try…catch…finally 这种异常处理的。 在Go语言中,使用多值返回来返回错误。不要用异常代替错误。 在极个别的情况下,才使用Go中引入的Exception处理:defer, panic, recover。
defer的特性是,在函数返回之前, 调用defer函数的操作, 简化函数的清理工作。 类似于 finally 函数内可以有多个defered函数,但是这些defered函数在函数返回时遵守后进先出的原则。
panic用法挺简单的, 其实就是throw exception。 panic是golang的内建函数,panic会中断函数的正常执行流程, 从函数中跳出来, 跳回到函数的调用者。 对于调用者来说, 这个函数看起来就是一个panic,所以调用者会继续向上跳出, 直到当前goroutine返回。 defered函数会正常执行。
recover 也是 golang 的一个内建函数, 其实就是try catch。 recover如果想起作用的话, 必须在defered函数中使用。
如果当前的 goroutine panic 了,那么 recover 将会捕获这个 panic 的值,并且让程序正常执行下去。不会让程序crash。
输出下面内容,goroutine 不会异常终止
结构体 struct
Go 语言中没有 类 的概念,也不支持 类的继承等面向对象的概念。 Go 语言的结构体与类都是复合结构体。 Go 语言不仅认为结构体能拥有方法,且每种自定义类型也可以拥有自己的方法。
结构体成员也可以称为字段,这些字段有以下特性
实例化
结构体本身是一种类型,可以像整型、字符串等类型一样,以 var 的方式声明结构体即可完成实例化
Go语言中,还可以使用 new 关键字对类型进行实例化,结构体在实例化后会形成指针类型的结构体。
point是一个指针类型,但是操作结构体的变量,仍然正常使用
.
来操作。或者 使用结构体指针
Go语言的类型或结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。
继承
Go语言中的继承是通过内嵌或组合来实现的,所以可以说,在Go语言中,相比较于继承,组合更受青睐。
方法
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
接收器类型可以几乎是任何类型,不仅仅是结构体类型,任何类型都可以有方法,可以是 int、bool、string 或数组的别名类型,但是接收器不能是一个接口类型。
在面向对象的语言中,类拥有的方法一般被理解为类可以做的事情。在Go语言中方法的概念与其他语言一致,只是Go语言建立的接收器强调方法的作用对象是接收器,也就是类实例,而函数没有作用对象。
每个方法只能有一个接收器。
接收器的格式如下
接收器类型可以是 指针类型 或者是 非指针类型。
区别在于,当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。
指针类型
非指针类型
接口
Go语言不是一种传统的面向对象编程语言。它里面没有类和继承的概念。 Go语言中接口类型的独特之处在于它是满足隐式实现的。Go语言中没有类似于 implements 的关键字。也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型,简单地拥有一些必需的方法就足够了。简单地说,就是实现了接口的方法就可以,不需显式的指定接口。
Go语言的每个接口中的方法数量不会很多。Go语言希望通过一个接口精准描述它自己的功能,而通过多个接口的嵌入和组合的方式将简单的接口扩展为复杂的接口。
接口与结构体结合,实现多态的效果
当接口没有方法时,它被称为空接口。 这由
interface{}
表示。 由于空接口没有任何方法,因此所有类型都实现了该接口。 相当于最上级的元素,比如 Java 的 Object。一个类型可以实现多个接口。
使用
o.(type)
来判断接口类型在go中,接口不能实现其他接口或扩展它们,但我们可以通过合并两个或多个接口来创建新接口。
之前,接口的实现方法的接受者是结构体,不是指针类型, 如果换成指针类型,需要这样
Go语言中引入 error 接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含 error。 一般情况下,如果函数需要返回错误,就将 error 作为多个返回值中的最后一个。 创建一个 error 最简单的方法就是调用 errors.New 函数,它会根据传入的错误信息返回一个新的 error。
包
Go 语言的源码复用建立在包(package)基础之上。 Go 语言的入口 main() 函数所在的包(package)叫 main,main 包想要引用别的代码,必须同样以包的方式进行引用。
包的习惯用法 包名一般是小写的。 包名一般要和所在的目录同名。 包一般使用域名作为目录名称,这样能保证包名的唯一性。 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。
常用内置包
fmt fmt 包实现了格式化的标准输入输出,其中的 fmt.Printf() 和 fmt.Println() 是开发者使用最为频繁的函数。
io 它主要的任务是对 os 包这样的原始的 I/O 进行封装。
bufio bufio 包通过对 io 包的封装,提供了数据缓冲功能,能够一定程度减少大块数据读写带来的开销。 在 bufio 各个组件内部都维护了一个缓冲区,数据读写操作都直接通过缓存区进行。当发起一次读写操作时,会首先尝试从缓冲区获取数据,只有当缓冲区没有数据时,才会从数据源获取数据更新缓冲。
sort sort 包提供了用于对切片和用户定义的集合进行排序的功能。
strconv strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能。
os os 包提供了不依赖平台的操作系统函数接口,设计像 Unix 风格,但错误处理是 go 风格,当 os 包使用时,如果失败后返回错误类型而不是错误数量。
sync sync 包实现多线程中锁机制以及其他同步互斥机制。
flag flag 包提供命令行参数的规则定义和传入参数解析的功能。绝大部分的命令行程序都需要用到这个包。
encoding/json JSON 目前广泛用做网络程序中的通信格式。encoding/json 包提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等。
html/template 主要实现了 web 开发中生成 html 的 template 的一些函数。
net/http net/http 包提供 HTTP 相关服务,主要包括 http 请求、响应和 URL 的解析,以及基本的 http 客户端和扩展的 http 服务。 通过 net/http 包,只需要数行代码,即可实现一个爬虫或者一个 Web 服务器,这在传统语言中是无法想象的。
reflect reflect 包实现了运行时反射。
os/exec os/exec 包提供了执行自定义 linux 命令的相关实现。
strings strings 包主要是处理字符串的一些函数集合,包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等。 strings 包与 bytes 包的函数接口功能基本一致。
bytes bytes 包提供了对字节切片进行读写操作的一系列函数。字节切片处理的函数比较多,分为基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数和子切片处理函数等。
log log 包主要用于在程序中输出日志。
time 处理时间,日期
context 专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作。
使用 Go Modules 管理依赖
go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
使用 go module 管理依赖后会在项目根目录下生成两个文件 go.mod 和 go.sum。
go.mod 中会记录当前项目的所依赖
go.sum记录每个依赖库的版本和哈希值
使用 go module 需要设定环境变量
然后在项目路径下,执行 go mod init
之后,在工程中引入第三方的 module
go run main.go运行代码会发现 go mod 会自动查找依赖自动下载。
下载的第三方包,会保存在 GOPATH 的 pkg / mod 下
反射
空接口相当于一个容器,能接受任何东西。如果想获取存储变量的类型信息和值信息就要使用反射机制。 利用 GO语言里面的 Reflect 包来实现反射。
如果要赋值,就需要指针类型
并发
Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程。 goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go 语言秉承的 CSP 并发模式的实现基础。
在java中我们要实现并发编程的时候,我们通常需要自己维护一个线程池,并且需要自己去包装一个又一个的任务和然后自己去调度线程执行任务并维护上下文切换。
goroutine 的概念类似于线程,但 goroutine 由Go 程序运行时的调度和管理。Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。 Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。 在Go语言编程中你不需要去自己写进程、线程、协程,你的技能包里只有一个技能–goroutinue,当你需要让某个任务并发执行的时候,你只需要起一个goroutinue就可以了。
比如,这样使用关键字 go ,就启动了一个goroutine。 使用
time.Sleep(time.Second)
的目的是,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。在上面代码中的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。
OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这个大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
OS线程是由OS内核来调度的,goroutine则是由Go运行时自己的调度器调度的。 goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多。
Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。 例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上。 Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
总结一下,Go语言中的操作系统线程和goroutine的关系
channel
go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信。 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。 channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。 Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。
每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
声明的通道后需要使用make函数初始化之后才能使用。
channel操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用<-符号。
发送
接受
关闭
只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。关闭后的通道有以下特点:
通道,又分 无缓冲的通道 和 有缓冲的通道。 无缓冲的通道只有在有人接收值的时候才能发送值。使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。
只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。
我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。
循环接受通道消息
我们通常使用的是 for range 的方式。
有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如只能发送或只能接收。Go语言中提供了单向通道来处理这种情况。
chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。 在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。
在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。 Go内置了select关键字,可以同时响应多个通道的操作。
有时候在Go代码中可能会存在多个goroutine同时操作一个资源,这种情况会发生竞态问题。
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。、 Go语言中使用sync包的Mutex类型来实现互斥锁。
使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁。 当互斥锁释放后,等待的goroutine才可以获取锁进入临界区。
读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。
读写锁在Go语言中使用sync包中的RWMutex类型。读写锁分为两种:读锁和写锁。 当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待。 当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
标准类库
io 基本的 IO 接口
Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中
所有实现了 Read 方法的类型都满足 io.Reader 接口。 也就是说,在所有需要 io.Reader 的地方,可以传递实现了 Read() 方法的类型的实例。
比如
ReadFrom 函数将 io.Reader 作为参数,也就是说,ReadFrom 可以从任意的地方读取数据,只要来源实现了 io.Reader 接口。比如,我们可以从标准输入、文件、字符串等读取数据。
同样的,所有实现了Write方法的类型都实现了 io.Writer 接口。
在fmt标准库中,有一组函数:Fprint/Fprintf/Fprintln,它们接收一个 io.Wrtier 类型参数,也就是说它们将数据格式化输出到 io.Writer 中。
有哪些实现了 Read / Writer 接口呢
常用的类型有,os.File、strings.Reader、bufio.Reader/Writer、bytes.Buffer、bytes.Reader。
ReaderAt 接口使得可以从指定偏移量处开始读取数据。 通过WriterAt 接口将数据写入到数据流的特定偏移量之后。
一次性从某个地方读或写到某个地方去。可以使用这两个接口
比如,实现将文件中的数据全部读取,显示在标准输出
我们也可以通过 ioutil 包的 ReadFile 函数获取文件全部内容。
该接口比较简单,只有一个 Close() 方法,用于关闭数据流。 文件 (os.File)、归档(压缩包)、数据库连接、Socket 等需要手动关闭的资源都实现了 Closer 接口。 实际编程中,经常将 Close 方法的调用放在 defer 语句中。
注意,应该将 defer file.Close() 放在错误检查之后。
ioutil 方便的IO操作函数集
一次性读取 io.Reader 中的数据
它读取目录并返回排好序的文件和子目录名( []os.FileInfo )
ReadFile 的实现和ReadAll 类似,不过,ReadFile 会先判断文件的大小,给 bytes.Buffer 一个预定义容量,避免额外分配内存。
通过 TempDir 创建一个临时目录。 TempFile 用于创建临时文件。
注意:创建者创建的临时文件和临时目录要负责删除这些临时目录和文件。
bufio 缓存IO
bufio.Reader 结构包装了一个 io.Reader 对象,提供缓存功能,同时实现了 io.Reader 接口。
对于简单的读取一行,Go1.1增加了一个类型:Scanner。
strings — 字符串操作
常用的操作
bytes byte slice 便利操作
该包定义了一些操作 byte slice 的便利操作。 因为字符串可以表示为 []byte,因此,bytes 包定义的函数、方法等和 strings 包很类似。 比如
strconv 字符串和基本数据类型之间转换
Atoi 是 ParseInt 的便捷版,内部通过调用 ParseInt(s, 10, 0) 来实现的。 ParseInt 转为有符号整型。 ParseUint 转为无符号整型。
我们经常会遇到需要将字符串和整型连接起来,在 Java 中,可以通过操作符 "+" 做到。不过,在 Go 语言中,你需要将整型转为字符串类型,然后才能进行连接。
Itoa 内部直接调用 FormatInt(i, 10) 实现的。
unicode
go 对 unicode 的支持包含三个包
utf16 包负责 rune 和 uint16 数组之间的转换。
unicode 包包含基本的字符判断函数。
utf8 包主要负责 rune 和 byte 之间的转换。
判断是否符合 utf8 编码的函数
判断字节串或者字符串的 rune 数
比如
Sort
数据集合,包括自定义数据类型的集合 排序需要实现 sort.Interface 接口的三个方法
数据集合实现了这三个方法后,即可调用该包的 Sort() 方法进行排序。
sort包已经支持的内部数据类型排序
sort包原生支持[]int、[]float64 和[]string 三种内建数据类型切片的排序操作,即不必我们自己实现相关的 Len()、Less() 和 Swap() 方法。
[]int int 切片
如果要使用降序排序
container包
该包实现了三个复杂的数据结构:堆,链表,环。
常用的list
日期与时间
Go 语言通过标准库 time 包处理日期和时间相关的问题。
时间可分为时间点与时间段,提供了以下两种基础类型
除此之外 Go 也提供了以下类型,做一些特定的业务
初始化
time.Parse , 其中layout的时间必须是"2006-01-02 15:04:05"这个时间,不管格式如何,时间点一定得是这个。
格式化
时间戳
其他
Duartion
比较两个时间点
Ticker类型
有时我们会遇到每隔一段时间执行的业务(比如设置心跳时间等),就可以用它来处理,这是一个重复的过程。
OS 包
os 包规定为所有操作系统实现的接口都是一致的。 有一些某个系统特定的功能,需要使用 syscall 获取。 实际上,os 依赖于 syscall。在实际编程中,我们应该总是优先使用 os 中提供的功能,而不是 syscall。
打开一个文件,一般通过 Open 或 Create。Open 是只读。
flag 包
flag 包实现了命令行参数的解析。
log 包
context 包
控制并发有两种经典的方式,一种是 WaitGroup ,另外一种就是 Context。
比如一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。 所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。
过去,我们使用 chan + select 来通知 goroutine 处理结束。比如
使用Go Context重写。
把原来的chan stop 换成Context,使用Context跟踪 goroutine,以便进行控制,比如结束等。
流程解析
context.Background()
返回一个空的Context,这个空的Context一般用于整个Context树的根节点。 然后我们使用context.WithCancel(parent)
函数,创建一个可取消的子Context,然后当作参数传给goroutine使用,这样就可以使用这个子Context跟踪这个goroutine。ctx.Done()
判断是否要结束,如果接受到值的话,就可以返回结束goroutine了。 context.WithCancel(parent)函数生成子Context的时候返回的,第二个返回值就是这个取消函数,它是CancelFunc类型的。我们调用它就可以发出取消指令,然后我们的监控goroutine就会收到信号,就会返回结束。Context控制多个goroutine
控制多个goroutine的例子,其实也比较简单。
启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。
Go内置已经帮我们实现了2个Context接口。
context.Background(),一般作为Context这个树结构的最顶层的Context,也就是根Context。 context.TODO(),它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
有了如上的根Context,那么是如何衍生更多的子Context的呢?这就要靠context包为我们提供的With系列的函数了。
通过这些函数,就创建了一颗Context树
前三个函数都返回一个取消函数CancelFunc,这就是取消函数的类型,该函数可以取消一个Context,以及这个节点Context下所有的所有的Context,不管有多少层级。
通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。
我们可以使用context.WithValue方法附加一对K-V的键值对。 这样我们就生成了一个新的Context,这个新的Context带有这个键值对,在使用的时候,可以通过Value方法读取ctx.Value(key)。 记住,使用WithValue传值,一般是必须的值,不要什么值都传递。
Context 使用原则
Go Web
Go 在编写 web 应用方面非常得力。
net/http
Go语言里面提供了一个完善的 net/http 包,通过http包可以很方便的搭建起来一个可以运行的Web服务。 同时使用这个包能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。
Go不需要 nginx、apache服务器,因为他直接就监听tcp端口了,做了nginx做的事情。
Go 通过简单的几行代码就已经运行起来一个Web服务了,而且这个Web服务内部有支持高并发的特性。
Go 实现Web服务的工作模式的流程图
Go 底层其实这样处理的,初始化一个server对象,然后调用了
net.Listen("tcp", addr)
,也就是底层用TCP协议搭建了一个服务,然后监控我们设置的端口。首先通过Listener接收请求,其次创建一个Conn,最后单独开了一个 goroutine,把这个请求的数据当做参数传递给这个 Conn 去服务,用户的每一次请求都是在一个新的 goroutine 去服务,相互不影响。然后根据路由设置,传递给不同的Handler。
详细的整个流程
Go 的 http 包详解
Go的http有两个核心功能:Conn,ServeMux
与我们一般编写的http服务器不同, Go为了实现高并发和高性能, 使用了goroutines来处理Conn的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是Go高效的保证。
Go在等待客户端请求里面是这样写的
这里我们可以看到 客户端的每次请求都会创建一个Conn,这个Conn里面保存了该次请求的信息,然后再传递到对应的handler,该handler中便可以读取到相应的header信息,这样保证了每个请求的独立性。
路由器
HttpRouter 路由
Restful 结构
httprouter 是一个高性能、可扩展的HTTP路由。 httprouter.New() 生成了一个
*Router
路由指针,然后使用GET方法注册一个适配/路径的Index函数。 最后*Router
作为参数传给ListenAndServe函数启动HTTP服务即可。使用 HttpRouter 的例子,这里使用了第三方库
github.com/julienschmidt/httprouter
httprouter.Router类型类似于http包中的ServeMux,它实现了http.Handler接口,所以它是一个http.Handler。它可以将请求分配给注册好的handler。 httprouter 中的 Handle 类似于 http.HandlerFunc ,只不过它支持第三个参数 Params,httprouter.Params。 Param类型是 key/value 型的结构,比如
这里的:name就是key,当请求的URL路径为/hello/abc,则key对应的value为abc。也就是说保存了一个Param实例
Params是Param的slice。也就是说,每个分组捕获到的key/value都存放在这个slice中。 ByName(str)方法可以根据Param的Key检索已经保存在slice中的Param的Value。
由于Params是slice结构,除了ByName()方法可以检索key/value,通过slice的方法也可以直接检索
CHI 框架
chi 是一个非常轻量级的框架,跟 net/http 兼容。
chi 的 Handler 跟 net/http 的 Handler 是兼容的。参数也是一样的,
http.ResponseWriter
和*http.Request
。使用 mount ,将路由分开
使用 chi 的中间件,chi 的中间件,就是 net/http 的中间件。 接受一个 http.Handler 作为参数,返回一个 http.Handler 。 通过中间件,可以写一些共同的前处理。
database/sql
使用数据库时,除了database/sql包本身,还需要引入想使用的特定数据库驱动。
一般使用_别名来匿名导入驱动,驱动的导出名字不会出现在当前作用域中。导入时,驱动的初始化函数会调用sql.Register将自己注册在database/sql包的全局变量sql.drivers中,以便以后通过sql.Open访问。
比如,导入PostgreSQL的驱动
加载驱动包后,需要使用sql.Open()来创建sql.DB。
执行sql.Open()并未实际建立起到数据库的连接,也不会验证驱动参数。第一个实际的连接会惰性求值,延迟到第一次需要时建立。 sql.DB对象是为了长连接而设计的,不要频繁Open()和Close()数据库。 而应该为每个待访问的数据库创建一个sql.DB实例,并在用完前一直保留它。需要时可将其作为参数传递,或注册为全局对象。
有了sql.DB实例之后就可以开始执行查询语句了。 Go将数据库操作分为两类:Query与Exec。两者的区别在于前者会返回结果,而后者不会。 Query表示查询,它会从数据库获取查询结果(一系列行,可能为空)。 Exec表示执行语句,它不会返回行。
如果一个查询每次最多返回一行,那么可以用快捷的单行查询来替代冗长的标准查询。
需要注意的是,对于单行查询,Go将没有结果的情况视为错误。sql包中定义了一个特殊的错误常量ErrNoRows,当结果为空时,QueryRow().Scan()会返回它。
Query和Exec返回的结果不同,两者的签名分别是
Exec不需要返回数据集,返回的结果是Result,Result接口允许获取执行结果的元数据
比如
通过db.Begin()来开启一个事务,Begin方法会返回一个事务对象Tx。 在结果变量Tx上调用Commit()或者Rollback()方法会提交或回滚变更,并关闭事务。
SetMaxOpenConns用于设置最大打开的连接数,默认值为0表示不限制。 SetMaxIdleConns用于设置闲置的连接数。设置最大的连接数,可以避免并发太高导致连接mysql出现too many connections的错误。设置闲置的连接数则当开启的一个连接使用完成后可以放在池里等候下一次使用。
日志框架 Zap
github.com/uber-go/zap 是非常快的、结构化的,分日志级别的Go日志库。 将 debug 级别的log写入。
使用Lumberjack进行日志切割归档
Lumberjack Logger采用以下属性作为输入
encoding/json
将一个对象编码成JSON数据,接受一个interface{}对象,返回[]byte和error。 Marshal函数将会递归遍历整个对象,依次按成员类型对这个对象进行编码。
类型转换规则如下
比如
将JSON数据解码,接受一个byte切片
比如
结构体必须是大写字母开头的成员才会被JSON处理到,小写字母开头的成员不会有影响。
Mashal时,结构体的成员变量名将会直接作为JSON Object的key打包成JSON Unmashal时,会自动匹配对应的变量名进行赋值,大小写不敏感。 Unmarshal时,如果JSON中有多余的字段,会被直接抛弃掉;如果JSON缺少某个字段,则直接忽略不对结构体中变量赋值,不会报错。
如果希望手动配置结构体的成员和JSON字段的对应关系,可以在定义结构体的时候给成员打标签。
现在有这么一种场景,结构体中的其中一个字段的格式是未知的:
使用json.RawMessage的话,Args字段在Unmarshal时不会被解析,直接将字节数据赋值给Args。我们可以能先解包第一层的JSON数据,然后根据Cmd的值,再确定Args的具体类型进行第二次Unmarshal。
这里要注意的是,一定要使用指针类型
*json.RawMessage
,否则在Args会被认为是[]byte类型,在打包时会被打包成base64编码的字符串。HttpClient
可以使用 net/http 包中的 client,发送 http 请求。
go-redis
gofmt
gofmt 可以将 go 的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。
会格式化该源文件的代码然后将格式化后的代码覆盖原始内容
格式化并重写所有 Go 源文件
格式化并重写project目录下所有 Go 源文件
go vet
vet 是一个优雅的工具,每个 Go 开发者都要知道并会使用它。 它可以在编译阶段和运行阶段发现bug。 vet 是 Go tool 套件的一部分,它和 go 编译器一起发布,这意味着它不需要额外的依赖,可以很方便地通过以下的命令调用。
比如下面的程序,在Go里面编译器没有任何输出。
这是vet发挥作用的时候了。
effective_go
effective_go
中文版