[代码重构](master): 更新了Go的内容

更新了变量语法学习
master
土豆兄弟 2 years ago
parent c6d8f16080
commit 9b99c80f34

@ -110,7 +110,7 @@
- gofmt它统一了 Go 语言的代码风格在其他语言开发者还在为代码风格争论不休的时候Go 开发者可以更加专注于领域业务中。
- 在提供丰富的工具链的同时Go 在标准库中提供了官方的词法分析器、语法解析器和类型检查器相关包,开发者可以基于这些包快速构建并扩展 Go 工具链。
## 2. 基础
## 2. 环境
### 2.1 配好环境选择一种最适合你的Go安装方法
@ -267,12 +267,379 @@ hello, world
- 我们也可以使用 go mod tidy 命令,让 Go 工具自动添加第三方包
- go.mod 已经记录了 hellomodule 直接依赖的包的信息。不仅如此hellomodule 目录下还多了一个名为 go.sum 的文件,这个文件记录了 hellomodule 的直接依赖和间接依赖包的相关版本的 hash 值,用来校验本地包的真实性。
### 2.3 标准先行Go项目的布局标准是什么
- Go 语言“创世项目”结构是怎样的?
- 用loccount 工具对 Go 语言发布的第一个Go 1.0 版本分析看看:
- ![Go项目的布局标准](pic/Go项目的布局标准.png)
- 现在的 Go 项目的典型结构布局是怎样的?
- 可执行程序项目是以构建可执行程序为目的的项目Go 社区针对这类 Go 项目所形成的典型结构布局是这样的:
```txt
$tree -F exe-layout
exe-layout
├── cmd/
│ ├── app1/
│ │ └── main.go
│ └── app2/
│ └── main.go
├── go.mod
├── go.sum
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
├── pkg2/
│ └── pkg2.go
└── vendor/
```
- 先来看 cmd 目录。cmd 目录就是存放项目要编译构建的可执行文件对应的 main 包的源文件。如果你的项目中有多个可执行文件需要构建,每个可执行
文件的 main 包单独放在一个子目录中,比如图中的 app1、app2cmd 目录下的各 app 的 main 包将整个项目的依赖连接在一起。
- 而且通常来说main 包应该很简洁。我们在 main 包中会做一些命令行参数解析、资源初始化、日志设施初始化、数据库连接初始化等工作,之后就会将程序的执行权限交给更高
级的执行控制对象。另外,也有一些 Go 项目将 cmd 这个名字改为 app 或其他名字,但它的功能其实并没有变。
- 接着我们来看 pkgN 目录,这是一个存放项目自身要使用、同样也是可执行文件对应 main 包所要依赖的库文件,同时这些目录下的包还可以被外部项目引用。
- 然后是 go.mod 和 go.sum它们是 Go 语言包依赖管理使用的配置文件。我们前面说过Go 1.11 版本引入了 Go Module 构建机制,这里我建议你所有新项目都基于 Go
Module 来进行包依赖管理,因为这是目前 Go 官方推荐的标准构建模式
- 对于还没有使用 Go Module 进行包依赖管理的遗留项目,比如之前采用 dep、glide 等作为包依赖管理工具的,建议尽快迁移到 Go Module 模式。Go 命令支持直接将 dep 的
Gopkg.toml/Gopkg.lock 或 glide 的 glide.yaml/glide.lock 转换为 go.mod。
- 最后我们再来看看 vendor 目录。vendor 是 Go 1.5 版本引入的用于在项目本地缓存特定版本依赖包的机制,在 Go Modules 机制引入前,基于 vendor 可以实现可重现构建,保
证基于同一源码构建出的可执行程序是等价的。
- 不过呢,我们这里将 vendor 目录视为一个可选目录。原因在于Go Module 本身就支持可再现构建,而无需使用 vendor。 当然 Go Module 机制也保留了 vendor 目录(通过
go mod vendor 可以生成 vendor 下的依赖包,通过 go build -mod=vendor 可以实现基于 vendor 的构建)。一般我们仅保留项目根目录下的 vendor 目录,
否则会造成不必要的依赖选择的复杂性。
- 当然了有些开发者喜欢借助一些第三方的构建工具辅助构建比如make、bazel 等。你可以将这类外部辅助构建工具涉及的诸多脚本文件(比如 Makefile放置在项目的顶层
目录下,就像 Go 创世项目中的 all.bash 那样。
- 当然如果你非要在一个代码仓库中存放多个 module那么新版 Go 命令也提供了很好的支持。比如下面代码仓库 multi-modules 下面有三个 modulemainmodule、
module1 和 module2
```txt
$tree multi-modules
multi-modules
├── go.mod // mainmodule
├── module1
│ └── go.mod // module1
└── module2
└── go.mod // module2
```
- 我们可以通过 git tag 名字来区分不同 module 的版本。其中 vX.Y.Z 形式的 tag 名字用于代码仓库下的 mainmodule而 module1/vX.Y.Z 形式的 tag 名字用于指示 module1 的
版本同理module2/vX.Y.Z 形式的 tag 名字用于指示 module2 版本。
- 如果 Go 可执行程序项目有一个且只有一个可执行程序要构建,那就比较好办了,我们可以将上面项目布局进行简化:
```txt
$tree -F -L 1 single-exe-layout
single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/
```
- 你可以看到,我们删除了 cmd 目录,将唯一的可执行程序的 main 包就放置在项目根目录下,而其他布局元素的功用不变。
- 好了到这里,我们已经了解了 Go 可执行程序项目的典型布局,现在我们再来看看 **Go 库项目**的典型结构布局是怎样的。
- Go 库项目仅对外暴露 Go 包,这类项目的典型布局形式是这样的:
```txt
$tree -F lib-layout
lib-layout
├── go.mod
├── internal/
│ ├── pkga/
│ │ └── pkg_a.go
│ └── pkgb/
│ └── pkg_b.go
├── pkg1/
│ └── pkg1.go
└──pkg2/
└──pkg2.go
```
- Go 库项目的初衷是为了对外部(开源或组织内部公开)暴露 API对于仅限项目内部使用而不想暴露到外部的包可以放在项目顶层的 internal 目录下面。当然 internal 也可以有
多个并存在于项目结构中的任一目录层级中,关键是项目结构设计人员要明确各级 internal 包的应用层次和范围。
- 对于有一个且仅有一个包的 Go 库项目来说,我们也可以将上面的布局做进一步简化,简化的布局如下所示:
```txt
$tree -L 1 -F single-pkg-lib-layout
single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└──internal/
```
- 简化后,我们将这唯一包的所有源文件放置在项目的顶层目录下(比如上面的 feature1.go和 feature2.go其他布局元素位置和功用不变。
### 2.4 构建模式Go是怎么解决包依赖管理问题的
- Go 构建模式是怎么演化的?
- Go 程序由 Go 包组合而成的Go 程序的构建过程就是确定包版本、编译包以及将编译后得到的目标文件链接在一起的过程。
- Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH、1.5 版本的 Vendor 机制,以及现在的 Go Module。
- 首先我们来看 GOPATH
- Go 编译器可以在本地 GOPATH 环境变量配置的路径下,搜寻 Go 程序依赖的第三方包。如果存在,就使用这个本地包进行编译;如果不存在,就会报编译错误。
- GOPATH 构建模式下编写的代码
```go
package main
import "github.com/sirupsen/logrus"
func main() {
logrus.Println("hello, gopath mode")
}
```
- 你可以看到,这段代码依赖了第三方包 logruslogrus 是 Go 社区使用最为广泛的第三方 log 包)。
- Go 编译器在 GOPATH 构建模式下,究竟怎么在 GOPATH 配置的路径下搜寻第三方依赖包呢?
- 可以通过 go get 命令将本地缺失的第三方依赖包下载到本地
```shell
$go get github.com/sirupsen/logrus
```
- 这里的 go get 命令,不仅能将 logrus 包下载到 GOPATH 环境变量配置的目录下,它还会检查 logrus 的依赖包在本地是否存在如果不存在go get 也会一并将它们下载到本地。
- 也就是说,在 GOPATH 构建模式下Go 编译器实质上并没有关注 Go 项目所依赖的第三方包的版本。但 Go 开发者希望自己的 Go 项目所依赖的第三方包版本能受到自己的控
制,而不是随意变化。于是 Go 核心开发团队引入了 Vendor 机制试图解决上面的问题。
- 要想开启 vendor 机制,你的 Go 项目必须位于 GOPATH 环境变量配置的某个路径的 src 目录下面。如果不满足这一路径要求,那么 Go 编译器是不会理会
Go 项目目录下的 vendor 目录的。
- 就在 Go 社区为包依赖管理焦虑并抱怨没有官方工具的时候Go 核心团队基于社区实践的经验和教训,推出了 Go 官方的解决方案Go Module。
- 创建你的第一个 Go Module
- 在 Go Module 模式下,通常一个代码仓库对应一个 Go Module。一个 Go Module 的顶层目录下会放置一个 go.mod 文件,每个 go.mod 文件会定义唯一一个 module也就是
说 Go Module 与 go.mod 是一一对应的。
- go.mod 文件所在的顶层目录也被称为 module 的根目录module 根目录以及它子目录下的所有 Go 包均归属于这个 Go Module这个 module 也被称为 main module
- **创建一个 Go Module**
- 第一步,通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module
- 第二步,通过 go mod tidy 命令自动更新当前 module 的依赖信息;
- 第三步,执行 go build执行新 module 的构建。
-
## 3. 语法
### 3.1 **变量声明** :静态语言有别于动态语言的重要特征
- 在编程语言中,为了方便操作内存特定位置的数据,我们**用一个特定的名字与位于特定位置的内存块绑定在一起**,这个名字被称为**变量**。
- 但这并不代表我们可以通过变量随意引用或修改内存,变量所绑定的内存区域是要有**一个明确的边界的**。
- 动态语言(比如 Python、Ruby 等)的解释器可以在运行时通过对变量赋值的分析,**自动确定变量的边界**。并且在动态语言中,一个变量可以在运行时被赋予大小不同的边界。
- Go 语言,它沿袭了静态语言的这一要求:**使用变量之前需要先进行变量声明**
- Go 语言的变量声明方法
- 通用的变量声明方法
- ![通用的变量声明方法](pic/通用的变量声明方法.png)
- 这个变量声明分为四个部分:
- var 是修饰变量声明的关键字;
- a 为变量名;
- int 为该变量的类型;
- 10 是变量的初值。
- 其实 Go 语言的变量声明形式与其他主流静态语言有一个显著的差异,那就是它将**变量名放在了类型的前面**
- 如果你没有**显式**为变量赋予初值Go 编译器会为变量**赋予这个类型的零值**
- var a int // a的初值为int类型的零值0
- Go 语言的每种原生类型都有它的默认值,这个默认值就是这个类型的零值。
- Go 规范定义的内置原生类型的默认值(即零值)
- ![内置原生类型的默认值](pic/内置原生类型的默认值.png)
- 像数组、结构体这样复合类型变量的零值就是它们**组成元素都为零值时的结果**
- 除了单独声明每个变量外Go 语言还提供了变量声明块block的语法形式可以**用一个 var 关键字将多个变量声明放在一起**,像下面代码这样:
```go
var (
a int = 128
b int8 = 6
s string = "hello"
c rune = 'A'
t bool = true
)
```
- 而且Go 语言还支持在**一行变量声明中同时声明多个变量**
```go
var a, b, c int = 5, 6, 7
```
- 这样的多变量声明同样也可以用在变量声明块中,像下面这样:
```go
var (
a, b, c int = 5, 6, 7
c, d, e rune = 'C', 'D', 'E'
)
```
- 当然了,虽然我们现在写的多变量声明都是在声明同一类型的变量,**但是它也适用于声明不同类型的变量**
- 为了给开发者带来更好的使用体验Go 语言还提供了两种变量声明的 **“语法糖”**
- 省略类型信息的声明:
- 在通用的变量声明的基础上Go 编译器允许我们省略变量声明中的类型信息,它的标准范式是 **“var varName = initExpression”** 省略了类型信息的变量声明:
```go
var b = 13
```
- Go 编译器**会根据右侧变量初值自动推导出变量的类型**,并给这个变量赋予初值所对应的默认类型。比如,整型值的默认类型 int浮点值的默认类型为 float64复数
值的默认类型为 complex128。其他类型值的默认类型就更好分辨了在 Go 语言中仅有唯一与之对应的类型,比如布尔值的默认类型只能是 bool字符值默认类型只能是
true字符串值的默认类型只能是 string 等。
- 如果我们不接受默认类型,而是要显式地为变量指定类型,除了通用的声明形式,我们还可以通过**显式类型转型**达到我们的目的:
```go
var b = int32(13)
```
- 显然这种省略类型信息声明的“语法糖”仅**适用于在变量声明的同时显式赋予变量初值的情况**
- 结合多变量声明,我们可以使用这种变量声明“语法糖”**声明多个不同类型的变量**
```go
var a, b, c = 12, 'A', "hello"
```
- 我们声明了三个变量 a、b 和 c但它们分别具有不同的类型分别为 int、rune 和 string。
- 是否还有更简化的变量声明形式呢?答案是有的。下面我们就来看看短变量声明。
- 短变量声明:
- 使用短变量声明时,我们甚至可以省去 var 关键字以及类型信息,它的标准范式是 **“varName := initExpression”**
```go
a := 12
b := 'A'
c := "hello"
```
- 而且,短变量声明也支持一次声明多个变量,而且形式更为简洁,是这个样子的:
```go
a, b, c := 12, 'A', "hello"
```
- Go 语言的两类变量
- Go 语言的变量可以分为两类:
- 一类称为包级变量 (package varible),也就是在包级别可见的变量。
- 如果是导出变量(大写字母开头),那么这个包级变量也可以被视为全局变量;
- 另一类则是局部变量 (local varible),也就是 Go 函数或方法体内声明的变量,仅在函数或方法体内可见。
- 而我们声明的所有变量都逃不开这两种。
- 包级变量的声明形式
- 先下个结论:**包级变量只能使用带有 var 关键字的变量声明形式**,不能使用短变量声明形式,但在形式细节上可以有一定灵活度。
- 我们可以从 **“变量声明时是否延迟初始化”** 这个角度,对包级变量的声明形式进行一次分类。
- **第一类:声明并同时显式初始化。**
```go
// $GOROOT/src/io/io.go
var ErrShortWrite = errors.New("short write")
var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
```
- 这个代码块里声明的变量都是 io 包的包级变量。在 Go 标准库中,对于变量声明的同时进行显式初始化的这类包级变量,实践中多使用这种省略类型信息的“语法糖”格式:
```go
var varName = initExpression
```
- Go 编译器会自动根据等号右侧 InitExpression 结果值的类型,来确定左侧声明的变量的类型,这个类型会是结果值对应类型的默认类型。
- 当然,如果我们不接受默认类型,而是要显式地为包级变量指定类型,那么我们有两种方式,我这里给出了**两种包级变量的声明形式**的对比示例。
```go
//第一种:
plain
var a = 13 // 使用默认类型
var b int32 = 17 // 显式指定类型
var f float32 = 3.14 // 显式指定类型
//第二种:
var a = 13 // 使用默认类型
var b = int32(17) // 显式指定类型
var f = float32(3.14) // 显式指定类型
```
- 虽然这两种方式都是可以使用的但从声明一致性的角度出发Go 更推荐我们**使用后者**,这样能统一接受默认类型和显式指定类型这两种声明形式,尤其是在将这些变量放在一个
var 块中声明时,你会更明显地看到这一点。
- 所以我们更青睐下面这样的形式:
```go
var (
a = 13
b = int32(17)
f = float32(3.14)
)
```
- **第二类:声明但延迟初始化。**
- 对于声明时并不立即显式初始化的包级变量,我们可以使用下面这种通用变量声明形式:
```go
var a int32
var f float64
```
- 我们知道虽然没有显式初始化Go 语言也会让这些变量拥有初始的“零值”。如果是自定义的类型,我也建议你**尽量保证它的零值是可用的**。
- 这里还有一个注意事项,就是**声明聚类与就近原则**。
- 正好Go 语言提供了**变量声明块**用来把多个的变量声明放在一起,并且在语法上也不会限制放置在 var 块中的声明类型,那我们就应该学会充分利用 var 变量声明块,让我们变量
声明更规整,更具可读性,现在我们就来试试看。
- 其实,我们**可以将延迟初始化的变量声明放在一个 var 声明块** (比如上面的第一个 var 声明块),然后将声明且**显式初始化的变量放在另一个 var 块中**(比如上面的第二个 var 声明
块),这里我称这种方式为“声明聚类”,声明聚类可以提升代码可读性。
- 我们是否应该将包级变量的声明全部集中放在源文件头部呢?答案不能一概而论。
- 使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:**就近原则**。
- 尽可能在靠近第一次使用变量的位置声明这个变量,就近原则实际上也是对变量的作用域最小化的一种实现手段。
```go
// $GOROOT/src/net/http/request.go
var ErrNoCookie = errors.New("http: named cookie not present")
func (r *Request) Cookie(name string) (*Cookie, error) {
for _, c := range readCookies(r.Header, name) {
return c, nil
}r
eturn nil, ErrNoCookie
}
```
- 在这个代码块里ErrNoCookie 这个变量在整个包中仅仅被用在了 Cookie 方法中,因此它被声明在紧邻 Cookie 方法定义的地方。当然了,如果一个包级变量在**包内部被多处**使
用,那么这个变量还是放在**源文件头部**声明比较适合的。
- **局部变量的声明形式**
- 这里我们也从“变量声明的时候是否延迟初始化”这个角度,对本地变量的声明形式进行分类说明。
- 第一类:**对于延迟初始化的局部变量声明,我们采用通用的变量声明形式**
- 省略类型信息的声明和短变量声明这两种“语法糖”变量声明形式都不支持变量的延迟初始化,因此对于这类局部变量,和包级变量一样,我们只能采用通用的变量声明形式:
```go
var err error
```
- 第二类:**对于声明且显式初始化的局部变量,建议使用短变量声明形式**
- 短变量声明形式是局部变量最常用的声明形式,它遍布在 Go 标准库代码中。对于接受默认类型的变量,我们使用下面这种形式:
```go
a := 17
f := 3.14
s := "hello, gopher!"
```
- 对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在":="右侧要做一个显式转型,以保持声明的一致性:
```go
a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")
```
- 这里我们还要注意:**尽量在分支控制时使用短变量声明形式**。
- strings 包的 LastIndexAny 方法为我们很好地诠释了如何**将短变量声明形式与分支控制语句融合在一起使用**
```go
// $GOROOT/src/strings/strings.go
func LastIndexAny(s, chars string) int {
if chars == "" {
// Avoid scanning all of s.
return -1
}
if len(s) > 8 {
// 作者注在if条件控制语句中使用短变量声明形式声明了if代码块中要使用的变量 as和 isASCII
if as, isASCII := makeASCIISet(chars); isASCII {
for i := len(s) - 1; i >= 0; i-- {
if as.contains(s[i]) {
return i
}
}
return -1
}
}
for i := len(s); i > 0; {
// 作者注在for循环控制语句中使用短变量声明形式声明了for代码块中要使用的变量c
r, size := utf8.DecodeLastRuneInString(s[:i])
i -= size
for _, c := range chars {
if r == c {
return i
}
}
}
return -1
}
```
- 而且,短变量声明的这种融合的使用方式也体现出“就近”原则,**让变量的作用域最小化**。
- 但是如果你在声明局部变量时遇到了适合聚类的应用场景,你也应该毫不犹豫地使用 var 声明块来声明多于一个的局部变量,具体写法你可以参考 Go 标准库 net 包中 resolveAddrList 方法:
```go
// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string, hint Addr) (addrList, error) {
... ...
var (
tcp *TCPAddr
udp *UDPAddr
ip *IPAddr
wildcard bool
)
... ...
}
```
- 总结:
- ![变量声明方式总结](pic/变量声明方式总结.png)
### 3.2 代码块与作用域:如何保证变量不会被遮蔽?
@ -282,6 +649,7 @@ hello, world
### 3.3
## 并发

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Loading…
Cancel
Save