1-Go - 基础语法
Go语言项目结构
再深入研究hello world
之前,还是要再来说说Go语言的项目结构的。
在进行Go语言开发的时候,我们的代码总是会保存在$GOPATH/src
目录下。在工程经过go build
、go install
或go get
等指令后,会将下载的第三方包源代码文件放在$GOPATH/src
目录下, 产生的二进制可执行文件放在 $GOPATH/bin
目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg
下。
如果我们使用版本管理工具(Version Control System,VCS。常用如Git)来管理我们的项目代码时,我们只需要添加$GOPATH/src
目录的源代码即可。bin
和 pkg
目录的内容无需版本控制。
Go语言中也是通过包来组织代码文件,我们可以引用别人的包也可以发布自己的包,但是为了防止不同包的项目名冲突,我们通常使用顶级域名来作为包名的前缀,这样就不担心项目名冲突的问题了。
但因为不是每个个人开发者都拥有自己的顶级域名,所以目前流行的方式是使用个人的github
用户名来区分不同的包,如下图:
大致对项目结构有了了解之后,并伴随后续学习,会对项目结构有更深的理解。
从hello world说Go语言的代码结构
Go语言的代码结构
现在我们来创建第一个Go语言项目——hello
。在你的GOPATH
路径下的src
目录中创建hello
目录,然后在该hello
目录下新建一个main.go
的文件,写代码:
package main // 声明 main 包,表明当前是一个可执行程序
import "fmt" // 导入内置的 fmt 包
// main 函数是程序的入口
func main() {
// 这是单行注释
/*
这里是多行注释
*/
fmt.Println("hello world!") // 在终端打印 hello world!
}
Go语言代码都是存放在以.go
为后缀名的文件中,每个go语言源代码文件大致组成如上例所示。
- 第一行
package
关键字定义了包名。注意,必须在源代码文件中非注释的第一行指明这个文件属于哪个包。上例的package main
表示这是一个可独立执行的程序,编译后会得到一个可执行文件。 import "fmt"
表示这个程序需要使用fmt
包。func main()
定义了程序执行的入口函数,每一个可执行程序都必须包含main
函数。- 而
//
表示单行注释,以/*
开头并且以*/
结尾的是多行注释,多行注释也叫块注释,多行注释一般用于包的文档描述或者注释成块的代码。另外,块注释不能嵌套使用,而且,注释也是代码中必不可少的一部分。 fmt.Println("hello world!")
是调用fmt
包的Println
函数在终端输出指定的hello world!
,并在最后自动添加换行符\n
。
知道了各行代码的意思,我们就可以在hello
目录内打开终端,执行go build
命令,该命令表示将Go源代码编译成可执行文件,然后你可以在终端中执行生成的可执行文件了。
默认的go build
命令编译生成的二进制文件名和go的代码文件名一致,这里还可以加参数-o
来为编译后生成的二进制文件自定义名称:
// 会在 当前目录下生成 main.exe 可执行文件
D:\MyGo\src\hello❯ go build main.go
// 通过 -o file_name 自定义生成的二进制文件名
D:\MyGo\src\hello❯ go build -o hello.exe main.go
再来补充一个不常用的命令:
// install 命令会将生成的可执行文件存放在项目的bin目录中
D:\MyGo\src\hello❯ go install .\main.go
Go语言语法特点
再来说说Go代码本身的一些特点:
- Go语言自带代码格式化,所以对代码中的缩进并无强制要求。
- Go语言中的左花括号
{
不能单独放在一行,它必须紧跟在其他语句的右侧。 - Go语言中一行代码就表示一个Go语句的结束,不需要在后面添加
;
分隔符。 - Go语言中的逻辑语句必须放在函数中。
- Go语言中不允许存在未使用的变量,如果存在,在编译时会报错。所以,变量一经定义就必须调用。
- Go语言中推荐使用驼峰式命名规则。
- Go语言默认使用
UTF-8
编码。
标识符与关键字
标识符
在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和下划线组成,并且只能以字母或下划线开头。 举几个例子:abc
、 _
、 _123
、 a123
。
关键字
关键字是指编程语言中预先定义好的具有特殊含义的标识符。Go语言中有25个关键字:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
此外,Go语言还有37个保留字:
Constants:
true false iota nil
Types:
int int8 int16 int32 int64 uint
uint8 uint16 uint64 uintptr
float32 float64 complex128 complex128
bool byte rune string error
Functions:
make len cap new append copy close
delete complex real imag panic recover
通常关键字和保留字都不建议用作标识符。
另外,Go对大小写敏感!其次,Go语言中,双引号表示字符串,单引号表示字符。
变量
我们通过一个名字,指向一个人,或者指向一个具体的事物。这在Go中是同样适用。在Go中,这个名字称为变量,而指向的对象为一串字符串,一个具体的数值等等。变量也是可变状态的、对内存地址一种抽象。
变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。
Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。
声明与初始化
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用,否则无法通过编译。
声明变量
标准声明
// var 变量名 变量类型
变量声明以关键字var
开头,变量类型放在变量的后面,行尾无需分号,示例:
package main
import "fmt"
func main() {
var a int // 声明时没有赋值, int:0
var b string // 默认为: ""
fmt.Println(a, b)
}
如上,只有声明,但没有给该变量赋值,打印时会有默认值。
批量声明
每声明一个变量就需要写var
关键字会比较繁琐,go语言中还支持批量变量声明:
var (
a string
b int
c bool
)
其他声明方式
一行声明多个同类型的变量:
var x, y int
初始化变量
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如:
- 整型和浮点型变量的默认值为
0
。 - 字符串变量的默认值为
""
。 - 布尔型变量默认为
false
。 - 切片、函数、指针变量的默认为
nil
。
我们也可以在声明时为其指定初始值:
var 变量名 变量类型 = 表达式
// 示例
var name string = "张开"
var age int = 18
也可以一次声明多个值:
var name, age = "张开", 18
再次强调,同一个作用域内,变量不支持重复声明,否则无法通过编译:
package main
import "fmt"
func main() {
var name = "张开"
var age = 18
var name = "张不开" // 无法通过编译,报错
fmt.Println(name, age)
}
类型推导
有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化。
var name = "张开" // 编译器根据 "张开" 的类型推导 name 的类型是 string
var age = 18
短变量声明
在函数内部,可以使用更简略的 :=
方式声明并初始化变量:
package main
import "fmt"
var name = "张不开" // 在全局可以使用var来声明变量
//name2 := "张开了" // 在全局不可以使用短变量声明
func main() {
name := "张开"
fmt.Println(name)
}
匿名变量
有些情况下,在多个值中,如果想要忽略某个值,可以使用匿名变量(anonymous variable)
。 匿名变量用一个下划线_
表示,例如:
package main
import "fmt"
func main() {
name, _ := "张开", 18
_, age := "张开", 18
fmt.Println(name, age)
}
匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明问题。 (在Lua
等编程语言里,匿名变量也被叫做哑元变量。)
注意事项:
- 函数外的每个语句都必须以关键字开始(
var
、const
、func
等) :=
不能使用在函数外。_
多用于占位,表示忽略值。
常量
声明
相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把var
换成了const
,常量在定义的时候必须赋值:
const pi = 3.1415
const ip = 127.0.0.1
const port = 8080
当变量在声明后,在整个程序运行期间它们的值都不能再发生变化了。
多个常量也可以一起声明:
const(
pi = 3.1415
ip = 127.0.0.1
port = 8080
)
const
同时声明多个常量时,如果省略了值则表示和上面一行的值相同。 例如:
package main
import "fmt"
func main() {
const(
n1 = 100
n2
n3
)
fmt.Println(n1, n2, n3) // 100 100 100
}
iota
iota
是go语言的常量计数器,只能在常量的表达式中使用。
iota
在const
关键字出现时将被重置为0。const
中每新增一行常量声明将使iota
计数一次(iota
可理解为const
语句块中的行索引)。 使用iota
能简化定义,在定义枚举时很有用。
package main
import "fmt"
func main() {
const(
n1 = iota // iota遇到const重置为0,所以 n1 = 0
n2 // 每新增一行常量声明 iota加1,所以 n2 = 1
// 空行不算,因为"每新增一行常量声明,iota加1",空行当然不算了
n3 // 每新增一行常量声明 iota加1,所以 n2 = 2
)
fmt.Println(n1, n2, n3) // 0 1 2
}
再来看几种常见的iota
示例。
使用_
跳过忽略某些值:
package main
import "fmt"
func main() {
const(
n1 = iota // iota遇到const重置为0,所以 n1 = 0
n2 // 每新增一行常量声明 iota加1,所以 n2 = 1
_ // 也算一行声明,只不过这个值我们不要 _ = 2
n3 // 每新增一行常量声明 iota加1,所以 n2 = 3
)
fmt.Println(n1, n2, n3) // 0 1 3
}
一匹黑马强势插入iota
中:
package main
import "fmt"
func main() {
const(
n1 = iota // iota遇到const重置为0,所以 n1 = 0
n2 = 100 // n2 = 100
n3 = iota // iota加1 n3 = 2
n4 // 如果声明时,常量的值被省略,则它的值和上一行的常量值一样 n4 = iota ---> n4 = 3
/*
n4这里理解起来比较麻烦,先看规则,在多个常量声明时,某个常量的值被省略,则它的值和上一行的常量值一样
这里的值指的是:
n3 = iota
n4 // 指的是n4的值跟n3的值一样,但n3的值是iota,而不是iota的值,相当于n4 = iota
*/
)
fmt.Println(n1, n2, n3, n4) // 0 100 2 3
}
iota
加位移运算的使用,1<<10
相当于210,依次类推:
package main
import "fmt"
func main() {
const(
_ = iota // iota = 0
KB = 1 << (10 * iota) // iota = 1; 1<<10 2 **10 ---> 1024
MB = 1 << (10 * iota) // iota = 2; 1<<20 2 ** 20 ---> 1048576
GB = 1 << (10 * iota) // iota = 3; 1<<30 2 ** 30 ---> 1073741824
TB = 1 << (10 * iota) // iota = 4; 1<<40 2 ** 40 ---> 1099511627776
PB = 1 << (10 * iota) // iota = 5; 1<<50 2 ** 50 ---> 1125899906842624
)
fmt.Println(KB, MB, GB, TB, PB) // 1024 1048576 1073741824 1099511627776 1125899906842624
}
多个iota
定义在一行:
package main
import "fmt"
func main() {
const(
a, b = iota + 1, iota + 2 // iota = 0 a = 0 + 1; b = 0 + 2 ---> a = 1; b = 2
c, d // iota = 1 c = 1 + 1; d = 1 + 2 ---> c = 2; d = 3
e, f // iota = 2 c = 2 + 1; d = 2 + 2 ---> c = 3; d = 4
)
fmt.Println(a, b, c, d, e, f) // 1 2 2 3 3 4
}
如果你对上面几个示例不太明白,那你一定要牢记iota
的特性:
- 遇到
const
初始化为0 const
中每新增一行变量声明iota
就递增1const
声明如果不写,默认就和上一行一样
跨平台编译
默认我们go build
的可执行文件都是当前操作系统可执行的文件,如果我想在windows
下编译一个linux
下可执行文件,那需要怎么做呢?
只需要在go build
之前,指定目标操作系统的平台和处理器架构即可:
SET CGO_ENABLED=0 // 禁用CGO
SET GOOS=linux // 目标平台是linux
SET GOARCH=amd64 // 目标处理器架构是amd64
PS:使用了cgo
的代码是不支持跨平台编译的。
然后再执行go build
命令,得到的就是能够在Linux平台运行的可执行文件了。
Mac 下编译 Linux 和 Windows平台 64位 可执行程序:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
Linux 下编译 Mac 和 Windows 平台64位可执行程序:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build
Windows下编译Mac平台64位可执行程序:
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build
批量导包
在之前的示例中,只使用了fmt
这个包来做打印操作,那么如果同时用到多个包该怎么导入呢?跟批量声明变量一样使用括号:
package main
// 在括号内导入多个包
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.MaxFloat64)
}
that's all, see also: