对应的代码仓库地址:gocode
首先通过官网地址地址下载安装包,根据自己的系统类型,选择不同的安装安装包,下面以Windows平台作为说明演示。下载完成后打开安装包,我这里下载完成后的完整名称是: go1.20.4.windows-amd64.msi。默认情况下,安装程序会将Go安装到 Program Files 或者 Program Fielss(x86)文件夹下(Linux和Mac系统则会默认安装到 /usr/local/go 下面)。当然我们可以根据自己的需要改变默认安装的位置,但是最好不要放到包含中文文件夹的路径下,避免不必要的麻烦。安装完成后,我们需要关闭当前已经打开的所有终端窗口,然后重新打开,以确保安装环境生效。
然后我们验证以下Go是否安装成功,验证方式为打开Windows下的cmd终端窗口,输入 go version
如果出现相关信息表明安装成功。
下面我们以经典的 Hello,World 开始我们的 第一个Go程序吧,下面我们的操作基本都在终端中进行,我们进入到一个准备存放代码的目录,然后创建一个问价夹,暂且将它命名为hello,然后再进入到hello文件夹下。
下面,我们为代码启用依赖项跟踪,什么意思呢?当我们的代码导入包含在其他模块中的包时,我们可以通过代码自己的模块来管理这些依赖关系。该模块由一个go.mod文件定义,该文件跟踪这些提供包的模块。go.mod文件将于代码一起保存,包括在源码中。
我们需要通过创建go.mod文件来启用依赖项跟踪,创建方式为使用 go mod init
其中为我们代码所在模块的名称,名称是模块的模块路径。
在实际开发过程中,模块路径通常是保存源代码的存储位置,例如模块路径可能是github.com/mymodule
,如果我们想将自己的模块提供给其他人使用,那么模块路径必须是Go工具可以下载的位置。有关更多的模块路径命名的相关内容也可以参考官网的管理依赖项部分。
对于本教程而言,我们只需要使用example/hello
就可以:
go mod init example/hello
注意,上面的命令是在你要存放go代码路径下执行,上面的指令执行完成后会出现下面的提示标识创建成功
go: creating new go.mod: module example/hello
此时,我们可以发现,在我们的代码目录下已经创建了一个名为go.mod的文件,里面的内容如下:
module example/hello
go 1.20
然后我们打开编辑器,创建一个hello.go的文件并在里面编写一些代码:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
上面就是我们在hello.go文件中要编写的代码,上面的代码中我们做了哪些事情?
fmt
包,它包含格式化文本的功能,包括打印到控制台,这个包是标准库中的其中一个,当我们在安装Go的时候已经得到了它,不需要额外下载。下面我们就来运行一下刚才的代码,在当前目录下的终端输入以下命令
go run .
如果不出意外的化我们会看到控制台打印了Hello,World!
。go run
命令是众多go命令的其中之一,我们可以使用go help
命令来获取所有go命令列表。
当我们想实现一些功能的时候,也许这些功能已经被其他人实现并发布,这时候我们不需要自己再重新去实现,只需要查询到对应的包然后调用它里面对应的方法就可以。下面我们通过引入外部包来是我们的打印信息变的更加有趣一些。如何去做呢?
rsc.io/quote
的包我们可以使用pkg.go.dev来查找已经发布的模块,这些模块的包中有我们需要的功能,包发布在模块中,例如rsc.io/quote
。
下面,我们在hello.go代码中引入rsc.io/quote
包,并调用其中的方法,添加后的代码如下:
package main
import "fmt"
import "rsc.io/quote"
func main() {
fmt.Println(quote.Go())
}
此时,go会添加quote模块作为一个requirement,以及生成一个用于验证模块的go.sum文件,关于验证模块,更多内容可以参阅:Authenticating modules。下面我们使用命令go mod tidy
来将模块引入到go.mod文件中。
go mod tidy
这时候可能会报错,我们可以通过go env
命令查看到一项——”GOPROXY“,它的值是https://proxy.golang.org,direct,这个网址中国访问会有问题,因此我们需要更改一下代理地址,这里我使用goproxy.io/zh/,进入它的网页后根据指引进行操作,在Linux或者Mac下进行如下的设置:
export GOPROXY=https://proxy.golang.com.cn,direct
在Windows下:
$env:GOPROXY = "https://proxy.golang.com.cn,direct"
不够上面的设置方法不会长期有效,下面是长期生效的配置方法:
Mac/Linux
# 设置你的 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下
1. 右键 我的电脑 -> 属性 -> 高级系统设置 -> 环境变量
2. 在 “[你的用户名]的用户变量” 中点击 ”新建“ 按钮
3. 在 “变量名” 输入框并新增 “GOPROXY”
4. 在对应的 “变量值” 输入框中新增 “https://proxy.golang.com.cn,direct”
5. 最后点击 “确定” 按钮保存设置
执行完上面的配置后,我们再次执行go mod tidy
,不出意外的话是成功了,如果还是失败,通过go env
的GOPROXY
检查一下你的环境变量配置是否生效。go mod tidy
执行成功后会在控制台打印找到的包的版本,并提示正在下载对应的包和所需要的其他包,我的控制台打印如下:
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文件,发现读了一些内容:
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
也可以看出,它的中文含义就是间接的、附带的。此时,我们再重新运行一下程序,对了,tidy的意思是整洁的、整齐的,因此它不光可以下载引用包,对于无用的包它也会进行移除,这点还是很不错的。
go run .
这时会在控制台打印如下内容:
Don't communicate by sharing memory, share memory by communicating.
quote.Go()
这个方法会打印一句Go语言的格言,具体含义大家可以自行搜索理解,它是在使用Go时的一种编程思想,以上是本片入门教程的内容,其实大部分是参照官网的内容进行的,只不过会加入一些自己在实际操作过程中遇到的问题和解决方法,感兴趣的可以自己访问官网好好阅读一下:Tutorial: Get started with Go
在下面的教程中,我们将创建两个模块,第一个是准备让其他库或者应用程序导入的库,也就是它是用来被引用的,第二个模块是使用第一个模块的调用方程序。总体来说我们的目的就是用第二个模块调用第一个模块的方法。
以下是接下来操作的七个简要的说明:
首先创建Go模块。在模块中,我们可以为一组离散(discrete)且有用的函数收集一个或多个相关的(related)包中。例如,我们可以创建一个包含包的模块,这些包具有进行财务分析的功能,以便其他编写财务应用的人可以使用,有关开发模块的更多信息,请参阅开发和发布模块
Go的代码被分组到包(package)中,而包被分组到模块(module)中,你的模块指定运行代码所需要的依赖项,包括Go版本以及所需要的一些其他模块,这一点在上面hello.go的演示说明中有提到过go.mod内容的描述。
当我们在模块中增加或者改进功能时,将发布模块的新版本。下面我们创建一个greetings文件夹用来编写我们的新模块,同样使用向hello.go那样的创建过程
go mod init example/greetings
然后我们在greetings文件夹下创建greetings.go文件,并打开编辑器进行编写代码。
package greetings
import "fmt"
//Hello 方法返回一句向名为name的人的问候语
func Hello(name string) string {
//返回一句问候语,并将name参数嵌入到消息中
message := fmt.Sprintf("Hi, %v. Welcome!", name)
return message
}
上面是这个模块中的第一个代码,它会向调用它的调用者返回一句问候语,上面的代码中我们做了哪些事情?
var message string
message = fmt.Sprintf("Hi, %v. Welcome!", name)
在本教程的最开始我们已经创建了hello模块,下面我们只需要更改hello.go中的代码即可,将代码更改为如下:
package main
import (
"fmt"
"example/greetings"
)
func main() {
message := greetings.Hello("Huhailong")
fmt.Println(message)
}
对于生产使用,我们可以将模块发布到存储库中,Go工具可以从那里找到并下载下来,但目前由于我们没有发布该模块,因此我们需要调整模块,以便它可以在本地系统中找到响应的模块。要做到这一点,我们可以使用go mod edit
命令来实现,它将Go工具从模块路径重定位到本地目录位置。
go mod edit -replace example/greetings=../greetings
上面这段命令是在hello文件夹下运行的,相当于把模块路径example/greetings重新定位到与hello文件夹同级的greetings文件夹目录。运行完上面的代码后hello文件夹下的go.mod文件也发生了变化,内容如下:
module example/hello
go 1.20
replace example/greetings => ../greetings
可以看到,在最后一行多了一行内容,然后我们在hello下运行go mod tidy
命令来同步hello所需的依赖项。出现类似以下打印表明同步依赖项成功
go: found example/greetings in example/greetings v0.0.0-00010101000000-000000000000
后面的v0.0.0-00010101000000-000000000000表示该模块的版本号,此时go.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
然后我们重新运行
go run .
运行结果为:
Hi, Huhailong. Welcome!
处理错误对于一个可靠的代码是一个必不可少的功能,下面我们将在greetings模块中添加一下段代码来返回一个错误,然后在调用方处理这个错误,首先我们修改greetings.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);第二点发生变化的是我们引入了新的标准库模块——“errors”,因为我们要使用它的errors.New()方法;第三点改变是增加了if表达式用来检查请求是否式无效的(这里我们规定如果传递的name参数为空字符串则式无效的请求),并且在确定式无效请求后通过errors模块的New方法返回一个空的消息和error信息;左后如果请求正确则返回问候语和nil,nil表示没有错误。
对应的,在我们的hello.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方法,如果没有发生错误则正常打印问候语,为了显示异常信息,我们把参数设置为了空字符串。下面我们重新运行以下代码
go run .
运行结果如下:
greetings: empty name
exit status 1
到目前为止,我们的greetings模块的Hello方法每次都是返回固定单一的消息,下面我们通过改造greetings.go来实现随机的返回一些问候语,在这里我们要用到Go中的slice,slice就像一个数组,只不过它可以改变大小,动态的增加和删除元素,如果你会Java,它和List是差不多的,slice是Go语言中非常有用的类型之一。关于slice的更多内容请参阅Go slice
下面是我们更改过后的greetings.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.。最后我们重新运行hello.go,运行结果如下:
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来实现这个功能,代码如下:
// 以下是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。在for循环中的操作是对方法接收到的names进行循环,对每一个name为其关联一个message,在for循环中range返回两个值,第一个是当前项在循环中的索引,第二个是对应项的拷贝,因为这里我们不需要索引,因此我们使用Go blank 这里是也就是下划线标识符来忽略它,更多关于Go blank的内容请参阅The blank identifier。
下面我们改变hello.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,再次重新运行代码后结果如下:
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
命令这是一个包含测试方法的文件。**在这个文件中加入以下代码:
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类型的指针作为参数。我们可以i使用此参数的方法从测试中进行报告和日志记录。至于regxp就是Go语言中的正则表达式库,\b 表示单词边界。也就是说,返回的内容左右两边需要有内容。
下面我们就通过 go test
命令来执行测试,使用这个命令是也可以加上-v来输出详细的日志打印。这里的命令是在greetings目录下执行的,注意切换
go test
输出
ok example/greetings 0.538s
使用-v输出详细日志
go test -v
输出
=== RUN TestHelloName
--- PASS: TestHelloName (0.00s)
=== RUN TestHelloEmpty
--- PASS: TestHelloEmpty (0.00s)
PASS
ok example/greetings 0.052s
上面是测试成功的效果,那如果测试失败会怎么样,我们修改以下Hello方法,修改如下:
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的值,接下来再执行测试,看看会发生什么?
go test -v
输出
=== RUN TestHelloName
greetings_test.go:14: Hello("Huhailong") = "Hail, %!v(MISSING)! Well met!", <nil>, 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 的话打包后的可执行文件会存放到指定的位置,在Windows下是 C:\Users\yourname\go\bin 下面,这以为这如果我们把这个路径配置到环境变量中,那么我们每次打包的应用程序可以在不指定文件夹路径的情况下运行。
现在让我们回到hello这个目录下,然后运行go build
命令来将代码编译成可执行文件
go bulid
当我们运行了上面的命令后发现在hello目录下多了一个hello.exe文件(如果是Linux和Mac会生成一个hello的可执行文件,通过./hello就可以运行),然后我们运行它发现输出的结果和使用 go run
返回的一样,这表明我们编译成功了。是的,就是这么简单。
go install
上面已经提到了,go install 会将打包号的可执行文件放到指定的位置,可以通过 go list
命令来查看安装路径
go list -f '{{.Target}}'
至此,Go语言的基础入门就Ok,本文只是我在通过Go官网学习的时候边实操边记录的,建议大家找一个时间比较充裕的时候从头到尾的实操一遍,只有基本入门了以后才可以继续深入的学习。