Skip to content

Golang开篇

windows配置

安装包获取

官网下载地址:https://golang.google.cn/dl/

根据自己的系统下载相应包即可,可选择x86或者x86-64,这里演示的是前者,配置都是一样的。

down

安装

下载安装包完成之后,双击msi启动安装程序根据步骤安装即可。

环境配置

使用msi安装程序安装的一般都会自动将环境变量添加到系统中,如果没有添加,手动添加即可。

手动添加环境变量

  1. 右击此电脑,选择属性进入系统信息

    env

  2. 点击高级系统设置

    env

  3. 点击环境变量

    env

  4. 查看用户变量,如果没有找到GOPATH以及Path下也没有go的配置

    新建用户变量,配置如下:

    变量名:GOPATH

    变量值:%USERPROFILE%\go

    点击用户变量下的Path新建变量如下:

    %USERPROFILE%\go\bin

  5. 配置系统变量

    点击系统变量的Path配置自己go的安装目录到bin如下:

    C:\Program Files (x86)\Go\bin

以上配置完成后可以输入以下命令查看版本号来检查go在系统下环境变量是否配置成功。

shell
go version

GO相关配置

我们可以通过go env命令查看go相关配置

查看go环境

配置MODULE

shell
go env -w GO111MODULE=on

配置代理

shell
go env -w GOPROXY=https://proxy.golang.com.cn,https://goproxy.cn,direct

检查配置

shell
go env

检查配置

环境变量说明

  • GOROOT

    用于指定Go语言环境的根目录,也就是Go语言的安装目录。除了Go语言的自带工具外,${GOROOT}/src下所有**.go**文件中定义的函数都可以被导入引用。

  • GOPATH

    用于指定除了GOROOT之外的源码目录。我们同样可以导入并引用${GOPATH}/src下的**.go**文件中定义的函数。

跨平台

Go语言并不支持“一次编译,到处运行”,而是“一次编写,到处运行”。因为Go语言并没有自己的虚拟机,其编译出的可执行文件直接运行在操作系统之上。这在一定程度上牺牲了便利性,但保障了性能。

如何实现跨平台

针对每种操作系统,Go语言需要编译不同版本的可执行文件,如同我们在前面看到的例子,无论是在macOS还是Windows上,都可以直接编译运行Go程序文件。那么Go语言,更具体地说是go build/run命令,是如何做到的呢?

跨平台准备

go build/run命令其实是依赖两个环境变量来识别当前操作系统。我们可以利用go env命令来检查这两个变量的值:

shell
    $ go env GOOS GOARCH
    darwin
    amd64

其中,GOOS代表当前操作系统,GOARCH代表当前CPU架构。当Go安装时,会自动检测操作系统信息,并填充二者的值。因此,即使我们未显式进行任何配置,也可以直接利用go build来编译基于当前操作系统的可执行文件。

表中列举了常见的操作系统及GOOS、GOARCH的值。

可以利用go tool dist list命令来查看Go语言支持的所有GOOS、GOARCH的可选值,例如:

shell
$ go tool dist list
aix/ppc64
android/386
android/amd64
android/arm
android/arm64
darwin/amd64
darwin/arm64
dragonfly/amd64
freebsd/386
freebsd/amd64
freebsd/arm
freebsd/arm64
freebsd/riscv64
illumos/amd64
ios/amd64
ios/arm64
js/wasm
linux/386
linux/amd64
linux/arm
linux/arm64
linux/loong64
linux/mips
linux/mips64
linux/mips64le
linux/mipsle
linux/ppc64
linux/ppc64le
linux/riscv64
linux/s390x
netbsd/386
netbsd/amd64
netbsd/arm
netbsd/arm64
openbsd/386
openbsd/amd64
openbsd/arm
openbsd/arm64
plan9/386
plan9/amd64
plan9/arm
solaris/amd64
wasip1/wasm
windows/386
windows/amd64
windows/arm
windows/arm64

执行跨平台编译

Go语言提供了两个环境变量并允许修改,自然是期望能够在单个操作系统上编译出多个目标操作系统的可执行文件。因此我们是可以通过修改GOOS变量和GOARCH变量后执行go build命令,就可以得到指定操作系统下的可执行文件。

shell
$ export set GOOS=windows GOARCH=amd64
$ echo $GOOS $GOARCH
windows amd64
$ go build main.go
$ ls -l main*
drwxr-xr-x 1 root root 1886999 6 9 10:24 main.exe
drwxr-xr-x 1 root root 	75 6 9 10:20 main.go

编译执行过程

查看编译命令选项

go
$ go help build
-a force rebuilding of packages that are already up-to-date.
-n print the commands but do not run them.
-p n the number of programs, such as build commands or test binaries, that can be run in parallel.The default is GOMAXPROCS, normally the number of CPUs available.
……
-work print the name of the temporary work directory and do not delete it when exiting.
-x rint the commands.

可以看到,go build的选项非常多,其中有3个需要我们特别关注:

  • -n:打印编译的详细过程,但是并不实际执行,相当于预览功能。
  • -x:执行编译,同时也打印编译的详细过程。
  • --work:这是一个追加选项,在go build的编译过程中会生成部分中间目录和文件,编译结束后,会将生成的临时目录和文件删除,--work用于保留生成的临时目录和文件,方便开发者查看和诊断问题。

查看编译详细过程

shell
$ go build -x --work main.go
WORK=C:\Users\l1823\AppData\Local\Temp\go-build2298203633
mkdir -p $WORK\b001\
cat >C:\Users\l1823\AppData\Local\Temp\go-build2298203633\b001\importcfg << 'EOF' # internal
# import config
packagefile fmt=C:\Users\l1823\AppData\Local\go-build\de\de81be052fc2f26175186936611fe152db6a253c5227add5fe9a00fcbabd66b7-d
packagefile runtime=C:\Users\l1823\AppData\Local\go-build\72\72d250ff975a378df59c9a9e086664c6449f19b4877e9c5e8675adda5647ac1a-d
EOF
cd D:\goSpace\demo\main
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\compile.exe" -o "$WORK\\b001\\_pkg_.a" -trimpath "$WORK\\b001=>" -p main -complete -buildid 3AJCTcfdN1APlrwJYbSf/3AJCTcfdN1APlrwJYbSf -goversion go1.21.4 -c=4 -nolocalimports -importcfg "$WORK\\b001\\importcfg" -pack "D:\\goSpace\\demo\\main\\main.go"
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\buildid.exe" -w "$WORK\\b001\\_pkg_.a" # internal
cp "$WORK\\b001\\_pkg_.a" "C:\\Users\\l1823\\AppData\\Local\\go-build\\bb\\bbc3f50557846a45d276ce72e3d10e36c3bead2392a3f4f5f801e29ae496937f-d" # internal
cat >C:\Users\l1823\AppData\Local\Temp\go-build2298203633\b001\importcfg.link << 'EOF' # internal
packagefile command-line-arguments=C:\Users\l1823\AppData\Local\Temp\go-build2298203633\b001\_pkg_.a
packagefile fmt=C:\Users\l1823\AppData\Local\go-build\de\de81be052fc2f26175186936611fe152db6a253c5227add5fe9a00fcbabd66b7-d
……
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nbuild\t-buildmode=exe\nbuild\t-compiler=gc\nbuild\tDefaultGODEBUG=panicnil=1\nbuild\tCGO_ENABLED=0\nbuild\tGOARCH=amd64\nbuild\tGOOS=windows\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK\b001\exe\
cd .
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\link.exe" -o "$WORK\\b001\\exe\\a.out.exe" -importcfg "$WORK\\b001\\importcfg.link" -X=runtime.godebugDefault=panicnil=1 -buildmode=pie -buildid=ZdNkRk6CBZPJY4pNhynM/3AJCTcfdN1APlrwJYbSf/S7F_EWSV4aU1G1sGOO2c/ZdNkRk6CBZPJY4pNhynM -extld=gcc "$WORK\\b001\\_pkg_.a"
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\buildid.exe" -w "$WORK\\b001\\exe\\a.out.exe" # internal
cp $WORK\b001\exe\a.out.exe main.exe

(1)WORK=C:\Users\l1823\AppData\Local\Temp\go-build2298203633:用于指定WORK变量的值。WORK就是go build的工作目录,该目录的前半部分“WORK=C:\Users\l1823\AppData\Local\Temp”来源于操作系统的临时目录;后半部分“go-build2298203633”是编译时自动追加的子文件夹。在Windows操作系统中,该值来源于环境变量TMP,而在macOS中操作系统临时目录利用变量$TMPDIR进行定义。

(2)mkdir -p $WORK\b001\:用于递归创建工作目录。它保证了工作目录一定存在。

(3)cat >C:\Users\l1823\AppData\Local\Temp\go-build2298203633\b001\importcfg << 'EOF' # internal,以及其后的多条packagefile语句,直至EOF:用于生成名为importcfg.link的文件。最后,将所有需要链接的文件(.a文件)写入importcfg.link中。

(4)mkdir -p $WORK\b001\exe\:用于在工作目录下创建一个名为exe的文件夹,该文件夹用于存放生成的可执行文件。

(5)"C:\Program Files\Go\pkg\tool\windows_amd64\link.exe" -o "$WORK\b001\exe\a.out.exe" -importcfg……:该指令执行链接操作,并生成a.out文件

(6)"C:\Program Files\Go\pkg\tool\windows_amd64\buildid.exe" -w "$WORK\b001\exe\a.out.exe" # internal:该指令用于更新a.out文件的build ID。-w选项代表要重写a.out中的build ID。build ID是可执行文件的独一无二的身份标识。

(7)cp $WORK\b001\exe\a.out.exe main.exe:将a.out重命名为main,main文件就是最终生成的可执行文件。

链接

在上述go build的过程中,链接(link)环节尤其值得我们注意。所有的程序实际上都需要与外部其他程序(或代码)进行关联和调用,被调用的代码往往被称为共享库。例如,在Go语言程序中,往往需要引用Go语言内置的函数。实际运行时,这些共享库如何被我们的代码调用就成为一个问题。链接就是为所有相互调用的函数指定入口位置。

链接分为静态链接和动态链接。

  • 静态链接:将使用到的所有共享库复制到最终的可执行文件中。典型的输出文件的形式是单个文件。
  • 动态链接:只将共享库的地址编译到最终的可执行文件中。共享库以独立的文件存在,其最终表现形式往往是多个文件。例如,在Windows中的一个应用程序,往往存在一个.exe文件和多个.dll文件。

因此,静态链接最终的可执行文件往往比较大,而动态链接最终的可执行文件比较小。当然,这并不意味着二者有绝对的优劣之分,只是不同的场合使用不同的链接方式。Go语言在绝大多数情况下都使用静态链接,这一点从前面的编译过程也能看出来——最终文件是单一的、较大的可执行文件。当然,Go语言也支持动态编译,这是因为Go语言也支持对于C语言程序的调用,而C语言程序往往以外部共享库的形式存在。

link完整命令

shell
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\link.exe" -o "$WORK\\b001\\exe\\a.out.exe" -importcfg "$WORK\\b001\\importcfg.link" -X=runtime.godebugDefault=panicnil=1 -buildmode=pie……

其中,命令选项-buildmode=pie指定构建模式为pie。我们可以通过go help build mode命令来查看构建模式的详细信息:

shell
$ go help buildmodes
-buildmode=archive
	// 将列出的主包以及它们导入的所有包构建到Go插件中。未命名为main的包将被忽略。
    Build the listed non-main packages into .a files. Packages named main are ignored.         
-buildmode=c-archive
	// 将列出的主包及其导入的所有包构建到C归档文件中。
	Build the listed main package, plus all packages it imports,into a C archive file. 
	// 唯一可调用的符号将是那些使用cgo导出注释导出的函数。
	The only callable symbols will be those functions exported using a cgo //export comment. 
	//只需要列出一个主程序包。
	Requires exactly one main package to be listed.s
-buildmode=c-shared
	// 将列出的主包及其导入的所有包构建到C共享库中。
    Build the listed main package, plus all packages it imports,into a C shared library. 
    // 唯一可调用的符号是那些使用cgo导出的函数//导出注释。
    The only callable symbols will be those functions exported using a cgo //export comment.
    // 只需要列出一个主程序包。
    Requires exactly one main package to be listed.
-buildmode=default
	// 列出的主程序包内置到可执行文件中,列出的非主程序包则内置到.a文件中(默认行为)。
    Listed main packages are built into executables and listed non-main packages are built into .a files (the default behavior).
-buildmode=shared
	// 将所有列出的非主包组合到一个共享库中,该库将在使用-linkshared选项构建时使用。
    Combine all the listed non-main packages into a single shared library that will be used when building with the -linkshared option. 
    // 名为main的包将被忽略。
    Packages named main are ignored.
-buildmode=exe
	// 构建列出的主包以及它们导入到可执行文件中的所有内容。
    Build the listed main packages and everything they import into executables. 
    // 未命名为main的包将被忽略。
    Packages not named main are ignored.
-buildmode=pie
	// 构建列出的主包以及它们导入到位置独立可执行文件(PIE)中的所有内容。
    Build the listed main packages and everything they import into position independent executables (PIE). 
    // 未命名为main的包将被忽略。
    Packages not named main are ignored.
-buildmode=plugin
	// 将列出的主包以及它们导入的所有包构建到Go插件中。
    Build the listed main packages, plus all packages that they import, into a Go plugin. 
    // 未命名为main的包将被忽略。
    Packages not named main are ignored.

程序入口

说明

许多编程语言在程序入口函数中有参数定义,用于接收启动时的个性化参数。例如,Java语言的参数接收方式有其固定格式:

java
public static void main(String[] args) {
}

其中,参数args是一个String类型的数组。该参数按照顺序接收命令行传递的参数。但是,Go语言使用了完全不同的策略——并没有显式的参数定义(例如前面的main.main()函数的定义中就没有任何参数定义),而是使用固定函数来获取命令行参数。

so.Args变量

os.Args是os包中定义的全局变量,其作用类似于Java语言main()方法中的args参数,其源码定义如下:

go
var Args[] string

该变量是一个string类型的切片。当运行Go程序时,可以增加参数选项,这些参数将会被自动捕获并填充到全局变量os.Args中。如此一来,我们便可以在程序中针对os.Args进行处理。如下代码1-1演示了这一用法。

go
package main

import (
	"fmt"
	"os"
)

func main() {
	for i, arg := range os.Args {
		fmt.Printf("参数%d = %s\n", i, arg)
	}
}

运行结果:

shell
$ go run demo.go a b c
参数0 = C:\Users\l1823\AppData\Local\Temp\go-build2403309614\b001\exe\demo.exe
参数1 = a
参数2 = b
参数3 = c

在输出中,参数0为编译后可执行文件的绝对路径,无论有没有附加参数,参数0都是默认存在的。显式附加的参数a、b、c分别对应了参数1、参数2、参数3。

利用os.Args全局变量可以获得附加参数列表,但是这种形式下,只能通过参数位置进行捕获,这就非常依赖于参数顺序的正确性。

os.Flag()函数

除了os.Args外,还可以利用flag.Int()、flag.Bool()、flag.String()等一系列os.Flag()函数来获得参数。与os.Args依赖位置不同,os.Flag()函数可以按参数名获得参数。如下代码1-2演示了这一用法。

go
package main

import (
	"flag"
	"fmt"
)

func main() {
	// 定义一个名为"intVal"且类型为int的变量占位。解析后会利用命令行参数intVal进行填充。
	var intVal = flag.Int("intVal", 0, "int数据类型")

	// 定义一个名为"boolVal"且类型为bool的变量占位。解析后会利用命令行参数boolVal进行填充。
	var boolVal = flag.Bool("boolVal", false, "bool数据类型")

	// 定义一个名为"stringVal"且类型为string的变量占位。解析后会利用命令行参数stringVal进行填充。
	var stringVal = flag.String("stringVal", "", "string数据类型")
	flag.Parse()
	fmt.Println("-intVal:", *intVal)
	fmt.Println("-boolVal:", *boolVal)
	fmt.Println("-stringVal:", *stringVal)
}

代码解析:

(1)flag.Int("intVal", 0, "int类型参数"):用于获取名为“intVal”的参数。flag.Int()会将参数视作int类型。其中的0代表如果参数未指定,则使用默认值0。

(2)类似地,flag.Bool("boolVal", false, "bool类型参数")和flag.String("stringVal", "", "string类型参数")分别用于获得名为“boolVal”和“stringVal”的参数,其数据类型分别为布尔型和字符串类型。

(3)flag.Parse()是必需的步骤,用于解析以上参数。

如果在未指定任何参数的情况下运行该代码段,将只输出默认值,如下所示。

shell
$ go run .\demo_2.go                                          
-intVal: 0
-boolVal: false
-stringVal:

指定参数的命令格式如下:

shell
go run main.go -argName1 argVal1 -argName2 argVal2 ...

可以利用如下命令执行上面代码,并传递intVal、boolVal和stringVal参数的值:

shell
$ go run .\demo_2.go -intVal 5 -boolVal true -stringVal dev
-intVal: 5
-boolVal: true
-stringVal:

在输出内容中,strintVal的值为空字符,即默认值。这是因为参数-boolVal true虽然在形式上与其他参数保持了一致性,但是对于布尔型参数解析(flag.Bool),单独的“-boolVal”会被识别为一个参数;继续解析时,会尝试将“true”识别为参数名。但是,参数名必须要以“-”开头,所以“true”被识别为非法的参数名,导致解析终止,后续字符串“-stringVal dev”的解析被忽略。

命令行参数的最优写法

Go程序指定命令行参数时有以下4种形式可供选择:

shell
-arg value
--arg value
-arg=value
--arg=value

但是布尔类型比较特殊,只以下支持两种形式:

shell
-arg
-arg=value

因此,为了统一格式和避免不必要的问题,在使用命令行传递参数时,推荐使用-arg=value。调用代码1-2的命令,使用如下形式是最佳选择:

shell
$ go run .\demo_2.go -intVal=5 -boolVal=true -stringVal=dev
-intVal: 5
-boolVal: true
-stringVal: dev

小结

本文利用一个最简单的实例介绍了如何编写、编译、运行一个Go语言程序。接着重点介绍了一些隐含的细节,例如,环境变量的作用、Go语言如何为环境变量提供默认值等。在日常开发中,IDE仍然是最高效的开发工具,因此,我们详细讲解了如何在GoLand中创建和运行第一个Go程序。除此之外,我们通过编译细节的讲解,解释了Go语言如何实现跨平台支持,以及编译时的主要可选项。当出现编译问题时,可以通过编译的详细日志来查找问题原因。