以简单的hello_world.go为例
package main
import "fmt"
func main() {
fmt.Println("hello, world")
}
包是结构化代码的一种方式,一个程序可以有很多个包构成,我们可以从自身或者其他包里面导入内容。每个Go文件都属于且仅属于一个包,一个包可以有多个.go
文件,每个Go文件必须在源文件的非注释第一行指明该源文件属于哪个包,类似package main
,package main
表示一个可独立运行的程序,每个Go程序都必须要有一个包名为main
的入口。其实包的概念和其他程序语言的类库或命名空间概念相似,如果了解其他程序语言的话,理解起来就比较方便。
一个应用程序可以包含不同的包,而且即使你只使用 main 包也不必把所有的代码都写在一个巨大的文件里:你可以用一些较小的文件,并且在每个文件非注释的第一行都使用 package main 来指明这些文件都属于 main 包。如果你打算编译包名不是为 main 的源文件,如 pack1,编译后产生的对象文件将会是 pack1.a 而不是可执行程序。另外要注意的是,所有的包名都应该使用小写字母。
Go程序在安装好后,会自带一些可以直接使用的包,即标准库,在 Windows 下,标准库的位置在 Go 根目录下的子目录 pkg\windows_386 中;在 Linux 下,标准库在 Go 根目录下的子目录 pkg\linux_amd64 中(如果是安装的是 32 位,则在 linux_386 目录中)。一般情况下,标准包会存放在 $GOROOT/pkg/$GOOS_$GOARCH/ 目录下。
Go的标准库中包含了大量的包,(如:fmt
和os
等),我们也可以自己创建自定义包,创建自定义包后可以参考( 2.4 )章节编译安装到环境中。
接下来说下程序的构建顺序,包和包内的文件必须以正确的顺序进行编译,包的依赖关系决定了其编译构建顺序。通常情况下,一个目录只包含一个包,属于同一个包的源文件必须全部一起编译,一个包即一个单元,
如果对一个包进行更改,那所有引用这个包的程序都需要重新进行编译。
Go 中的包模型采用了显式依赖关系的机制来达到快速编译的目的,编译器会从后缀名为 .o 的对象文件(需要且只需要这个文件)中提取传递依赖类型的信息。
我们用例子来解释下编译顺序的原理:
假如A.go
引用了B.go
,而B.go
又引用了C.go
,
- 编译顺序应该是先编译
C.go
,然后是B.go
,最后编译的是A.go
。 - 编译
A.go
的时候读取的不是C.go
编译文件C.o
,而是读取B.go
的编译文件B.o
。
这种机制对于编译大型的项目时可以显著地提升编译速度。也就是说 每一段代码只会被编译一次,如果C.go
没有被修改,重新编译A.go
的时候只需要调用之前编译好的B.o
文件即可,不需要重复去编译B.go
源文件。
说完包的概念,接下来说说导入。
一个Go 程序是通过import
关键字将一组包连接在一起。
import "fmt"
是告诉编译器这个程序需要使用到fmt
包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。如果你打算从已编译的包中导入并加载公开声明的方法,不需要插入已编译包的源代码。
导入的语法有很多种,建议使用第三种
第一种
import "fmt"
import "os"
第二种
import "fmt"; import "os"
第三种
import (
"fmt"
"os"
)
导入包的时候建议按照字母顺序进行导入,这样做更加清晰易读
如果包名不是以 . 或 / 开头,如 "fmt" 或者 "container/list",则 Go 会在全局文件进行查找;如果包名以 ./ 开头,则 Go 会在相对目录中查找;如果包名以 / 开头(在 Windows 下也可以这样使用),则会在系统的绝对路径中查找。
一般导入的包都是本地标准库或者线上库的包,如果我们要导入本地非标准库中的包要如何导入?这边分本地包在同一项目和不同一项目进行举例:
.
├── pack1
│ └── pack1.go
├── go.mod
└── main.go
对于这种情况,从项目根目录到包名即可
package main
import (
"fmt"
"project/pack1"
)
本地包不在同一项目也可以理解为要导入其他项目的 Go 文件,比如我在main.go中导入pack2/pack2.go文件。
.
├── pack2
│ └── pack2.go
└── project
├── pack1
│ └── pack1.go
├── go.mod
└── main.go
首先需要在pack2项目中通过go mod init pack2创建依赖工具
module pack2Demo
go 1.19
接着修改project项目的依赖工具
module project
go 1.19
require (
pack2Demo v0.0.0
)
replace pack2Demo => ../pack2
不同项目导入主要用到require和replace关键字,require是声明引入的包名以及版本,replace是用来替换包的指向路径
导入包即等同于包含了这个包的所有的代码对象。
除了符号 _,包中所有代码对象的标识符必须是唯一的,以避免名称冲突。但是相同的标识符可以在不同的包中使用,因为可以使用包名来区分它们。
包通过下面这个被编译器强制执行的规则来决定是否将自身的代码对象暴露给外部文件,
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象经过 import
导入包后,就可以被外部包的代码所使用,类似其他面向对象语言中的public
;如果标识符为小写开头,如:group2,这种对于外部包来说是不可见的,只能原本包内可见和可以使用的,类似其他面向对象语言中的private
。
大写字母可以使用任何 Unicode 编码的字符,比如希腊文,不仅仅是 ASCII 码中的大写字母)。
用一个简单的小例子来说明下,比如pack1包中有个变量或函数叫A,还有一个b
,当外部包import pack1
之后,我们就可以通过 pack1.A
来调用这个public的变量或函数,而b
只能在pack1中可见并使用,外部包是不可见并无法使用的。
在使用其他包的变量或函数时,包名是不可以省略的,包名也可以当做命名空间使用,可以避免命名命名冲突:两个变量的区别在于他们的包名,例如:pack1.A
和 pack2.A
.
另外,Go 也支持使用包的别名来解决包名之间的名称冲突,如:import fm "fmt"
例子:
package main
import fm "fmt" // alias
func main() {
fm.Println("hello, world")
}
func functionName(){
}
这是一个简单的函数格式,你可以在()
内写0或多个参数,参数名称后面必须紧跟着该参数的类型,多个参数之间使用 ,
逗号隔开。
main()
函数时每个可执行程序必须有的函数,如果没有init()
函数的话,main()
函数是程序启动后第一个执行的入口,如果main
包的源代码没有包含 main()
函数,则会引发构建错误 undefined: main.main
,main() 函数既没有参数,也没有返回类型(与 C 家族中的其它语言恰好相反)。如果你不小心为 main() 函数添加了参数或者返回类型,将会引发构建错误:
func main must have no arguments and no return values results.
函数里的代码(函数体)使用大括号 {}
括起来。
左大括号 {
必须与方法的声明放在同一行,这是编译器的强制规定,否则你在使用 gofmt 时就会出现错误提示:
`build-error: syntax error: unexpected semicolon or newline before {`
(这是因为编译器会产生 func main();
这样的结果,很明显这是错误的)
Go 语言虽然看起来不使用分号作为语句的结束,但实际上这一过程是由编译器自动完成,因此才会引发像上面这样的错误
右大括号 } 需要被放在紧接着函数体的下一行。如果你的函数非常简短,你也可以将它们放在同一行:
func Sum(a, b int) int { return a + b }
对于大括号 {} 的使用规则在任何时候都是相同的(如:if 语句等)。
因此符合规范的函数一般写成如下的形式:
func functionName(parameter_list) (return_value_list) {
…
}
其中:
- parameter_list 的形式为 (param1 type1, param2 type2, …)
- return_value_list 的形式为 (ret1 type1, ret2 type2, …)
只有当某个函数需要被外部包调用的时候才使用大写字母开头,并遵循 Pascal 命名法;否则就遵循骆驼命名法,即第一个单词的首字母小写,其余单词的首字母大写。
下面这一行调用了 fmt 包中的 Println 函数,可以将字符串输出到控制台,并在最后自动增加换行字符 \n:
fmt.Println("hello, world")
使用 fmt.Print("hello, world\n") 可以得到相同的结果。
当被调用函数的代码执行到结束符 } 或返回语句时就会返回,然后程序继续执行调用该函数之后的代码。
程序正常退出的代码为 0 即 Program exited with code 0;如果程序因为异常而被终止,则会返回非零值,如:1。这个数值可以用来测试是否成功执行一个程序。
注释不会被编译,但可以通过 godoc 来使用,通过godoc工具,自动获取每个注释并生成对应的文档
单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。
多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
每一个包应该有相关注释,在 package 语句之前的块注释将被默认认为是这个包的文档说明,其中应该提供一些相关信息并对整体功能做简要的介绍。一个包可以分散在多个文件中,但是只需要在其中一个进行注释说明即可。当开发人员需要了解包的一些情况时,自然会用 godoc 来显示包的文档说明,在首行的简要注释之后可以用成段的注释来进行更详细的说明,而不必拥挤在一起。另外,在多段注释之间应以空行分隔加以区分。
示例:
// Package superman implements methods for saving the world.
//
// Experience has shown that a small number of procedures can prove
// helpful when attempting to save the world.
package superman
几乎所有全局作用域的类型、常量、变量、函数和被导出的对象都应该有一个合理的注释。如果这种注释(称为文档注释)出现在函数前面,例如函数 Abcd,则要以 "Abcd..." 作为开头。 示例:
// enterOrbit causes Superman to fly into low Earth orbit, a position
// that presents several possibilities for planet salvation.
func enterOrbit() error {
...
}
变量(或常量)包含数据,这些数据可以有不同的数据类型,简称类型。使用 var 声明的变量的值会自动初始化为该类型的零值。类型定义了某个变量的值的集合与可对其进行操作的集合。
类型可以是基本类型,如:int、float、bool、string;结构化的(复合的),如:struct、array、切片 (slice)、map、通道 (channel);只描述类型的行为的,如:interface。
结构化的类型没有真正的值,它使用 nil 作为默认值(在 Objective-C 中是 nil,在 Java 中是 null,在 C 和 C++ 中是 NULL 或 0)。值得注意的是,Go 语言中不存在类型继承。
函数也可以是一个确定的类型,就是以函数作为返回类型。这种类型的声明要写在函数名和可选的参数列表之后,例如:
func FunctionName (a typea, b typeb) typeFunc
你可以在函数体中的某处返回使用类型为 typeFunc 的变量 var:
return var
一个函数可以拥有多返回值,返回类型之间需要使用逗号分割,并使用小括号 () 将它们括起来,如:
func FunctionName (a typea, b typeb) (t1 type1, t2 type2)
返回的形式:
return var1, var2
除了返回Go内置类型外,还可以使用type
关键字自定义你自己的类型,可以是一个结构体,或者是一个已经存在的类型,例如:
type IZ int
这里并不是真正意义上的别名,因为使用这种方法定义之后的类型可以拥有更多的特性,且在类型转换时必须显式转换。 然后我们可以使用下面的方式声明变量:
var a IZ = 5
这里我们可以看到 int 是变量 a 的底层类型,这也使得它们之间存在相互转换的可能 多个类型需要定义,可以使用因式分解的方式进行定义,如:
type (
IZ int
FZ float64
STR string
)
每个值都必须在经过编译后属于某个类型(编译器必须能够推断出所有值的类型),因为 Go 语言是一种静态类型语言。
Go的结构虽然没有强制要求,编译器也不关心main函数在前还是变量在前,但是一个好的统一的结构可以使你阅读或者维护代码更为友好。
Go的一般结构思路如下:
- import 导入包要保持在最前面。
- 在完成包的 import 之后,开始对全局常量、变量和类型的定义或声明。
- 如果存在 init() 函数的话,则对该函数进行定义(这是一个特殊的函数,每个含有该函数的包都会首先执行这个函数)。
- 如果当前包是 main 包,则定义 main() 函数。
- 然后定义其余的函数,首先是类型的方法,接着是按照 main() 函数中先后调用的顺序来定义相关函数,如果有很多函数,则可以按照字母顺序来进行排序。
例如:
package main
import (
"fmt"
)
const c = "C"
var v int = 5
type T struct{}
func init() { // initialization of package
}
func main() {
var a int
Func1()
// ...
fmt.Println(a)
}
func (t T) Method1() {
//...
}
func Func1() { // exported function Func1
//...
}
Go 程序的执行(程序启动)顺序如下:
- 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
- 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
- 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init() 函数的话,则调用该函数。
- 在完成这一切之后,main 也执行同样的过程,最后调用 main() 函数开始执行程序。
在必要可行的情况下,一个类型的值可以转换为另一个类型的值,但是Go语言不支持隐式转换,所以所有的类型转换都必须显示说明。 例如:
valueOfTypeB = typeB(valueOfTypeA)
类型 B 的值 = 类型 B(类型 A 的值)
a := 5.0
b := int(a)
这种转换只能在定义正确的情况下转换成功,比如从一个取值范围较小的值转为取值范围较大的值(int16 转为int32)。如果是从取值范围较大的值转为范围较小的值,则会出现精度丢失的情况(例如将 int32 转换为 int16 或将 float32 转换为 int),当编译器捕捉到非法的类型转换时会引发编译时错误,否则将引发运行时错误。
具有相同底层类型的变量之间可以相互转换:
var a IZ = 5
c := int(a)
d := IZ(c)
干净、可读的代码和简洁性是 Go 追求的主要目标。通过 gofmt 来强制实现统一的代码风格。Go 语言中对象的命名也应该是简洁且有意义的。像 Java 和 Python 中那样使用混合着大小写和下划线的冗长的名称会严重降低代码的可读性。名称不需要指出自己所属的包,因为在调用的时候会使用包名作为限定符。返回某个对象的函数或方法的名称一般都是使用名词,没有 Get... 之类的字符,如果是用于修改某个对象,则使用 SetName()。有必须要的话可以使用大小写混合的方式,如 MixedCaps() 或 mixedCaps(),而不是使用下划线来分割多个名称。
- 目录
- 上一页 3.1 Go 的文件名、关键字和标识符
- 下一页 3.3 常量和变量