目录
【安装Go】
Go语言中的环境变量的含义
入门代码 hello.go
【Go依赖管理】
Go Modules
go get
go.mod 和 go.sum
GoLand 不能识别依赖包的问题
扩展版本变更
【Go程序的执行顺序】
Go语言的作用域和包
Go包的初始化顺序
init函数的用途
Go语言的特点:
var a int16 = 5
var b int32 = 8
var c int64
c = int64(a) + int64(b)
fmt.Printf("%d\n", c) //13
Go语言中组合的理解:
// $GOROOT/src/io/io.go
type ReadWriter interface {
Reader
Writer
}
//再比如
type User struct{
name string
Mutex //在User结构体中嵌入类型 Mutex
}
Go语言的并发:
Go原生支持并发,采用了用户层轻量级线程,Go 将之称为 goroutine。Go 运行时默认为每个 goroutine 分配的栈空间仅 2KB,一个 Go 程序中可以创建成千上万个并发的 goroutine。并且所有的 Go 代码都在 goroutine 中执行。可以通过语言内置的 channel 传递消息或实现同步,并通过 select 实现多路 channel 的并发控制。
本机系统 MacOS 10.14,安装 go1.20.4,https://dl.google.com/go/go1.20.4.darwin-amd64.pkg
或者通过brew安装:brew install go,其它系统访问 The Go Programming Language 自行下载安装。
安装完成后配置环境变量:vim ~/.bash_profile,新增如下内容:
export GOROOT=/usr/local/go #go程序所在目录
export GOPATH=/Users/yourname/go #go代码目录
export GOBIN=$GOPATH/bin #go生成的二进制文件的目录
## 记得 source ~/.bash_profile 立即生效。
然后使用 go version 命令 和 go env 命令查看版本号和环境配置信息:
- 对于国内的Go开发者来说,⽬前有3个常⽤的GOPROXY可供选择,分别是官⽅、七⽜和阿⾥云。 官⽅的GOPROXY,国内用户可能访问不到,所以更推荐使⽤七⽜的goproxy.cn。
- GOPRIVATE示例:go env -w GOPRIVATE=*.mywebsite.com #设置不走proxy的私有仓库,多个可以用逗号分隔
GOPATH时代的项目目录结构一般如下设置:
bin 存放编译后可执行的文件。
pkg 存放编译后的应用包。
src 存放应用源代码。
package main
import "fmt"
func main() {
fmt.Println("Hello Golang")
}
go run hello.go 直接执行,不生成任何文件
go build hello.go 在当前目录下生成可执行二进制文件
go install hello.go 会把编译好的结果移动到 bin目录中
go fmt hello.go 格式化代码
Go Modules 的管理命令为go mod,有很多⼦命令,可以通过go help mod来获取所有的命令。
download:下载go.mod⽂件中记录的所有依赖包。
edit:编辑go.mod⽂件。
graph:查看现有的依赖结构。
init:把当前⽬录初始化为⼀个新模块。
tidy:添加丢失的模块,并移除⽆⽤的模块。默认情况下,Go不会移除go.mod⽂件中的⽆⽤依赖。当依赖包不再使⽤了,可以使⽤go mod tidy命令来清除它。
vendor:将所有依赖包存到当前⽬录下的vendor⽬录下。
verify:检查当前模块的依赖是否已经存储在本地下载的源代码缓存中,以及检查下载后是否有修改。
why:查看为什么需要依赖某模块。
可以通过设置环境变量 GO111MODULE 的值在两种构建模式间切换。GO111MODULE有3个值。
go env -w GO111MODULE=off/on #使用 Go modules 时请设置为on
# auto:在Go1.14版本中是默认值,在$GOPATH/src下,且没有包含go.mod时则关闭Go Modules,其他情况下都开启Go Modules。
# on:启⽤Go Modules,Go1.14版本推荐打开,未来版本会设为默认值。
# off:关闭Go Modules,不推荐。
可以通过 go get 命令将本地缺失的第三方依赖包下载到本地,比如:
go get github.com/sirupsen/logrus
#备注:go get 下载的包只是当前的最新版本
#可以使⽤go get -u 更新package到latest版本,也可以使⽤go get -u=patch只更新⼩版本,例如从 v1.2.4到v1.2.5。
go get的参数:
一个 Go Module 的顶层目录下会放置一个 go.mod 文件和 go.sum文件,每个 go.mod 文件会定义唯一一个 module。一个go.mod文件示例:
module test-module
go 1.20
require (
github.com/google/uuid v1.3.0
github.com/sirupsen/logrus v1.8.2
)
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
go mod子命令示例:
go mod edit -fmt # go.mod 格式化
go mod edit -require=golang.org/x/[email protected] # 添加⼀个依赖
go mod edit -droprequire=golang.org/x/text # require的反向操作,移除⼀个依赖
go mod edit -replace=github.com/gin-gonic/gin=/home/colin/gin # 替换模块版本
go mod edit -dropreplace=github.com/gin-gonic/gin # replace的反向操作
go mod edit -exclude=golang.org/x/[email protected] # 排除⼀个特定的模块版本
go mod edit -dropexclude=golang.org/x/[email protected] # exclude的反向操作
基于当前项目创建一个 Go Module的步骤:
go mod init test-module #创建 go.mod 文件
go mod tidy #自动更新当前 module 的依赖
go mod vendor #在当前项目下生成vendor目录,也会创建vendor/modules.txt⽂件,来记录包和模块的版本信息
go build #执行新 module 的构建,会生成一个可执行文件 test-module
//新建main.go
package main
import (
"github.com/sirupsen/logrus"
)
func main() {
logrus.Println("hello, go module")
}
go.sum⽂件⽤来记录每个依赖包的hash值,在构建时,如果本地的依赖包hash值与go.sum⽂件中记录的不⼀致,则会拒绝构建。
go mod tidy 会自动更新当前 module 的依赖并生成新的 go.sum 文件,并且会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。推荐把 go.mod 和 go.sum 两个文件也提交到代码版本中。
需要修改GoLand的Go Module,增加:GOPROXY=https://goproxy.cn,direct 就可以了。
查看扩展的发布版本有哪些:
demo01 $ go list -m -mod=mod -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 ... v1.8.2 v1.9.0
对扩展降级:
demo01 $ go mod edit -require=github.com/sirupsen/[email protected]
demo01 $ go mod tidy
go: downloading github.com/sirupsen/logrus v1.8.2
查看当前 module 的所有依赖: go list -m all
在Java或者PHP中,通过public、private 这些修饰符修饰一个类的作用域,而在Go语言中,并没有这样的作用域修饰符,它是通过首字母是否大写来区分的;代码的package可以和所在的目录不一致,但是同一目录里的Go代码的package要保持一致。Go语言中,所有的定义,比如函数、变量、结构体等如果首字母是大写,那么就可以被其他包使用,例如:fmt.Println();反之,如果首字母是小写的,就只能在同一个包内使用。
可执行程序的 main 包必须定义 main 函数(无参数无返回值),否则 Go 编译器会报错。在启动了多个 Goroutine 中,main.main 函数将在 Go 应用的主 Goroutine 中执行。除了 main 包外,其他包也可以拥有自己的名为 main 的函数或方法。但按照 Go 的可见性规则(小写字母卡头的标识符为非导出标识符),非 main 包中自定义的 main 函数仅限于包内使用。
需要注意的是,main包的main函数虽然是用户层逻辑的入口函数,但它却不一定是用户层第一个被执行的函数,因为还有一个Go 包的初始化函数:init 函数(也是无参数无返回值),类似于构造函数的意思。在 Go 程序中不能手工显式地调用 init,否则会编译错误。另外,Go包中可以拥有多个init函数,Go 会按照一定的次序,逐一顺序地调用这个包的 init 函数。只有当一个 init 函数执行完毕后,才会去执行下一个 init 函数。
Go 在进行包初始化的过程中,会采用“深度优先”的原则,递归初始化各个包的依赖包。在包内按照“常量 -> 变量 -> init 函数 -> main函数” 的顺序进行初始化。如下图所示:
例如下面的代码:
/* order/orders.go */
package order
import "fmt"
func init() {
fmt.Println("order/orders.init()")
}
/* user/user.go */
package user
import (
"fmt"
_ "test-init/order"
)
var username = "zhangsan"
const AGE = 18
func init() {
fmt.Println("user/user.init()")
}
func Hello() {
fmt.Println("user/user.hello()")
fmt.Println(username)
fmt.Println(AGE)
}
/* goods/goodsInfo.go */
package goods
import (
"fmt"
_ "test-init/order"
)
func init() {
fmt.Println("goods/goodsInfo.init()")
}
/* main.go */
package main
import (
"fmt"
_ "test-init/goods"
"test-init/user"
)
// 包初始化逻辑,可以有多个init()方法
func init() {
fmt.Println("main.init1()")
fmt.Println(user.AGE)
}
func init() {
fmt.Println("main.init2()")
user.Hello()
}
func main() {
fmt.Println("main.main()")
}
注意看上面,user和goods都依赖了order包,但是却只输出了一次order的信息。原因在于:每个 init 函数在整个 Go 程序生命周期内仅会被执行一次。
(1) 重置包级变量值:负责对包内部以及暴露到外部的包级数据(主要是包级变量)的初始状态进行检查。
(2) 实现对包级变量的复杂初始化,例如 标准库 http 包中的示例:
var (
http2VerboseLogs bool // 初始化时默认值为false
http2logFrameWrites bool // 初始化时默认值为false
http2logFrameReads bool // 初始化时默认值为false
http2inTests bool // 初始化时默认值为false
)
(3) 在 init 函数中实现“注册模式”,有效降低了 Go 包对外的直接暴露。
比如上面示例中,user/user.go 中使用空导入的方式导入了 order包,这样一来,就可以有效降低order包对外的直接暴露,从而避免了外部通过包级变量对包状态的改动。其实更像是设计模式中的工厂模式。
比如标准库 image 包获取各种格式图片的宽和高:
import (
"fmt"
"image"
"os"
)
import (
_ "image/gif" // 以空导入方式注入gif图片格式驱动
_ "image/jpeg" // 以空导入方式注入jpeg图片格式驱动
_ "image/png" // 以空导入方式注入png图片格式驱动
)
func main() {
// 支持png, jpeg, gif
width, height, err := imageSize(os.Args[1]) // 获取传入的图片文件的宽与高
if err != nil {
fmt.Println("get image size error:", err)
return
}
fmt.Printf("image size: [%d, %d]\n", width, height)
}
func imageSize(imageFile string) (int, int, error) {
f, _ := os.Open(imageFile) // 打开图文文件
defer f.Close()
img, _, err := image.Decode(f) // 对文件进行解码,得到图片实例
if err != nil {
return 0, 0, err
}
b := img.Bounds() // 返回图片区域
return b.Max.X, b.Max.Y, nil
}
/*image/png、image/jpeg 和 image/gif 包都在各自的 init 函数中,将自己“注册”到 image 的支持格式列表中*/
// $GOROOT/src/image/png/reader.go
func init() {
image.RegisterFormat("png", pngHeader, Decode, DecodeConfig)
}
// $GOROOT/src/image/jpeg/reader.go
func init() {
image.RegisterFormat("jpeg", "\xff\xd8", Decode, DecodeConfig)
}
// $GOROOT/src/image/gif/reader.go
func init() {
image.RegisterFormat("gif", "GIF8?a", Decode, DecodeConfig)
}
综上所述,init 函数十分适合做一些包级数据初始化工作以及包级数据初始状态的检查的工作,同时需要注意主 Goroutine 是否要等待其他子 Goroutine 做完清理收尾工作退出后再行退出。
参考源代码:https://gitee.com/rxbook/go-demo-2023/tree/master/module