GoLang 学习笔记(一)
将要上的项目要用到 GoLang,还不知道什么时候去,先自学一下 Go。
学习路径是参照这个:
第一步是看官方文档的教程。
1. 安装
简单
2. go.mod 文件
注意:
首先最重要知道的是,go 语言中,module 是 package 的集合。
module 是更大的概念,package 是更小的概念
当需要导入一些其他 modules 的 package 的时候,go 是通过自己的 module 跟踪这些依赖。这个 module 由 go.mod 定义,跟踪那些提供 packages 的其他 modules 们。可以说,go.mod 的作用就是跟踪依赖。
2.1 创建 go.mod 文件来开启依赖跟踪
go.mod 使用 goModule 特性,这个是 go 在 1.11 版本才推出的,用于管理包的工具。在此之前都是社区的各种 vendoring tool。
使用如下命令创建:
go mod init <module_path>
或
go mod init
- 该命令会在当前目录下新建一个 go.mod 文件,实际上会在当前文件夹下创建一个 main module。(使用的时候注意当前目录不能有另一个 go.mod 文件)。这样的话,当前目录就变成了 goModule 的根目录。(可以类比一下 git init 后当前目录变为 git 的根目录)。
- 该命令可以加或不加 module_path 参数
2.1.1 添加 module_path 参数
2.1.1.1 module_path 是什么
module_path 是用于识别某个 module 的一个别名。go.mod 文件的第一行就是 module_path,表示当前 module(即 main module)的路径,同时也是当前 module 的名字。module_path 用于识别 module,以及作为寻找在这个 module 里的 package 时候的前缀。
module_path 有多种使用情况:
- go.mod 里第一行的 module_path:
- 这个 module_path 是用于标识当前 module 的名字的,当然同时也是路径。
- 只需要在根目录下使用 init 创建一个 go.mod 文件,子目录下的其他 modules 是不用再 init 的。go 会自动把 module_path 作为前缀,后接子目录的相对路径即可。
- 使用 module_path 引入其他 modules:
- module_path 一般都是由一个 repository root path + subdirectory + major cersion suffix 构成。
- repository 的根目录,就是开发该 module 时所在的 vsc 中的根地址。很多 module 都是直接放在根目录下。所以这种时候,根目录其实就是 module_path,同时也是 module 的名字。比如
github.com/oyishyi/my-go-project
,是一个名字叫做github.com/oyishyi/my-go-project
的 main module 的根目录。
2.1.1.2 module_path 里必须有 xxx.com 吗
如果要发布的话,就需要有这个作为前缀,可以没有,只是没法发布。go 的官方文档教程里使用 example.com, 其实反正也不发布,随便写个啥都行,只是个本地 module 的名字罢了。如果发布了,因为要去找 module 的地址,所以才要在名字上带有真实域名。
2.1.2 不添加 module_path 参数
如果不添加 module_path 参数。go 会尝试从 .go
文件的 import 注释中,vendoring tool 配置文件中,和当前文件夹(如果现在是在 GOPATH 路径下的话。GOPATH 是 go 的一个环境变量,指的是当前项目的工作目录的绝对路径)中去推断 module_path。
(注意:如果是从 vendoring tool 配置文件中推断出的话。很可能会有版本冲突问题。这也是为什么 go 要发明 go module 来代替其他 vendoring tool 的原因。)
3. hello,world
突然想起来掘金禁止写 hello,world,这里写一个会不会被警告(
// 声明一个叫做 main 的 package。解释:package 用于打包函数,由同一目录下的所有文件构成。
package main
// 导入著名的 fmt package。解释:fmt 包包含各种有用函数比如格式化文本,打印等函数。
// 这个包属于标准库,在安装 go 的时候就已经安装了
import "fmt"
// main 函数会在 main package 运行的时候运行。
func main() {
fmt.Println("Hello, World!!!!!!!!!")
}
使用 go run .
运行
4. 寻找和使用第三方包
4.1 寻找
正如 js 的 npm 社区,python 的 pip 社区。go 自然也有自己的第三方包的社区。
这里演示了一下 rsc.io/quote
这个包的寻找和使用。
- 去 pkg.go.dev 搜索想要的包。(这里可以看到包都是以域名开头的,正是前面所说的 module_path + package 后缀)
- 进入包页面后,在 Documentation 里有一个 Index,这里列出了所有能使用的函数。
- 进入包页面后,可以看到包名不带域名前缀了,因为包名确实不带域名前缀,带前缀的是 module_path。搜索的时候之所以能看到前缀,那是因为那时候看到的是 module_path + package 后缀。
- 页面最上方可以看到 module 信息,拿两个包的不同情况作为例子,见下图:
正如之前所说的,package 在 module 内,我们使用的是 package 里的函数。
4.2 使用
- 在代码里 import 要使用的 package,并使用其带有的函数。这时候 ide 会报错,说在现有的 module 里找不到这个包。
- 使用
go mod tidy
,go 会自动给你找到导入到包所对应的 module,下载最新版本,然后修改 go.mod 文件。还会生成一个 go.sum 文件用于检测包是否完整。(关于 module 下载到哪里,如果你到现在为止什么都没设置的话,win10 会默认下载到 <用户文件夹>/go/pkg/mod 里)(默认情况下 tidy 会使用 module 名字当路径去网上找包,但是可以通过 replace 来重定向到本地,从而使用本地包) - 使用
go run .
运行代码
5. 创建一个自己的 module
5.1 写一个自己的 module
go 代码组成了 packages,packages 又组成了 modules。module 会声明运行代码所需要的依赖。包括 go 的版本,以及 require 的其他 modules。
go.mod 文件会跟踪这些依赖,随着代码的编写,会逐渐更新和扩展。
go mod init go-goudoubuyong.com/greetings
创建一个 go.mod 文件。- 写一些代码:
以上代码所做的事是:package greetings import "fmt" // Hello 函数:给某人问号 func Hello(name string) string { msg := fmt.Sprintf("Hi, %v, 我觉得你有问题", name) return msg }
- 声明一个 greetings 的包,用于存放相关函数。
- 写一个 Hello 函数,该函数接受一个参数叫 name,类型是 string。该函数返回一个 string。
(如果想在其他 package 中使用这个 package 的函数,函数名必须以大写字母开头)这种名字叫做 exported name。(如果没有首字母大写,访问是会出错的,没有大写的函数叫做 unexported name,也就是说这不是规范,是语法) - 这里使用了 := ,表示声明变量,同时初始化值。如果换成等号,就要先声明变量。
var msg string msg = fmt.Aprintf("Hi, %v, 我觉得你有问题", name)
- fmt package 里的 Sprintf 函数,可以输出格式化文本。%v 表示对应变量的默认输出格式。
5.2 使用自己刚创建的 module
5.2.1 目录结构
再创建一个文件夹,init 一下,加上上一步创建的文件夹,整个结构是这样的
两个 modules 里各有一个 go.mod 文件。
5.2.2 导入包并使用
在新目录下建立一个 hello.go 文件并写上如下代码:
package main
import (
"fmt"
"go-goudoubuyong.com/greetings"
)
func main() {
msg := greetings.Hello("zouli")
fmt.Println(msg)
}
现在是不够的,因为 go.mod 文件里还没有 require 本地的这个包。
现在使用 go mod tidy
是不行的,因为 tidy 只会在网上寻找这个包,而我们例子里只是给了个假路径。
5.2.3 go.mod 里 require 本地包
go module 是为了 production 而生,所以导入本地 module 是要特殊写法的。一般如果在 production 环境下,往往都会把包上传到一个 repository 上,go 可以直接登录这个 repository 下载包。但是现在我们的本地 module 没有上传是找不到的,这里就要使用 replace
关键字来让 go.mod 能够找到本地的 module。
这背后的思想就是重定向。
把本来要去的我们写的 module 名字的这个假路径换成本地路径。
是的,你没看错,想要访问本地包,没有直接手段。只能先写个假路径,然后重定向到本地路径
- 添加 replace 的命令如下,使用
go mod edit
:# 官方文档这里多打了个 =,真的垃圾文档 $ go mod edit -replace go-goudoubuyong.com/greetings=../greetings
- 运行上面的命令后,可以看到 go.mod 的文件已经改变。
当然,完全也可以直接在 go.mod 里更改。 - 现在完成了重定向,可以运行
go mod tidy
了,tidy 遇到重定向,不再去网上找 module 了。结果如下:
后面这一串数字,是 go 自动生成的假版本号,因为你必须有一个版本号,如果没有就生成一个假的。
5.3 错误处理
- 修改 greetings.go 的代码如下:
go 里的函数可以返回多个值,只是注意必须在开头声明返回值的类型。package greetings import ( "errors" "fmt" ) // Hello 函数:给某人问号 // 返回两个值,一个是 sting,一个是 error func Hello(name string) (string, error) { if name == "" { return "", errors.New("empty name") } msg := fmt.Sprintf("Hi, %v, 我觉得你有问题", name) // 由于必须返回两个值,这里第二个值设为 nil return msg, nil }
nil 的意思比较复杂,自己去查。nil 是很多类型的零值/空值,除了布尔值,数值,字符串。
这里用 nil 表示没有错误。 - 换文件夹,修改 hello.go 的代码如下:
package main import ( "fmt" "log" "go-goudoubuyong.com/greetings" ) func main() { // 设置 log 打印的前缀,这样就能知道是从哪里来的问题了 log.SetPrefix("greetings: ") // 设置 flag 为 0, 这样 log 就不会打印时间,源文件,行号等内容了 log.SetFlags(0) // 使用 Hello 函数,由于参数为空字符串,返回错误 msg, err := greetings.Hello("") if err != nil { // Fatal 会打印错误,同时退出程序 // 相当于 Print + os.Exit(1) log.Fatal(err) } else { fmt.Println(msg) } }
5.4 随机和 init 函数
在 go 中有一个叫做 slice 的类型,类似于 array,是可变的。官方文档说这是 go 最有用的类型之一,我暂时没看出来。
改变 greetings.go:
package greetings
import (
"errors"
"fmt"
"math/rand"
"time"
)
// Hello 函数:给某人问号
func Hello(name string) (string, error) {
if name == "" {
return "", errors.New("empty name")
}
// 使用自定义函数
msg := fmt.Sprintf(randomForamt(), name)
return msg, nil
}
// init 函数是一个特殊的函数
// go 会在程序开始时,全局变量初始化之后,自动运行 init 函数
func init() {
// 用当前时间作为随机种子
rand.Seed(time.Now().UnixNano())
}
// 由于不在外部使用,首字母小写
func randomForamt() string {
// 由 string formats 构成的 slice
// slice 在 string 前面的中括号里隐藏长度,
// 这 告诉 go,隐藏在 slice 背后的 array 的长度是可以改变的
formats := []string{
"Hi, %v, Welcome!",
"Great to see you, %v!",
"Hail, %v, Well met!", // 注意最后一个要加逗号,意※义※不※明的语法
}
// Intn(k) 返回一个 [1,k) 的整数
return formats[rand.Intn(len(formats))]
}
重点:
- init 函数。
- slice 实际是可变的 array。
- Intn 函数
- 用 len() 返回长度
5.5 多值输入,输出,循环
**golang 不支持函数重载。**
如果是在 production 环境,要改变一个函数的签名,如果该函数已经在上个版本发布。那么这时候就不适合改变这个函数。不然用户的一些已有代码就会出问题。因此最好创建一个新的函数。
现在想对 Hello 函数进行改变,让其支持多值输入和输出,这样会改动到函数签名,因此新建一个新的函数 Hellos。
// 返回一个 map(键值分别为各个 name 和对应的 greeting msg) 和 error
func Hellos(names []string) (map[string]string, error) {
// 生成一个 map
msgs := make(map[string]string)
// 遍历 names 这个 slice,使用旧的 Hello 函数生成 greeting msg
for _, name := range names {
msg, err := Hello(name)
if err != nil {
// 这里 nil 表示出错了,自然 msgs 应该为空
return nil, err
}
msgs[name] = msg
}
return msgs, nil
}
重点:
- map 就是键值对,使用
make(map[key-type]value-type)
初始化一个 map。 - range 会在每轮循环时返回 index 和 item。使用
_
来不使用 index。 - 这里的
!= nil
常用,其实在很多其他语言这一段一般是直接省略,见下不同语言代码对比// go if err != nil { // do sth }
# python if err: # do sth
// javascript if (err) { // do sth; }
然后再更改 hello.go 为以下语句
names := []string{"zouli", "zl", "shabi"}
msgs, err := greetings.Hellos(names)
重点:
- 初始化 slice 的语法
5.6 测试
go 自带单元测试。
5.6.1 建立测试文件
在 greeting 文件夹里新建一个 greeting_test.go
文件。(后面加 _test 是固定写法,不能写其他的,不然 go test 无法识别)
写上如下代码:
package greetings
import (
"regexp"
"testing"
)
// 测试函数是有特殊结构的,函数名以 Test 开头就会被识别为测试函数
// 参数是固定的,就是下面这个参数,类型为 testing.T 的指针类型
// 使用该参数 t 的方法可以得到各种关于测试结果的日志和报告
func TestHelloName(t *testing.T) {
name := "zouli"
want := regexp.MustCompile(`\b` + name + `\b`)
// 同一个 package,即使不同文件也不需要导入 Hello
msg, err := Hello("zouli")
if !want.MatchString(msg) || err != nil {
t.Fatalf(`Hello("zouli") = %q, %v, want match for %#q, nil`, msg, err, want)
}
}
func TestHelloEmpty(t *testing.T) {
msg, err := Hello("")
if msg != "" || err == nil {
t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err)
}
}
5.6.2 使用 go test
开始测试
- 直接在目录下使用
go test
即可开启测试,或者使用go test -v
查看详细输出。go test
会自动寻找当前目录中以_test
结尾的 go 文件。并运行其中的以Test
开头的函数。
5.7 编译/安装应用
本节介绍两个方法,go build
和 go install
。前者编译 packages,但不安装;后者编译后,并且安装。
- 在 hello 文件夹里使用
go build
,会编译成一个可执行二进制文件。执行这个二进制文件即可运行。 - 需要进入二进制文件所在目录才能运行。如果想在任何地方都能运行,则需要安装该 package。
- 使用
go list -f "{{.Target}}"
可以查看将要安装在哪个位置。顺便也能看到 go 的包安装位置。 - 将该位置设置为环境变量,即可在任意地方使用安装的包。
$ ser PATH=%PATH%;C:\path\to\your\install\directory
- 或者如果想要把包安装到别的位置,可以更改 go 的环境变量
GOBIN
。$ go env -w GOBIN=C:\path\to\your\bin
- 接下来就可以直接在终端上打包的名字,从而运行包