GoLang 学习笔记(一)

将要上的项目要用到 GoLang,还不知道什么时候去,先自学一下 Go。
学习路径是参照这个

image.png

第一步是看官方文档的教程。

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
  1. 该命令会在当前目录下新建一个 go.mod 文件,实际上会在当前文件夹下创建一个 main module。(使用的时候注意当前目录不能有另一个 go.mod 文件)。这样的话,当前目录就变成了 goModule 的根目录。(可以类比一下 git init 后当前目录变为 git 的根目录)。
  2. 该命令可以加或不加 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 有多种使用情况:

  1. go.mod 里第一行的 module_path:
    • 这个 module_path 是用于标识当前 module 的名字的,当然同时也是路径。
    • 只需要在根目录下使用 init 创建一个 go.mod 文件,子目录下的其他 modules 是不用再 init 的。go 会自动把 module_path 作为前缀,后接子目录的相对路径即可。
  2. 使用 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 这个包的寻找和使用。

  1. pkg.go.dev 搜索想要的包。(这里可以看到包都是以域名开头的,正是前面所说的 module_path + package 后缀)
  2. 进入包页面后,在 Documentation 里有一个 Index,这里列出了所有能使用的函数。
  3. 进入包页面后,可以看到包名不带域名前缀了,因为包名确实不带域名前缀,带前缀的是 module_path。搜索的时候之所以能看到前缀,那是因为那时候看到的是 module_path + package 后缀。
  4. 页面最上方可以看到 module 信息,拿两个包的不同情况作为例子,见下图:
    image.png
    image.png
    正如之前所说的,package 在 module 内,我们使用的是 package 里的函数。

4.2 使用

  1. 在代码里 import 要使用的 package,并使用其带有的函数。这时候 ide 会报错,说在现有的 module 里找不到这个包。
  2. 使用 go mod tidy,go 会自动给你找到导入到包所对应的 module,下载最新版本,然后修改 go.mod 文件。还会生成一个 go.sum 文件用于检测包是否完整。(关于 module 下载到哪里,如果你到现在为止什么都没设置的话,win10 会默认下载到 <用户文件夹>/go/pkg/mod 里)(默认情况下 tidy 会使用 module 名字当路径去网上找包,但是可以通过 replace 来重定向到本地,从而使用本地包)
  3. 使用 go run . 运行代码

5. 创建一个自己的 module

5.1 写一个自己的 module

go 代码组成了 packages,packages 又组成了 modules。module 会声明运行代码所需要的依赖。包括 go 的版本,以及 require 的其他 modules。
go.mod 文件会跟踪这些依赖,随着代码的编写,会逐渐更新和扩展。

  1. go mod init go-goudoubuyong.com/greetings 创建一个 go.mod 文件。
  2. 写一些代码:
    package greetings
    import "fmt"
    
    // Hello 函数:给某人问号
    func Hello(name string) string {
            msg := fmt.Sprintf("Hi, %v, 我觉得你有问题", name)
            return msg
    }
    
    以上代码所做的事是:
    1. 声明一个 greetings 的包,用于存放相关函数。
    2. 写一个 Hello 函数,该函数接受一个参数叫 name,类型是 string。该函数返回一个 string。
      (如果想在其他 package 中使用这个 package 的函数,函数名必须以大写字母开头)这种名字叫做 exported name。(如果没有首字母大写,访问是会出错的,没有大写的函数叫做 unexported name,也就是说这不是规范,是语法)
    3. 这里使用了 := ,表示声明变量,同时初始化值。如果换成等号,就要先声明变量。
      var msg string
      msg = fmt.Aprintf("Hi, %v, 我觉得你有问题", name)
      
    4. fmt package 里的 Sprintf 函数,可以输出格式化文本。%v 表示对应变量的默认输出格式。

5.2 使用自己刚创建的 module

5.2.1 目录结构

再创建一个文件夹,init 一下,加上上一步创建的文件夹,整个结构是这样的
image.png
两个 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 名字的这个假路径换成本地路径。
是的,你没看错,想要访问本地包,没有直接手段。只能先写个假路径,然后重定向到本地路径
  1. 添加 replace 的命令如下,使用 go mod edit
    # 官方文档这里多打了个 =,真的垃圾文档
    $ go mod edit -replace go-goudoubuyong.com/greetings=../greetings
    
  2. 运行上面的命令后,可以看到 go.mod 的文件已经改变。
    image.png
    当然,完全也可以直接在 go.mod 里更改。
  3. 现在完成了重定向,可以运行 go mod tidy 了,tidy 遇到重定向,不再去网上找 module 了。结果如下:
    image.png
    后面这一串数字,是 go 自动生成的假版本号,因为你必须有一个版本号,如果没有就生成一个假的。

5.3 错误处理

  1. 修改 greetings.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
    }
    
    go 里的函数可以返回多个值,只是注意必须在开头声明返回值的类型。
    nil 的意思比较复杂,自己去查。nil 是很多类型的零值/空值,除了布尔值,数值,字符串。
    这里用 nil 表示没有错误。
  2. 换文件夹,修改 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))]
}

重点:

  1. init 函数。
  2. slice 实际是可变的 array。
  3. Intn 函数
  4. 用 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
}

重点:

  1. map 就是键值对,使用 make(map[key-type]value-type) 初始化一个 map。
  2. range 会在每轮循环时返回 index 和 item。使用 _ 来不使用 index。
  3. 这里的 != 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)

重点:

  1. 初始化 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 buildgo install。前者编译 packages,但不安装;后者编译后,并且安装。

  1. 在 hello 文件夹里使用 go build,会编译成一个可执行二进制文件。执行这个二进制文件即可运行。
  2. 需要进入二进制文件所在目录才能运行。如果想在任何地方都能运行,则需要安装该 package。
  3. 使用 go list -f "{{.Target}}" 可以查看将要安装在哪个位置。顺便也能看到 go 的包安装位置。
  4. 将该位置设置为环境变量,即可在任意地方使用安装的包。
    $ ser PATH=%PATH%;C:\path\to\your\install\directory
    
  5. 或者如果想要把包安装到别的位置,可以更改 go 的环境变量 GOBIN
    $ go env -w GOBIN=C:\path\to\your\bin
    
  6. 接下来就可以直接在终端上打包的名字,从而运行包
posted @ 2021-05-21 13:52  oyishyi  阅读(179)  评论(0编辑  收藏  举报