# gocode **Repository Path**: hlovez/gocode ## Basic Information - **Project Name**: gocode - **Description**: go语言学习 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-06-02 - **Last Updated**: 2023-06-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Go语言入门教程 ## 安装环境 首先通过[官网地址](https://golang.google.cn/doc/install)地址下载安装包,根据自己的系统类型,选择不同的安装安装包,下面以Windows平台作为说明演示。下载完成后打开安装包,我这里下载完成后的完整名称是: go1.20.4.windows-amd64.msi。默认情况下,安装程序会将Go安装到 Program Files 或者 Program Fielss(x86)文件夹下(Linux和Mac系统则会默认安装到 /usr/local/go 下面)。当然我们可以根据自己的需要改变默认安装的位置,但是最好不要放到包含中文文件夹的路径下,避免不必要的麻烦。**安装完成后,我们需要关闭当前已经打开的所有终端窗口,然后重新打开,以确保安装环境生效。** 然后我们验证以下Go是否安装成功,验证方式为打开Windows下的cmd终端窗口,输入 `go version`如果出现相关信息表明安装成功。 ## 入门教程 ### 前提条件 - 你应该具有一些变成的经验,入门代码会非常的简单,但是如果你有一些变成的经验会更好的理解它的含义。 - 编写代码的工具,任何文本编辑器都可以作为编写Go的代码编辑器,这里我推荐使用VScode,当然你也可以使用自己熟悉的,例如Vim等。 - 一个命令终端,Go在Linux和Mac的任何命令终端和Windows上的PowerShell与Cmd下都可以很好的工作。 ### 入门代码 下面我们以经典的 Hello,World 开始我们的 第一个Go程序吧,下面我们的操作基本都在终端中进行,我们进入到一个准备存放代码的目录,然后创建一个问价夹,暂且将它命名为hello,然后再进入到hello文件夹下。 下面,我们为代码启用**依赖项跟踪**,什么意思呢?当我们的代码导入包含在其他模块中的包时,我们可以通过代码自己的模块来管理这些依赖关系。该模块由一个go.mod文件定义,该文件跟踪这些提供包的模块。go.mod文件将于代码一起保存,包括在源码中。 我们需要通过创建go.mod文件来启用依赖项跟踪,创建方式为使用 `go mod init `其中为我们代码所在模块的名称,名称是模块的模块路径。 在实际开发过程中,模块路径通常是保存源代码的存储位置,例如模块路径可能是`github.com/mymodule`,如果我们想将自己的模块提供给其他人使用,那么模块路径必须是Go工具可以下载的位置。有关更多的模块路径命名的相关内容也可以参考官网的[管理依赖项](https://golang.google.cn/doc/modules/managing-dependencies#naming_module)部分。 对于本教程而言,我们只需要使用`example/hello`就可以: ~~~bash go mod init example/hello ~~~ 注意,上面的命令是在你要存放go代码路径下执行,上面的指令执行完成后会出现下面的提示标识创建成功 ~~~bash go: creating new go.mod: module example/hello ~~~ 此时,我们可以发现,在我们的代码目录下已经创建了一个名为go.mod的文件,里面的内容如下: ~~~mod module example/hello go 1.20 ~~~ 然后我们打开编辑器,创建一个hello.go的文件并在里面编写一些代码: ~~~go package main import "fmt" func main() { fmt.Println("Hello, World!") } ~~~ 上面就是我们在hello.go文件中要编写的代码,上面的代码中我们做了哪些事情? - 生命了一个名为main的包(包是对函数进行分组的一种方式,它由用一个目录下的所有文件组成) - 导入`fmt`包,它包含格式化文本的功能,包括打印到控制台,这个包是标准库中的其中一个,当我们在安装Go的时候已经得到了它,不需要额外下载。 - 实现一个 main 函数,用来向控制台打印消息。当我们运行主程序包时,默认会执行一个main函数。 下面我们就来运行一下刚才的代码,在当前目录下的终端输入以下命令 ~~~bash go run . ~~~ 如果不出意外的化我们会看到控制台打印了`Hello,World!`。`go run`命令是众多go命令的其中之一,我们可以使用`go help`命令来获取所有go命令列表。 ### 使用外部拓展包 当我们想实现一些功能的时候,也许这些功能已经被其他人实现并发布,这时候我们不需要自己再重新去实现,只需要查询到对应的包然后调用它里面对应的方法就可以。下面我们通过引入外部包来是我们的打印信息变的更加有趣一些。如何去做呢? 1. 首先我们访问网站[pkg.go.dev](https://pkg.go.dev/search?q=quote),然后搜索名为”quote“的包 2. 在搜索结果中找到名为`rsc.io/quote`的包 3. 然后我们进入到它的主页,在文档(Documentation)下的Index中可以看到可以从代码中调用的函数列表 我们可以使用[pkg.go.dev](https://pkg.go.dev)来查找已经发布的模块,这些模块的包中有我们需要的功能,包发布在模块中,例如`rsc.io/quote`。 下面,我们在hello.go代码中引入`rsc.io/quote`包,并调用其中的方法,添加后的代码如下: ~~~go package main import "fmt" import "rsc.io/quote" func main() { fmt.Println(quote.Go()) } ~~~ 此时,go会添加quote模块作为一个requirement,以及生成一个用于验证模块的go.sum文件,关于验证模块,更多内容可以参阅:[Authenticating modules](https://golang.google.cn/ref/mod#authenticating)。下面我们使用命令`go mod tidy`来将模块引入到go.mod文件中。 ~~~bash go mod tidy ~~~ 这时候可能会报错,我们可以通过`go env`命令查看到一项——”GOPROXY“,它的值是`https://proxy.golang.org,direct`,这个网址中国访问会有问题,因此我们需要更改一下代理地址,这里我使用[goproxy.io/zh/](https://goproxy.io/zh/),进入它的网页后根据指引进行操作,在Linux或者Mac下进行如下的设置: ~~~bash export GOPROXY=https://proxy.golang.com.cn,direct ~~~ 在Windows下: ~~~bash $env:GOPROXY = "https://proxy.golang.com.cn,direct" ~~~ 不够上面的设置方法不会长期有效,下面是长期生效的配置方法: **Mac/Linux** ~~~bash # 设置你的 bash 环境变量 echo "export GOPROXY=https://proxy.golang.com.cn,direct" >> ~/.profile && source ~/.profile # 如果你的终端是 zsh,使用以下命令 echo "export GOPROXY=https://proxy.golang.com.cn,direct" >> ~/.zshrc && source ~/.zshrc ~~~ **Windows下** ~~~bash 1. 右键 我的电脑 -> 属性 -> 高级系统设置 -> 环境变量 2. 在 “[你的用户名]的用户变量” 中点击 ”新建“ 按钮 3. 在 “变量名” 输入框并新增 “GOPROXY” 4. 在对应的 “变量值” 输入框中新增 “https://proxy.golang.com.cn,direct” 5. 最后点击 “确定” 按钮保存设置 ~~~ 执行完上面的配置后,我们再次执行`go mod tidy`,不出意外的话是成功了,如果还是失败,通过`go env`的`GOPROXY`检查一下你的环境变量配置是否生效。`go mod tidy`执行成功后会在控制台打印找到的包的版本,并提示正在下载对应的包和所需要的其他包,我的控制台打印如下: ~~~bash go: finding module for package rsc.io/quote go: found rsc.io/quote in rsc.io/quote v1.5.2 go: downloading rsc.io/sampler v1.3.0 go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c ~~~ 执行完后我们重新打开go.mod文件,发现读了一些内容: ~~~mod module example/hello go 1.20 require rsc.io/quote v1.5.2 require ( golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect rsc.io/sampler v1.3.0 // indirect ) ~~~ 上面多了两个`require`,第一个是我们引用包和对应的版本,下面的是额外需要的,从后面的注释`indirect`也可以看出,它的[中文含义](https://fanyi.baidu.com/#en/zh/indirect)就是间接的、附带的。此时,我们再重新运行一下程序,对了,tidy的意思是整洁的、整齐的,因此它不光可以下载引用包,对于无用的包它也会进行移除,这点还是很不错的。 ~~~bash go run . ~~~ 这时会在控制台打印如下内容: ~~~bash Don't communicate by sharing memory, share memory by communicating. ~~~ `quote.Go()`这个方法会打印一句Go语言的格言,具体含义大家可以自行搜索理解,它是在使用Go时的一种编程思想,以上是本片入门教程的内容,其实大部分是参照官网的内容进行的,只不过会加入一些自己在实际操作过程中遇到的问题和解决方法,感兴趣的可以自己访问官网好好阅读一下:[Tutorial: Get started with Go](https://golang.google.cn/doc/tutorial/getting-started) ### 创建Go模块 在下面的教程中,我们将创建两个模块,第一个是准备让其他库或者应用程序导入的库,也就是它是用来被引用的,第二个模块是使用第一个模块的调用方程序。总体来说我们的目的就是用第二个模块调用第一个模块的方法。 以下是接下来操作的七个简要的说明: 1. **创建一个模块:** 编写一个小模块,其中包含可以从另一个模块调用的函数。 2. **在另外一个模块中调用你的代码:** 导入并使用你第一步新创建的模块 3. **返回并处理一个错误:** 添加一个简单的错误处理 4. **随机返回一句问候语:** 处理slice中的数据,slice是Go语言总的一个动态数组 5. **为多人返回问候:** 将键值对存储到map中 6. **添加一个测试:** 使用Go语言中内置的单元测试功能测试我们的代码 7. **编译并打包应用程序:** 在本地编译并打包我们的代码 #### 创建一个其他人可以使用的模块 首先创建Go模块。在模块中,我们可以为一组离散(discrete)且有用的函数收集一个或多个相关的(related)包中。例如,我们可以创建一个包含包的模块,这些包具有进行财务分析的功能,以便其他编写财务应用的人可以使用,有关开发模块的更多信息,请参阅[开发和发布模块](https://golang.google.cn/doc/modules/developing) Go的代码被分组到包(package)中,而包被分组到模块(module)中,你的模块指定运行代码所需要的依赖项,包括Go版本以及所需要的一些其他模块,这一点在上面hello.go的演示说明中有提到过go.mod内容的描述。 当我们在模块中增加或者改进功能时,将发布模块的新版本。下面我们创建一个greetings文件夹用来编写我们的新模块,同样使用向hello.go那样的创建过程 ~~~bash go mod init example/greetings ~~~ 然后我们在greetings文件夹下创建greetings.go文件,并打开编辑器进行编写代码。 ~~~go package greetings import "fmt" //Hello 方法返回一句向名为name的人的问候语 func Hello(name string) string { //返回一句问候语,并将name参数嵌入到消息中 message := fmt.Sprintf("Hi, %v. Welcome!", name) return message } ~~~ 上面是这个模块中的第一个代码,它会向调用它的调用者返回一句问候语,上面的代码中我们做了哪些事情? - 声明了一个greetings的包用来收集相关的功能方法 - 实现了一个名为Hello的方法用来返回问候语,这个方法携带了一个string类型的name参数,并且该方法还返回一个字符串,**在Go中,名称以大写字母开头的方法可以由不在同一个包中的方法调用。这在Go中被称为导出名称(exported name)**,更多相关内容请参阅[Exported names](https://golang.google.cn/tour/basics/3) ![function-syntax](https://golang.google.cn/doc/tutorial/images/function-syntax.png) - 声明了一个message变量来处理我们的问候语,在Go中,”:=“操作符是在一行中声明和初始化变量的一种快捷方式,至于属于什么类型,Go会根据操作符右侧的内容来确定。如果不使用快捷方式那么创建过程如下: ~~~go var message string message = fmt.Sprintf("Hi, %v. Welcome!", name) ~~~ - 使用fmt包中的Sprintf方法去创建一句问候信息,它的第一个参数是一个格式化的字符串,并且Sprintf将第二个参数name参数的值替换了%v格式的谓词(verb)。 - 最后返回被格式化好的文本给调用者 #### 在hello模块中调用Hello 在本教程的最开始我们已经创建了hello模块,下面我们只需要更改hello.go中的代码即可,将代码更改为如下: ~~~go package main import ( "fmt" "example/greetings" ) func main() { message := greetings.Hello("Huhailong") fmt.Println(message) } ~~~ 对于生产使用,我们可以将模块发布到存储库中,Go工具可以从那里找到并下载下来,但目前由于我们没有发布该模块,因此我们需要调整模块,以便它可以在本地系统中找到响应的模块。要做到这一点,我们可以使用`go mod edit`命令来实现,它将Go工具从模块路径重定位到本地目录位置。 ~~~bash go mod edit -replace example/greetings=../greetings ~~~ 上面这段命令是在hello文件夹下运行的,相当于把模块路径example/greetings重新定位到与hello文件夹同级的greetings文件夹目录。运行完上面的代码后hello文件夹下的go.mod文件也发生了变化,内容如下: ~~~mod module example/hello go 1.20 replace example/greetings => ../greetings ~~~ 可以看到,在最后一行多了一行内容,然后我们在hello下运行`go mod tidy`命令来同步hello所需的依赖项。出现类似以下打印表明同步依赖项成功 ~~~bash go: found example/greetings in example/greetings v0.0.0-00010101000000-000000000000 ~~~ 后面的v0.0.0-00010101000000-000000000000表示该模块的版本号,此时go.mod文件也发生改变,增加了引用的模块名称和对应的版本号 ~~~mod module example/hello go 1.20 replace example/greetings => ../greetings require example/greetings v0.0.0-00010101000000-000000000000 ~~~ 此时后面的版本号是一个伪版本号来代替语义版本号,要引用已发布的的模块,go.mod文件通常会忽略replace指令,并使用末尾带有标记版本号的require指令,不过现在来说不重要,关于版本很好的更多内容可以参阅[Module version numbering](https://golang.google.cn/doc/modules/version-numbers) 然后我们重新运行 ~~~bash go run . ~~~ 运行结果为: ~~~bash Hi, Huhailong. Welcome! ~~~ ### 返回并处理错误 处理错误对于一个可靠的代码是一个必不可少的功能,下面我们将在greetings模块中添加一下段代码来返回一个错误,然后在调用方处理这个错误,首先我们修改greetings.go代码: ~~~go package greetings import ( "errors" "fmt" ) func Hello(name string) (string, error) { if name == "" { return "", errors.New("empty name") } message := fmt.Sprintf("Hi, %v. Welcome!") return message, nil } ~~~ 上面的代码相对于之前首先返回值发生了变化,现在返回两个值:一个字符串值,一个error值,调用者会检查第二个参数,以查看是否发生了异常(在Go中,任何方法都可以返回多个值,更多相关内容请参阅[Effective Go](https://golang.google.cn/doc/effective_go.html#multiple-returns));第二点发生变化的是我们引入了新的标准库模块——"errors",因为我们要使用它的errors.New()方法;第三点改变是增加了if表达式用来检查请求是否式无效的(这里我们规定如果传递的name参数为空字符串则式无效的请求),并且在确定式无效请求后通过errors模块的New方法返回一个空的消息和error信息;左后如果请求正确则返回问候语和nil,nil表示没有错误。 对应的,在我们的hello.go文件中也做出修改,代码如下: ~~~go package main import ( "fmt" "log" "example/greetings" ) func main() { //设置日志打印前缀 log.SetPrefix("greetings: ") //设置禁用显示日志时间、源文件和行号 log.SetFlags(0) message, err := greetings.Hello("") //如果err不为nil,则表示发生了错误,打印错误日志 if err != nil { log.Fatal(err) } fmt.Println(message) } ~~~ hello.go的代码改变为增加了标准库——"log",因为我们要打印错误日志,然后我们设置了日志的一些属性,例如上面的设置日志打印前缀,并通过设置flag禁用显示日志的时间、源文件和行号信息。在接收greetings的Hello函数时也增加了err变量用来接收错误信息,然后通过判断err是否为nil来决定是否需要打印错误日志,打印错误日志使用log的Fatal方法,如果没有发生错误则正常打印问候语,为了显示异常信息,我们把参数设置为了空字符串。下面我们重新运行以下代码 ~~~bash go run . ~~~ 运行结果如下: ~~~bash greetings: empty name exit status 1 ~~~ ### 返回随机的问候语 到目前为止,我们的greetings模块的Hello方法每次都是返回固定单一的消息,下面我们通过改造greetings.go来实现随机的返回一些问候语,在这里我们要用到Go中的slice,slice就像一个数组,只不过它可以改变大小,动态的增加和删除元素,如果你会Java,它和List是差不多的,slice是Go语言中非常有用的类型之一。关于slice的更多内容请参阅[Go slice](https://blog.golang.org/slices-intro) 下面是我们更改过后的greetings.go代码: ~~~go package greetings import ( "errors" "fmt" "math/rand" ) func Hello(name string) (string, error) { if name == "" { return "", errors.New("empty name") } //调用随机方法 message := fmt.Sprintf(randomFormat(), name) return message,nil } //随机生成格式化字符串方法 func randomFormat() string { //声明一个元素类型为string的slice,并存放了三条问候语格式化字符串 formats := []string{ "Hi, %v. Welcome!", "Great to see you, %v", "Hail, %v! Well met!", } //随机返回上面三个字符串 return formats[rand.Intn(len(formats))] } ~~~ 在上面的greetings.go的代码中,我们增加了`randomFormat()`方法,并且这个方法的名称是以小写字母开头的,这意味着它只能在同一个包中被调用,它没有被暴露(exported)出去,如果想让其他包中也使用它则需要将它的方法名首字母改为大写。然后在方法中声明了一个名为formats的slice,并且设置了三条消息,当我们在生命slice的时候使用了空中括号,这表明我们的slice大小是可以动态改变的。然后我们使用`math/rand`包去生成一个随机数来选择slice的元素,init函数会使用当前时间作为rand包设定随机种子。在初始化完全局变量后,Go启动时会自动执行init函数。有关init函数的更多消息请参阅[Effective Go.](https://golang.google.cn/doc/effective_go.html#init)。最后我们重新运行hello.go,运行结果如下: ~~~bash Hi, Huhailong. Welcome! Hi, Huhailong. Welcome! Great to see you, Huhailong Great to see you, Huhailong Hail, Huhailong! Well met! ~~~ 可以看到已经是随机的进行回复了。 ### 使用键值对进行多人的回复 前面的每次请求调用都是传递一个name参数,下面我们通过传递一个slice数组参数,然后返回一个键值对的结果来实现一次请求,多人回复的功能,为此我们在greetings.go文件中新增一个函数——Hellos来实现这个功能,代码如下: ~~~go // 以下是greetings.go新增的代码,之前的代码与上面的相同,不再显示 func Hellos(names []string) (map[string]string, error) { messages := make(map[string]string) for _, name := range names { message, err := Hello(name) if err != nil { return nil, err } messages[name] = message } return messages, nil } ~~~ 上面的方法中我们使用一个slice类型的参数——names,而不是单个字符串名称,另外我们将返回类型由字符串改为了map类型。在Hellos方法中我们使用了已经存在的函数——Hello,这有助于减少重复代码。然后我们创建了一个名为messages的map,这个map使用name作为key,消息作为value进行关联。在Go语言中,初始化一个map的语法是:make(map[key-type]value-type)。方法中最终将map和异常信息返回。更多关于map的内容请参阅[Go map in action blog](https://blog.golang.org/maps)。在for循环中的操作是对方法接收到的names进行循环,对每一个name为其关联一个message,在for循环中**range**返回两个值,第一个是当前项在循环中的索引,第二个是对应项的拷贝,因为这里我们不需要索引,因此我们使用Go blank 这里是也就是下划线标识符来忽略它,更多关于Go blank的内容请参阅[The blank identifier](https://golang.google.cn/doc/effective_go.html#blank)。 下面我们改变hello.go文件 ~~~go package main import ( "fmt" "log" "example/greetings" ) func main() { log.SetPrefix("greetings: ") log.SetFlags(0) names := []string{"Huhailong","Wuxinhua","Xiaochun"} messages, err := greetings.Hellos(names) if err != nil { log.Fatal(err) } fmt.Println(messages) } ~~~ hello.go中改变的地方主要是传参的改变,由之前的单个字符串替换为了使用slice类型的参数,然后调用函数由Hello改为了Hellos,再次重新运行代码后结果如下: ~~~bash map[Huhailong:Hail, Huhailong! Well met! Wuxinhua:Hail, Wuxinhua! Well met! Xiaochun:Hi, Xiaochun. Welcome!] ~~~ ### 增加测试 如果你已经跟着上面的教程进行到了这里,那你真的很不错,下面我们就为Hello函数增加一个测试。Go语言内置对单元测试的支持,因此在测试时是容易的。具体的来说,使用命名约定、Go的测试包和测试命令,我们可以快速编写和执行测试。 首先,我们在greetings目录下创建一个名为 greetings_test.go的文件,**这里需要注意一个命名约定,那就是以_test.go结尾的文件会告诉`go test`命令这是一个包含测试方法的文件。**在这个文件中加入以下代码: ~~~go package greetings import ( "testing" "regexp" ) //调用greetings.Hello并带有有效的name,验证返回值是否有效 func TestHelloName(t *testing.T) { name := "Huhailong" want := regexp.MustCompile(`\b`+name+`\b`) msg, err := Hello("Huhailong") if !want.MatchString(msg) || err != nil { t.Fatalf(`Hello("Huhailong") = %q, %v, wnat match for %#q, nil`, msg, err, want) } } //调用greetings.Hello并带有空name,验证error是否正常 func TestHelloEmpty(t *testing.T) { msg, err := Hello("") if msg != "" || err == nil { t.Fatalf(`Hello("") = %q, %v, want "", error`, msg, err) } } ~~~ **注意:测试代码应该与被测试的代码处于同一个包中,这里他们都属于greetings包。** 上面的测试方法中有两个方法,一个用来测试正常传递name的返回值是否有效,第二个用来测试当name为空字符穿的时候error是否正常返回。此外,测试函数将指向测试包的[testing.T](https://pkg.go.dev/testing#T)类型的指针作为参数。我们可以i使用此参数的方法从测试中进行报告和日志记录。至于regxp就是Go语言中的正则表达式库,\b 表示单词边界。也就是说,返回的内容左右两边需要有内容。 下面我们就通过 `go test`命令来执行测试,使用这个命令是也可以加上-v来输出详细的日志打印。这里的命令是在greetings目录下执行的,注意切换 ~~~bash go test ~~~ 输出 ~~~bash ok example/greetings 0.538s ~~~ 使用-v输出详细日志 ~~~bash go test -v ~~~ 输出 ~~~bash === RUN TestHelloName --- PASS: TestHelloName (0.00s) === RUN TestHelloEmpty --- PASS: TestHelloEmpty (0.00s) PASS ok example/greetings 0.052s ~~~ 上面是测试成功的效果,那如果测试失败会怎么样,我们修改以下Hello方法,修改如下: ~~~go func Hello(name string) (string, error) { if name == "" { return "", errors.New("empty name") } //Return a greeting taht embeds the name in a message //message := fmt.Sprintf(randomFormat(), name) message := fmt.Sprintf(randomFormat()) return message,nil } ~~~ 我们在返回的结果中去掉了参数name的值,接下来再执行测试,看看会发生什么? ~~~bash go test -v ~~~ 输出 ~~~bash === RUN TestHelloName greetings_test.go:14: Hello("Huhailong") = "Hail, %!v(MISSING)! Well met!", , wnat match for `\bHuhailong\b`, nil --- FAIL: TestHelloName (0.00s) === RUN TestHelloEmpty --- PASS: TestHelloEmpty (0.00s) FAIL exit status 1 FAIL example/greetings 0.530s ~~~ 在输出结果中我们可以看到,第二个测试方法是正常通过了,第一个测试方法提示测试失败,提示我们断言返回的和真实返回的不一致。 ### 编译和打包程序 这是本篇教程的最后一部分内容,主要说明如何编译和打包我们的应用程序。虽然上面我们通过 `go run`命令可以快速的进行编译并运行,但是它不会生成可执行的二进制文件,这意味着如果其他人项使用我们的程序时,如果我们不打包,对方需要也安装Go语言环境,这显然是不好的。下面我们学习两个新的命令来解决这个问题。 - **go build:** 编译这个包以及它的依赖项,但它不会打包结果 - **go install:** 编译并且打包这个包 > go build 和 go install 的区别在于,go build 会在目录下生成一个可执行的文件,而使用 go install 的话打包后的可执行文件会存放到指定的位置,在Windows下是 C:\Users\yourname\go\bin 下面,这以为这如果我们把这个路径配置到环境变量中,那么我们每次打包的应用程序可以在不指定文件夹路径的情况下运行。 现在让我们回到hello这个目录下,然后运行`go build`命令来将代码编译成可执行文件 ~~~bash go bulid ~~~ 当我们运行了上面的命令后发现在hello目录下多了一个hello.exe文件(如果是Linux和Mac会生成一个hello的可执行文件,通过./hello就可以运行),然后我们运行它发现输出的结果和使用 `go run`返回的一样,这表明我们编译成功了。是的,就是这么简单。 ~~~bash go install ~~~ 上面已经提到了,go install 会将打包号的可执行文件放到指定的位置,可以通过 `go list` 命令来查看安装路径 ~~~bash go list -f '{{.Target}}' ~~~ 至此,Go语言的基础入门就Ok,本文只是我在通过Go官网学习的时候边实操边记录的,建议大家找一个时间比较充裕的时候从头到尾的实操一遍,只有基本入门了以后才可以继续深入的学习。 # Go语言中的多模块工作空间介绍 本片主要是介绍了Go语言中多模块工作空间的基础知识。使用多模块工作空间,我们可以告诉Go命令我们正在同时在多个模块下编写代码,并可以轻松的在这些模块中构建和运行代码。 下面我们将在共享的多模块工作空间中创建两个模块,然后在这些模块之间进行修改,并在最终的构建中看到这些修改的结果。 **注意:如果你还没有安装Go语言环境和使用基本的命令并创建模块等基本操作的话请阅读我的另外一篇博客:[Go语言入门教程](https://blog.csdn.net/hhl18730252820/article/details/131003961)。** ## 创建我们自己的模块 首先我们创建自己的模块并编写代码,在这之前我们先创建文件夹,由于我们要使用工作空间,因此我们先创建一个大的文件夹,这里我将其命名为 workspace ,后续的模块文件夹都会放到该目录下。 在创建好 workspace 文件夹后进入到该文件夹下再创建我们的模块文件夹,这里还是以 hello 模块作为演示吧,所以创建 hello 文件夹并进入到hello目录下,初始化模块: ~~~bash go mod init example.com/hello ~~~ 下面我们使用 `go get` 命令来为 hello 模块添加 golang.org/x/example 依赖,因为我们要使用这个模块中的方法。在博客[Go语言入门教程](https://blog.csdn.net/hhl18730252820/article/details/131003961)中有一个 `go mod tidy`的命令,如果你不使用`go get`在代码中的import中引入了这个模块时,使用`go mod tidy`也可以添加依赖,但是`go mod tidy`和`go get`还是有区别的,`go mod tidy`我们说过它除了可以添加依赖还可以清理无用依赖,一般我们拿到一个Go项目后通常运行一下`go mod tidy`来确保所有依赖被下载。如果你熟悉前端的npm或者yarn的话你可以把`go mod tidy`比作`yarn`或`npm install`命令,它是一个全局性的,而`go get`类似于`yarn add`和`npm install`命令。 ~~~bash go get golang.org/x/example ~~~ 然后创建 hello.go 文件,并编写代码,如下: ~~~go package main import ( "fmt" "golang.org/x/example/stringutil" ) func main() { //反转字符串Hello fmt.Println(stringutil.Reverse("Hello")) } ~~~ 运行代码: ~~~bash go run example.com/hello ~~~ ## 创建工作空间 下面我们在 workspace 目录下生成一个 go.work 文件,这个文件的作用是为模块指定工作空间。 ### 初始化工作空间 在 workspace 目录下运行下面的指令: ~~~bash go work init ./hello ~~~ 执行完上面的指令后会在 workspace 中自动生成了一个 go.work 文件,下面是 go.work 文件中的内容: ~~~ go 1.20 use ./hello ~~~ 它的语法和 go.mod 文件类似,第一行是中`go`指令告诉Go应该使用哪个版本,use告诉我们哪个目录下的模块是主模块。因此,工作空间下的任何子目录魔窟都将处于激活状态。这里是什么意思呢?通过一个演示我们就明白了,我们在workspace 目录下新建一个test目录,然后将刚刚的 go.work 文件先删掉,再进入到test目录下,运行hello模块 ~~~bash go run example.com/hello ~~~ 运行结果 ~~~bash no required module provides package example.com/hello: go.mod file not found in current directory or any parent directory; see 'go help modules' ~~~ 可以看到是无法运行成功的,下面我们再把 go.work 文件恢复或者再次使用上面的`go work init ./hello`初始化一下,此时我们再进入到test文件夹运行会运行成功。 ### 在workspace目录运行程序 在workspace目录下执行: ~~~bash go run example.com/hello ~~~ 运行结果 ~~~ olleH ~~~ 上面的命令包含所有的模块作为主模块在工作空间,这使得我们可以引用模块中的包,甚至可以引用模块外的包。 下面,我们将在工作空间中添加一个`golang.org/x/example`模块的本地副本,我们将在这个模块的`stringutil`包中添加一个新的方法来代替`Reverse`方法。 ## 下载并修改`golang.org/x/example`模块 我们使用`git`将`golang.org/x/example`模块克隆到 workspace 文件夹下,也就是我们的工作空间下 ~~~bash git clone https://go.googlesource.com/example ~~~ 如果上面的克隆地址无法正常使用多半是因为国内无法访问该网址的问题,请尝试官方为其在GitHub设置的镜像仓库,换成如下命令 ~~~bash git clone https://github.com/golang/example ~~~ 然后同样的,我们将这个模块目录添加到工作空间中 ~~~bash go work use ./example ~~~ 运行完上面的命令后,go.work 文件也发生了变化,如下: ~~~ go 1.20 use ( ./example ./hello ) ~~~ 现在我们工作空间中包含了两个模块,一个是刚刚克隆到本地的 `golang.org/x/example`另一个是我们最开始创建的`example.com/hello`。这将允许我们使用一会我们在拉取模块中stringutil的副本中编写的代码,而不使用`go get`命令下载的模块缓存中的代码。 下面我们就来添加一个新的方法吧,首先我们进入到 workspace/example/stringuil 目录下,然后创建一个名为 toupper.go 的文件,接着添加如下代码: ~~~go package stringutil import "unicode" func ToUpper(s string) string { r := []rune(s) for i := range r { r[i] = unicode.ToUpper(r[i]) } return string(s) } ~~~ 上面新添加的方法顾名思义,就是将传进来的参数转为大写,这里有一个新的类型 rune ,它是Go的一个内置类型,因为这里用到了Unicode,在这里可以把它理解为一个Unicode字符,在处理字符串尤其是包含多种语言字符串情况下它会使用的比较多 接下来,我们回到 workspace/hello 目录下修改我们的 hello.go 文件,将Revers方法替换为ToUpper方法: ~~~go package main import ( "fmt" "golang.org/x/example/stringutil" ) func main() { fmt.Println(stringutil.ToUpper("Hello")) } ~~~ 然后我们在 workspace 目录下再次运行 ~~~bash go run example.com/hello ~~~ 运行结果 ~~~ HELLO ~~~ 上面Go命令会根据go.work文件找到工作目录下包含的模块文件夹,然后找到对应我们运行命令中的模块,类似的,也会根据 go.work 解析导入 golang.org/x/example,如果我们把 go.work 删除掉,也就是没有使用Go的工作空间时那么会发生什么,我们来试一下, 这里我先把 go.work 文件删除掉,然后重新运行,结果如下: ~~~ no required module provides package example.com/hello: go.mod file not found in current directory or any parent directory; see 'go help modules' ~~~ 可以看到,没有了 go.work 文件 workspace 就是一个普通的目录,它不知道该去哪里找模块,那我们进入到 hello 文件夹下直接运行会发生什么呢?运行结果如下: ~~~ # example.com/hello .\hello.go:9:25: undefined: stringutil.ToUpper ~~~ 可以看到,提示我们没有定义 stringutil.ToUpper ,同样也是因为没有了 go.work 的指引,我们的 hello.go 不知道去哪里找这个方法 还记得[Go语言入门教程](https://blog.csdn.net/hhl18730252820/article/details/131003961)里的例子吗?hello 和 greetings 也是两个不同的模块,他们之间的互相调用是使用了`replace`指令,在hello文件夹下的go.mod 文件指定了要调用的模块在哪个文件夹。如果我们在进行多模块开发,相对于那种方式,显然使用工作空间更加方便和优雅。 当我们修改完一个模块后如何正确的发布它呢?这通常是通过模块的版本控制存储库tag一个提交来完成的,这部分更多的内容可以参考[官方文档](https://go.dev/doc/modules/release-workflow),一旦发布完成,我们可以在模块中指定需要的版本,例如我们将 `golang.org/x/example` 发布为 `v0.1.0`那么,我们可以在hello模块使用该模块的时候指定下载对应版本的依赖 ~~~bash cd hello go get golang.org/x/example@v0.1.0 ~~~ 这样,Go命令就可以正确的解析工作空间外部的模块了。 ## 关于工作空间的一些子命令 除了上面我们演示到的,关于工作空间还有一些子命令,下面简单的介绍一下: - go work use [-r] [dir]:为 go.work 文件中添加 use指令,-r 表示递归性检查 - go work edit:编辑 go.work 文件,类似 `go mod eidt` - go work sync 将工作区的构建列表中的依赖项同步至工作区中的每一个模块中 以上就是关于 Go语言中使用工作空间的一些简单的使用和介绍。 # 使用Go和Gin开发RESTFul API 本篇内容介绍如何使用Go和 [Gin Web Framework](https://gin-gonic.com/docs/)来编写RESTFul API 服务的基础知识。如果你还对Go的基础操作不熟悉的话最好还是先看一下: [入门教程](https://blog.csdn.net/hhl18730252820/article/details/131003961) Gin是一个Go语言的Web开发框架,它简化了构建Web应用的编码。在下面的介绍和代码中,我们将通过Gin来路由请求、处理请求数据和返回JSON响应。 ## 设计API端点 这里我们根据官方给出的示例来进行,我们将建立一个API,它可以访问一家出售古董黑胶唱片的商店。因此,我们需要创建可以查询和增加唱片的端点(官网用 endpoint 来表达,其实就是我们平常的接口路由的概念),下面是教程中会创建的两个端点: **/albums** - GET: 用来查询所有的专辑,返回的是JSON数据 - POST: 发送JSON数据的请求来添加专辑 **/albums/:id** - GET: 用来获取指定ID的专辑 ## 创建文件夹和代码 首先我们创建存放我们代码的文件夹,这里命名为 web-service-gin ,然后进入到该文件夹,执行 `go mod init`进行初始化: ~~~bash go mod init example/web-service-gin ~~~ ## 创建数据 下面我们来设计数据结构,教程中为了简单演示,因此将数据是存储到了内存中进行处理,通常情况下是要结合数据库来一起使用的。因此本教程的数据在每次服务的关闭和启动后数据会被重新初始化为内存中的数据。 下面,我们创建一个名为 main.go 的文件,然后进行编码: ~~~go package main type album struct { ID string `json:"id"` Title string `json:"title"` Artist string `json:"artis"` Price float64 `json:"price"` } ~~~ 一个独立的程序(相对库来说)它始终位于main包中,然后我们定义了 album 结构体,**这里需要注意的是`json:"id"`这样的表示当被序列化为JSON时的字段名为什么,上面显是将序列化后的字段统一为小写**,如果没有它们,在返回的JSON和传递参数的JSON都需要将首字母大写。 下面我们来创建一些数据,大家可以直接粘贴下面的数据到你的结构提下面 ~~~go var albums = []album{ {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, } ~~~ ## 编写处理代码 下面,我们将实现处理数据的代码。 ### 编写返回所有数据项代码 当客户端对`/albums`发起GET请求时,我们要把所有的数据项以JSON的形式返回给客户端,实现这个我们需要关注下面两点: - 处理响应的逻辑 - 映射请求路径的逻辑 接下来,我们在代码中增加一个方法,名为 getAlbums ~~~go func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) } ~~~ 在上面的代码中,我们首先创建了一个 getAlbums 方法,这里的方法名可以随意,然后将`gin.Context`作为参数。`gin.Context`是Gin最终要的部分,它携带了请求的详情、验证和序列化JSON等等。然后`Context.IndentedJSON`的作用是将数据序列化为JSON后作为响应,这个方法的第一个参数是请求的状态,这里我们复制为了请求成功的状态,也就是200,第二个参数就是我们要序列化返回给客户端的数据。 > 注意:我们可以使用`Context.JSON`来代替`Context.IndentedJSON`,两者的区别是`Context.JSON`会是序列化后的JSON更紧凑,`Context.IndentedJSON`是带缩进的格式化后的JSON。 下面我们再添加主方法,它的作用是将请求和对应的处理进行关联 ~~~go func main() { router := gin.Default() router.GET("/albums", getAlbums) router.Run("localhost:8080") } ~~~ 上面的代码中我们首先使用`Default`方法初始化了路由,然后使用路由的GET方法将请求路径为`/albums`和处理方法`getAlbums`进行关联。最后使用`Run`函数启动了一个host为localhost端口为8080的http服务。 添加完上面的代码后我们需要添加对应的依赖库,需要在 package main 下加入下面代码后保存文件: ~~~go package main import ( "net/http" "github.com/gin-gonic/gin" ) ~~~ #### 运行代码 在运行代码前我们首先获取我们需要的相关依赖,这里我们使用 `go get` 命令去添加,执行如下代码: ~~~bash go get . ~~~ 然后我们在 main.go 的目录下运行代码 ~~~bash go run . ~~~ 一旦运行,现在就有一个HTTP服务在运行,我么可以向它发送请求,你可以在浏览器输入地址: `http://localhost:8080/albums`来访问数据,或者使用curl命令在终端中请求 ~~~bash curl http://localhost:8080/albums ~~~ 请求到的数据如下: ~~~json [ { "id": "1", "title": "Blue Train", "artis": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artis": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artis": "Sarah Vaughan", "price": 39.99 } ] ~~~ ### 编写添加新数据项代码 下面我们编写另外一个端点,当用户对`/albums`发起POST请求时,我们将接收到的JSON数据反序列化后添加到`albums`数据中,同样的,我们下面的代码关注两个点: - 向存在的列表添加新数据项的逻辑 - 处理POST请求的逻辑代码 首先,我们在 main.go 文件中增加下面的方法: ~~~go func postAlbums(c *gin.Context) { var newAlbum album //使用BindJSON来绑定接收到的JSON数据到newAlbum if err := c.BindJSON(&newAlbum); err != nil { return } //添加新的albm到slice中 albums = append(albums, newAlbum) c.IndentedJSON(http.StatusCreated, newAlbum) } ~~~ 上面的代码中使用`Context.BindJSON`来绑定请求体到`newAlbum`,然后将新的album添加到albums slice中,最后添加201状态码,并将新添加的内容返回给客户端。下面我们修改main方法中的代码 ~~~go func main() { router := gin.Default() router.GET("/albums", getAlbums) router.POST("/albums", postAlbums) router.Run("localhost:8080") } ~~~ #### 运行代码 ~~~bash go run . ~~~ 接下来我们测试一下,如果你使用的是 Linux/MacOS 直接使用 curl 命令发起请求: ~~~bash curl http://localhost:8080/albums \ --include \ --header "Content-Type: application/json" \ --request "POST" \ --data '{"id": "4","title": "The Modern Sound of Betty Carter","artist": "Betty Carter","price": 49.99}' ~~~ 如果是Windows的话使用Postman或其他的请求工具,发送如下的JSON数据 ~~~json { "id": "4", "title": "The Modern Sound of Betty Carter", "artist": "Betty Carter", "price": 49.99 } ~~~ 我这里用客户端请求工具请求的结果如下: ![请求截图](https://www.devcat.cn/res/img/photos/20230608111244.png) 然后再次访问GET请求发现新增成功,结果如下: ~~~json [ { "id": "1", "title": "Blue Train", "artis": "John Coltrane", "price": 56.99 }, { "id": "2", "title": "Jeru", "artis": "Gerry Mulligan", "price": 17.99 }, { "id": "3", "title": "Sarah Vaughan and Clifford Brown", "artis": "Sarah Vaughan", "price": 39.99 }, { "id": "4", "title": "The Modern Sound of Betty Carter", "artis": "", "price": 49.99 } ] ~~~ ### 编写返回指定的数据项代码 当客户端发起一个 `/albums/[id]`这样的GET请求时,我们需要从albums中找到匹配的ID然后返回给客户端,下面我们来编写代码,我们在postAlbums方法下面增加getAlbumByID的方法,代码如下: ~~~go func getAlbumByID(c *gin.Context) { id := c.Param("id") //循环albums列表,然后找打匹配的数据项,如果没有找到返回Not Found状态和信息 for _, a := range albums { if a.ID == id { c.IndentedJSON(http.StatusOK, a) return } } c.IndentedJSON(http.StatusNotFound, gin.H{"message":"album not found"}) } ~~~ 上面的代码中使用`Context.Param`方法来检索请求路径中的对应占位符的参数,然后通过循环albums数据对比ID来找出对应的album数据,如果没有找到则返回404状态码,然后我们再修改main函数中的代码 ~~~go func main() { router := gin.Default() router.GET("/albums", getAlbums) router.GET("/albums/:id", getAlbumByID) router.POST("/albums", postAlbums) router.Run("localhost:8080") } ~~~ #### 运行代码 再次运行代码,这次在之前GET请求的基础上修改为 `http://localhost:8080/albums/1` ,这表示我们想查询ID为1的album数据项,请求结果如下: ~~~json { "id": "1", "title": "Blue Train", "artis": "John Coltrane", "price": 56.99 } ~~~ ## 完整代码 下面是本篇教程中的完整代码 ~~~go package main import ( "net/http" "github.com/gin-gonic/gin" ) type album struct { ID string `json:"id"` Title string `json:"title"` Artist string `json:"artis"` Price float64 `json:"price"` } var albums = []album{ {ID: "1", Title: "Blue Train", Artist: "John Coltrane", Price: 56.99}, {ID: "2", Title: "Jeru", Artist: "Gerry Mulligan", Price: 17.99}, {ID: "3", Title: "Sarah Vaughan and Clifford Brown", Artist: "Sarah Vaughan", Price: 39.99}, } func getAlbums(c *gin.Context) { c.IndentedJSON(http.StatusOK, albums) } func postAlbums(c *gin.Context) { var newAlbum album if err := c.BindJSON(&newAlbum); err != nil { return } albums = append(albums, newAlbum) c.IndentedJSON(http.StatusCreated, newAlbum) } func getAlbumByID(c *gin.Context) { id := c.Param("id") for _, a := range albums { if a.ID == id { c.IndentedJSON(http.StatusOK, a) return } } c.IndentedJSON(http.StatusNotFound, gin.H{"message": "album not fond"}) } func main() { router := gin.Default() router.GET("/albums", getAlbums) router.POST("/albums", postAlbums) router.GET("/albums/:id", getAlbumByID) router.Run("localhost:8080") } ~~~ # Go语言中的泛型 本文介绍了Go中泛型的基础知识。使用泛型,你可以声明和使用具有为调用代码提供的一组类型的函数或类型。在下面,我们将声明两个简单的非泛型函数,然后我们用一个泛型函数来实现相同的功能。 我们将完成以下几个部分: 1. 为我们的代码先创建一个文件夹 2. 添加非泛型函数方法 3. 添加泛型函数方法来处理多类型 4. 当调用泛型函数方法时移除类型参数 5. 声明一个类型约束 ## 创建代码文件夹 这里我就将文件夹命名为 generics ,然后我们进入到该文件夹并初始化模块 ~~~bash cd generics ~~~ ~~~bash go mod init example/generics ~~~ ## 添加非泛型方法 在这一步中,我们将添加两个函数,每一个函数将map中的数相加并返回。 ### 编写代码 main.go ~~~go package main func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } func SumFloats(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s } ~~~ 上面的代码中的两个方法分别是对都以字符串为key,分别以int64和float64为value的数进行求和并返回对应的数据。下面我们编写主方法并在里面初始化两个map然后调用上面的方法 ~~~go func main() { ints := map[string]int64{ "first": 34, "second": 12, } floats := map[string]float64{ "first": 35.98, "second": 26.99 } fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloats(floats)) } ~~~ 由于上面我们使用了 fmt 进行打印,因此需要引入 fmt 标准库 ~~~go package main import "fmt" ~~~ ### 运行代码 使用 `go run .`命令运行代码 ~~~bash go run . ~~~ 运行结果 ~~~ Non-Generic Sums: 46 and 62.97 ~~~ ## 添加泛型方法处理多类型 上面使用了两个方法实现类类似的功能,只不过它们的参数类型不同,如果一两个这样的操作你觉得无所谓,但是当数据类型很多时如果实现的功能相同那上面的做法很不优雅,会产生大量的冗余代码,下面我们将使用一个方法来实现上面的功能,该方法允许int64或float64作为参数类型 ~~~go func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { var s V for -, v := range m { s += v } return s } ~~~ 上面的代码中声明了一个SumIntsOrFloats函数,该函数具有两个类型参数(方括号里面的)K和V,以及一个使用类型参数的形参集map[K]V的m,该函数返回V类型的值。 给K指定了 `comparable` 类型的参数,Go要求map的Key值具有可比性,因此将它设置为可比较的类型是必要的,这还确保了调用者对map简直的类型使用。 给V指定了两种类型的并集,即int64和float64,只要符合其中的一种类型即可,形参m指定为了map[K]V,这里我们确定map是一个有效的map,因为在前面已经对K做了可比较类型的约束。下面我们修改主方法: ~~~go fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats)) ~~~ ### 运行代码 ~~~bash go run . ~~~ 运行结果 ~~~ Generic Sums: 46 and 62.97 ~~~ 可以看到,是同样的结果,但是我们使用了泛型函数只需要一个函数就可以。 ## 调用时移除类型参数 在上面的main方法中我们在调用泛型方法时加了参数类型,也就是中括号里面的内容`[string, int64]`来告诉泛型函数我们调用时传递的参数类型,但其实Go编译器在编译的时候是可以根据方法参数推断出参数的类型,因此我们可以省略不写。但是注意:**这并不总是可能的,如果被调用的泛型函数没有参数,此时我们在调用方法的时候应该包含参数类型。** 下面我们修改一下main.go的代码: ~~~go fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats)) ~~~ 下面我们运行代码,这次我把上面的两个打印都放开,对比一下,结果是: ~~~ Non-Generic Sums: 46 and 62.97 Generic Sums: 46 and 62.97 Generic Sums, type parameters inferred: 46 and 62.97 ~~~ 可以看到,当我们省略了参数类型的时候依然正确的运行了程序,说明Go编译的时候自动推断出了参数的类型。 ## 声明类型约束接口 下面,我们将把前面定义的约束移动到我们自己的接口中,这样我们就可以在多个地方重用它,这种声明方式有助于简化代码,例如当约束更复杂时。 当我们将类型约束声明为接口,约束允许实现接口的任何类型。例如,如果用三个方法声明类型约束接口,然后将其与泛型函数中的类型参数一起使用,则用于调用该函数的类型参数被许具有所有的这些方法。 约束接口也可以引用特定的类型,例如我们下面这样使用 ~~~go type Number interface { int64 | float64 } 在上面的代码中,我们声明了一个名为Number的约束接口,在接口内声明了int64和float64的并集,下面当我们需要使用`int64 | float64`这样的约束类型时就可以使用Number这个类型,而不需要写`int64 | float64`代码如下 ~~~go func SumNumbers[K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s } ~~~ 然后我们将main.go中添加新方法的打印 ~~~go fmt.printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats)) ~~~ 然后我们运行代码,结果如下: ~~~ Non-Generic Sums: 46 and 62.97 Generic Sums: 46 and 62.97 Generic Sums, type parameters inferred: 46 and 62.97 Generic Sums with Constraint: 46 and 62.97 ~~~ ### 本文最终完整代码 ~~~go package main import "fmt" type Number interface { int64 | float64 } //Non-Generic Function func SumInts(m map[string]int64) int64 { var s int64 for _, v := range m { s += v } return s } func SumFloat(m map[string]float64) float64 { var s float64 for _, v := range m { s += v } return s } // Generic Function func SumIntsOrFloats [K comparable, V int64 | float64](m map[K]V) V { var s V for _, v := range m { s += v } return s } func SumNumbers [K comparable, V Number](m map[K]V) V { var s V for _, v := range m { s += v } return s } func main() { ints := map[string]int64 { "first": 34, "second": 12, } floats := map[string]float64 { "first": 35.98, "second": 26.99, } fmt.Printf("Non-Generic Sums: %v and %v\n", SumInts(ints), SumFloat(floats)) fmt.Printf("Generic Sums: %v and %v\n", SumIntsOrFloats[string, int64](ints), SumIntsOrFloats[string, float64](floats)) fmt.Printf("Generic Sums, type parameters inferred: %v and %v\n", SumIntsOrFloats(ints), SumIntsOrFloats(floats)) fmt.Printf("Generic Sums with Constraint: %v and %v\n", SumNumbers(ints), SumNumbers(floats)) } ~~~