1.2 开启Go的第一个程序
1.2-helloWorld.go
package main
import "fmt"
func main(){
fmt.Println("Hello World~")
}
在源文件所在目录输入如下命令:
$go run 1.2-helloWorld.go
输出如下:
Hello World~
也可以运行“go build”命令编译:
go build 1.2-helloWorld.go
编译成功后,运行如下命令:
$./1.2-helloWorld
helloWorld~
通过上面Go语言的第一个程序可以看到,Go语言程序的结构非常简单,只需要短短几行代码就能跑起来。
1. 包的声明
Go语言以“包”作为程序项目的管理单位。如果要正常运行Go语言的源文件,则必须先声明它所属的包。每一个Go源文件的开头都是一个package
声明,格式如下:
package xxx
其中,package
是声明包名的关键字,xxx
是包的名字。
一般来说,Go语言的包与源文件所在文件夹有一一对应的关系。
Go语言的包具有以下几点特性:
- 一个目录下的同级文件属于同一个包。
- 包名可以与其目录名不同。
-
main
包是Go语言应用程序的入口包。一个Go语言应用程序必须有且仅有一个main
包。如果一个程序没有main
包,则编译时将会报错,无法生成可执行文件。
2.包的导入
在声明了包之后,如果需要调用其他包的变量或者方法,则需要使用import
语句。import
语句用于导入程序中所以来的包,导入的包名使用英文双引号(""
)包围,格式如下:
import "package_name"
其中,import
是导入包的关键字,package_name
是所导入包的名字。例如,代码1.2-helloWorld.go
程序中的import "fmt"
语句表示导入了fmt
包,这行代码会告诉Go编译器——我们需要用到fmt
包中的变量或者函数等。
fmt包是Go语言标准库为我们提供的、用于格式化输入输出的内容,在开发调试的过程中会经常用到。
在实际编码中,为了看起来直观,一般会在package
和import
之间空一行。当然这个空行不是必需的,有没有都不影响程序执行。
在导入包的过程中要注意:如果导入的包没有被使用,则Go编译器会报编译错误。在实际编码中,集成开发环境(Integrated Development Environment, IDE
)类编辑器(比如Goland
等)会自动提示哪些包没有被使用,并自动提示没有使用的import
语句。
可以用一个import
关键字同时导入多个包。此时需要用括号“()
”将包的名字包围起来,并且每个包名占用一行,形式如下:
import(
"os"
"fmt"
)
也可以给导入的包设置自定义别名,形式如下:
import(
alias1 "os"
alias2 "fmt"
)
这样就可以用别名“alias1
”来代替os
,用别名“alias2
”来代替fmt
了。
如果只想初始化某个包,不适用导入包中的变量或者函数,则可以直接以下画线(_
)代替别名:
import(
_ "os"
alias2 "fmt"
)
如果已经用下画线(
_
)代替了别名,继续再调用这个包,则会再编译时返回形如“undefined: 包名
”的错误。比如上面这段代码再编译时会返回“undefined:os
”的错误。
3.main()函数
代码1.2-helloWorld.go
中的func main()
就是一个main()
函数。main()
函数时Go语言应用程序的入口函数。main()
函数只能声明再main
包中,不能声明再其他包中,并且一个main
包中必须有且仅有一个main()
函数。这和C/C++
类似,一个程序有且只能有一个main()
函数。main()
函数是自定义函数的一种,在Go语言中,所有函数都是以关键字func
开头的。定义格式如下所示:
func 函数名 (参数类别) (返回值列表){
函数体
}
具体说明如下。
- 函数名:由字母、数字、下画线(
_
)组成。其中第1个字母不能为数字,并且在同一个包内函数名称不能重复。 - 参数列表:一个参数由参数变量和参数类型组成,例如
func foo(name string, age int)
。 - 返回值列表:可以是返回值类型列表,也可以是参数列表那样的变量名与类型的组合列表。函数有返回值时,必须在函数体中使用
return
语句返回。 - 函数体:函数体是用大括号“
{}
”括起来的若干语句,它们完成了一个函数的具体功能。
Go语言函数的左大括号“
{
”必须和函数名称在同一行,否则会报错。
fmt.Println("Hello World~")
中,Println()
是fmt
包中的一个函数,用于格式化输出数据,比如字符串、整数、小数等,类似于C语言中的printf()
函数。这里使用Println()
函数来打印字符串(即()
里面使用双引号""
包裹的部分)。
Println()
函数打印完成后会自动换行。ln
是line
的缩写。
和Java
类似,每一行语句的结尾处不需要英文分号“;
”来作为结束符,Go编译器会自动添加,当然也可以加上分号。
1.3 Go基础语法与使用
1.3.1 基础语法
1. Go语言编辑
Go语言由关键字、标识符、常量、字符串、符号等多种标记组成。
2. 行分隔符
在Go程序中,一般来说一行就是一个语句,不用像Java
、PHP
等其他语言那样需要在一行的最后用英文分号(;
)结尾,因为这些工作都将由Go编译器自动完成。但如果多个语句写在同一行,则必须使用分号(;
)将它们分隔开。但在实际开发中并不鼓励这种写法。
如下的写法是两个语句:
fmt.Println("Hello, Let's Go!")
fmt.Println("Go Web编成实战派从入门到精通")
3. 注释
在Go程序中,注释分为单行注释和多行注释。
(1)单行注释。单行注释是最常见的注释形式,以双斜线“//
”开头的单行注释,可以在任何地方使用。形如:
// 单行注释
(2)多行注释。也被称为“块注释”,通常以“/*
”开头,并以“*/
”结尾。形如:
/*
多行注释
多行注释
*/
4. 标识符
标识符通常用来对变量、类型等程序实体进行命名。一个标识符实际上就是一个或是多个字母(A~Z
和a~z
)、数字(0~9
)、下画线(_
)组成的字符串序列。第1个字符不能是数字或Go程序的关键字。
以下是正确命名的标识符:
product user add user_name abc_123
resultValue name1 _tmp k
以下是错误命名的标识符:
switch (错误命名:Go语言的关键字)
3ab(错误命名:以数字开头)
c-d(错误命名:运算符不允许)
5. 字符串连接
Go语言的字符串可以通过“+
”号实现字符串连接,示例如下:
代码 1.3-goWeb.go
package main
import "fmt"
func main(){
fmt.Println("Go Web编程实战派" + "————从入门到精通")
}
6. 关键字
在Go语言中有25个关键字或保留字。
- | - | - | - | - |
contimue | for | import | return | var |
const | fallthrough | if | range | type |
chan | else | goto | package | switch |
case | defer | go | map | struct |
break | default | func | interface | select |
最新版本的Go语言中有30多个预定义标识符,可以分为以下三类:
(1)常量相关预定义标识符:true
、false
、iota
、nil
。
(2)类型相关预定义标识符:int
、int8
、int16
、int32
、int64
、uint
、uint8
、uint16
、uint32
、uint64
、uintptr
、float32
、float64
、complex128
、complex64
、bool
、byte
、rune
、string
、error
。
(3)函数相关预定义标识符:make
、len
、cap
、new
、append
、copy
、close
、delete
、complex
、real
、imag
、panic
、recover
。
7. Go语言的空格
在Go语言中,变量的声明必须使用空格隔开,如:
var name string
在函数体语句中,适当使用空格能让代码更易阅读。如下语句无空格,看起来不直观:
name=shirdon+liao
在变量与运算符间加入空格,可以让代码看起来更加直观,如:
name = shirdon + liao
1.3.2 变量
1. 声明
变量来源于数学,是计算机语言中存储计算结果或表示值的抽象概念。
在数学中,变量表示没有固定值且可改变的数。但从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
Go语言是静态类型语言,因此变量(variable
)是有明确类型的,编译器也会检查变量类型的正确性。声明变量一般使用var
关键字:
var name type
其中,var
是声明变量的关键字,name
是变量名,type
是变量的类型。
和许多其他编成语言不同,Go语言在声明变量时需将变量的类型放在变量的名词之后。
例如在Go语言中声明整型指针类型的变量,格式如下:
var c, d *int
当一个变量被声明后,系统自动赋予它该类型的零值或空值:例如int
类型为0,float
类型为0.0
,bool
类型为false
,string
类型为空字符串,指针类型为nil
等。
变量的命名规则遵循”骆驼“命名法,即首个单词小写,每个新单词的首字母大写,例如:stockCount
和totalPrice
。当然,命名规则不是强制性的,开发这可以按照自己的习惯指定自己的命名规则。
变量的声明形式可以分为标准格式、批量格式、简短格式这3种形式。
(1)标准格式。
Go语言变量声明的标准格式如下:
var 变量名 变量类型
变量声明以关键字var
开头,中间是变量名,后面是变量类型,行尾无须有分号。
(2)批量格式。
Go语言还提供了一个更加高效的批量声明变量的方法——使用关键字var
和括号将一组变量定义放在一起,形如下方代码:
var(
age int
name string
balance float32
)
(3)简短格式。
除var
关键字外,还可使用更加简短的变量定义和初始化语法,格式如下:
名字 := 表达式
需要注意的是,简短模式(short variable declaration
)有以下限制:
- 只能用来定义变量,同时会显示初始化。
- 不能提供数据类型。
- 只能用在函数内部,即不能用来声明全局变量。
和var
形式声明语句一样,简短格式变量声明语句也可以用来声明和初始化一组变量:
name, goodAt := "Shirdon", "Programming"
因为具有简洁和灵活的特点,简短格式变量声明被广泛用于局部变量的声明的初始化。var
形式的声明语句往往用于需要显式指定变量类型的地方,或者用于声明在初始值不太重要的变量。
2.赋值
(1)给单个变量赋值。
给变量赋值的标准方式为:
var 变量名 [类型] = 变量值
这时如果不想声明变量类型,可以忽略,编译器会自动识别变量值的类型。例如:
var language string = "Go"
var language = "Go"
language := "Go"
以上3种方式都可以进行变量的声明。
(2)给多个变量赋值。
给多个变量赋值的标准方式为:
var (
变量名1 (变量类型1) = 变量值1
变量名2 (变量类型2) = 变量值2
// ...省略多种变量
)
或者,多个变量和变量值在同一行,中间用英文逗号“,
”隔开,形如:
var 变量名1, 变量名2, 变量名3 = 变量值1, 变量值2, 变量值3
例如,声明一个用户的年龄(age)、名字(name)、余额(balance),可以通过如下方式批量赋值:
var (
age int = 18
name string = "shirdon"
balance float32 = 999999.99
)
或者另外一种形式:
var age, name, balance = 18, "shirdon", 999999.99
最简单的形式是:
age, name, balance := 18, "shirdon", 999999.99
以上三者是等价的。当交换两个变量时,可以直接采用如下格式:
d, c := "D", "C"
c, d = d, c
3. 变量的作用域
Go语言中的变量可以分为局部变量和全局变量。
(1)局部变量。
在函数体内声明的变量被称为“局部变量”,他们的作用域只在函数体内,参数和返回值变量也是局部变量。以下示例main()
函数使用了局部变量local1
、local2
、local3
。
代码 chapter1/1.3-varScope1.go 局部变量声明的示例
package main
import "fmt"
func main(){
// 声明局部变量
var local1, local2, local3 int
// 初始化参数
local1 = 8
local2 = 10
local3 = local1 * local2
fmt.Printf("local1 = %d, local2 = %d and local3 = %d\n", local1, local2, local3)
}
以上代码的运行结果如下:
local1 = 8, local2 = 10 and local3 = 80
(2)全局变量。
在函数体外声明的变量被称为“全局变量”。全局变量可以在整个包甚至外部包(被到处后)中使用,也可以在任何函数中使用。
代码 chapter1/1.3-varScope2.go 全局变量声明及使用示例
package main
import "fmt"
// 声明全局变量
var global int
func main(){
// 声明局部变量
var local1, local2 int
// 初始化参数
local1 = 8
local2 = 10
global = local1 * local2
fmt.Printf("local1 = %d, local2 = %d and g = %d\n", local1, local2, global)
}
以上代码的运行结果如下:
local1 = 8, local2 = 10 and g = 80
在Go语言应用程序中,全局变量与局部变量名词可以相同,但是函数内的局部变量会被优先考虑,示例如下:
代码 chapter1/1.3-varScope3.go 全局变量与局部变量的声明
package main
import "fmt"
// 声明全局变量
var global int = 8
func main(){
// 声明局部变量
var global int = 999
fmt.Printf("global = %d\n", global)
}
以上代码的运行结果如下:
global = 999
1.3.3 常量
1. 常量的声明
Go语言的常量使用关键字const
声明。常量用于存储不会改变的数据。常量是在编译时被创建的,即使声明在函数内部也是如此,并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。由于编译时有限制,声明常量的表达式必须为“能被编译器求值的常量表达式”。
常量的声明格式和变量的声明格式类似,如下:
const 常量名 [类型] = 常量值
例如,声明一个常量pi
的方法如下:
const pi = 3.14159
在Go语言中,可以省略类型说明符“[类型]”,因为编译器可以根据变量的值来推断其类型。
- 显式类型声明:
const e float32 = 2.7182818
- 隐式类型声明:
const e = 2.7182818
常量的值必须是能够在编译时可被确定的,可以在其赋值表达式中涉及计算过程,但是所有用于计算的值必须在编译期间就能获得。
- 正确的做法:
const c1 = 5/2
- 错误的做法:
const url = os.GetEnv("url")
上面这个声明会导致编译报错,因为os.GetEnv("url")
只有在运行期才能知道返回结果,在编译期并不能知道结果,所以无法作为常量声明的值。
可以批量声明多个变量:
const(
e = 2.7182818
pi = 3.1415926
)
所有常量的运算实在编译期间完成的,这样不仅可以减少运行时的工作量,也可以方便其他代码的编译优化。当被操作的数是常量时,一些运行时的错误也可以在编译时被发现,例如整数除零、字符串索引越界、任何导致无效浮点数的操作等。
常量间所有算数运行、逻辑运算和比较运算的结果也是常量。对常量进行类型转换,或对len()
、cap()
、real()
、imag()
、complex()
和unsafe.Sizeof()
等函数进行调用,都返回常量结果。因为它们的值在编译期就是确定的,因此常量可以构成类型的一部分,例如用于指定数字类型的长度。如下示例常量IPv4Len
来指定数组p
的长度:
const IPv4Len = 4
// parseIPv4解析一个IP v4地址(addr.addr.addr.addr)。
func parseIpv4(s string) IP {
var p [IPv4Len]byte
// ...
}
2. 常量生成器iota
常量声明可以使用常量生成器iota
初始化。iota
用于生成一组以相似规则初始化的常量,但是不用每行都写一边初始化表达式。
在一个const
声明语句中,在第1个声明的常量所在的行,iota
会被置0,之后的每一个有常量声明的行会被加1。
例如我们常用的东南西北4个方向,可以首先定义一个Direction
命名类型,然后为东南西北各定义一个常量,从北方0开始。在其他编程语言中,这种类型一般被称为”枚举类型“。
在Go语言中,iota
的用法如下:
type Direction int
const(
North Direction = iota
East
South
West
)
以上声明中,North的值为0、East的值为1,其余以此类推。
3. 延迟明确常量的具体类型
Go语言的常量有一个不同寻常指出:虽然一个常量可以有任意一个确定的基础类型(例如int
或float64
,或者是类似time.Duration
这样的基础类型),但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量,提供比基础类型更高精度的算数运算。
Go语言有6种未明确类型的常量类型:无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
延迟明确常量的具体类型,不仅可以提供更高的运算精度,还可以直接用于更多的表达式而不需要显式的类型转换。
例如,无类型的浮点数常量math.Pi
,可以直接用于任何需要浮点数或复数的地方:
var a float32 = math.Pi
var b float64 = math.Pi
var c complex128 = math.Pi
如果math.Pi
被确定未特定类型(比如float64
),则结果精度可能会不一样。同时在需要float32
或complex128
类型值得地方,需要对其进行一个明确得强制类型转换:
const Pi64 float64 = math.Pi
var a float32 = float32(Pi64)
var b float64 = Pi64
var c complex128 = complex128(Pi64)
对于常量面值,不同的写法会对应不同得类型。例如0
、0.0
、0i
和\u0000
虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true
和false
也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
1.3.4 运算符
运算符是用来在程序运行时执行数学运算或逻辑运算的符号。在Go语言中,一个表达式可以包含多个运算符。如果表达式中存在多个运算符,则会遇到优先级的问题。这个就由Go语言运算符的优先级来决定。
比如表达式:
var a, b, c int = 3, 6, 9
d := a + b * c
优先级 | 分类 | 运算符 | 结合性 |
1 | 逗号运算符 |
| 从左到右 |
2 | 赋值运算符 |
| 从右到左 |
3 | 逻辑”或“ |
| 从左到右 |
4 | 逻辑”与“ |
| 从左到右 |
5 | 按位”或“ |
| 从左到右 |
6 | 按位”异或“ |
| 从左到右 |
7 | 按位”与“ |
| 从左到右 |
8 | 相等/不等 |
| 从左到右 |
9 | 关系运算符 |
| 从左到右 |
10 | 位移运算符 |
| 从左到右 |
11 | 加法/减法 |
| 从左到右 |
12 | 乘法/除法/取余 |
| 从左到右 |
13 | 单目运算符 |
| 从右到左 |
14 | 后缀运算符 |
| 从左到右 |
1.3.5 流程控制语句
1. if-else
(分支结构)
在Go语言中,关键字if
用于判断某个条件(布尔型或逻辑型)。如果该条件成立,则会执行if
后面由大括号{}
括起来的代码块,否则就忽略该代码块继续执行后续的代码。
if b > 10 {
return 1
}
如果存在第2个分支,则可以在上面代码的基础上添加else
关键字及另一代码块,见下方代码。这个代码块中的代码只有在if
条件不满足时才会执行。if{}
和else{}
中的两个代码块是相互独立的分支,两者只能执行其中一个。
if b > 10 {
return 1
}else{
return 2
}
如果存在第3分支,则可以使用下面这种3个独立分支的形式:
if b > 10 {
return 1
}else if b == 10{
return 2
}else{
return 3
}
一般来说,else-if
分支的数量是没有限制的。但是为了代码的可读性,最好不要在if
后面加入太多的else-if
结构。如果必须使用这种形式,则尽可能把先满足的条件放在前面。
关键字if
和else
之后的左大括号{
必须和关键字在同一行。如果使用了else-if
结构,则前段代码块的右大括号}
必须和else if
语句在同一行。这两条规则都是被编译器强制规定的,如果不满足,则编译不能通过。
2. for
循环
与多数语言不同的是,Go语言种的循环语句只支持for
关键字,不支持while
和do-while
结构。
product := 1
for i := 1; i < 5; i++{
product *= i
}
无限循环场景:
i := 0
for {
i++
if i > 50 {
break
}
}
在使用循环语句时,需要注意以下几点:
- 左花括号
{
必须与for
处于同一行。 - Go语言种的
for
循环与C语言一样,都允许在循环条件中定义和初始化变量。唯一的区别是,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量。 - Go语言的
for
循环同样支持用continue
和break
来控制循环,但它提供了一个更高级的break
——可以选择中断哪一个循环,如下例:
JumpLoop:
for j := 0; j < 5; j++{
for i := 0; i < 5; i++{
if i > 2{
break JumpLoop
}
fmt.Println(i)
}
}
在上述代码中,break
语句终止的是JumpLoop
标签对应的for
循环。for
中的初始语句是在第1次循环前执行的语句。一般使用初始语句进行变量初始化,但如果变量在for
循环中被声明,则其作用域只是这个for
的范围。初始语句可以被忽略,但是初始语句之后的分号必须要写,代码如下:
j := 2
for ; j > 0; j--{
fmt.Println(j)
}
在上面这段代码中,将j
放在for
的前面进行初始化,for
中没有初始语句,此时j
的作用域比在初始语句中声明的j
的作用域要大。
for
中的条件表达式是控制是否循环的开关。在每次循环开始前,都会判断条件表达式,如果表达式为true
,则循环继续;否则结束循环。条件表达式可以被忽略,忽略条件表达式后默认形成无限循环。
下面代码会忽略条件表达式,但是保留结束语句:
var i int
JumpLoop:
for ; ; i++{
if i > 10 {
// println(i)
break JumpLoop
}
}
上面的代码还可以改写为更美观的写法,如下:
var i int
for {
if i > 10 {
break
}
i++
}
在for
循环中,如果循环被break
、goto
、return
、panic
等语句强制退出,则之后的语句不会被执行。
3. for-range
循环
for-range
循环结构是Go语言特有的一种的迭代结构,其引用十分广泛。for-range
可以遍历数组、切片、字符串、map
及通道(channel
)。
for-range
的语法结构:
for key, val := range 复合变量值 {
// ...逻辑语句
}
需要注意的是,val
始终为集合中对应索引值的一个复制值。因此,它一般只具有“只读”属性,对它所做的任何修改都不会影响集合中原有的值。一个字符串是Unicode
编码的字符(或称之为rune
)集合,因此也可以用它来迭代字符串:
for position, char := range str{
// ...逻辑语句
}
每个rune
字符和索引在for-range
循环中的值是一一对应的,它能够自动根据UTF-8
规则识别Unicode
编码的字符。
通过for-range
遍历的返回值有一定的规律:
- 数组、切片、字符串返回索引和值。
-
map
返回值和键。 - 通道(
channel
)只返回通道内的值。
(1)遍历数组、切片。
在遍历代码中,key
和value
分别代表切片的下标及下标对应的值。
下面的代码展示如何遍历切片,数组也是类似的遍历方法:
for key, value := range []int{0, 1, -1, -2}{
fmt.Printf("key:%d value:%d\n", key, value)
}
以上代码的运行结果如下:
key:0 value:0
key:1 value:1
key:2 value:-1
key:3 value:-2
(2)遍历字符串。
Go语言和其他语言类似:可以通过for-range
的组合对字符串进行遍历。在遍历时,key
和value
分别代表字符串的索引和字符串中的一个字符。
下面这段代码展示了如何遍历字符串:
var str = "hi 加油"
for key, value := range str{
fmt.Printf("key:%d value:0x%x\n", key, value)
}
以上代码的运行结果如下:
key:0 value:0x68
key:1 value:0x69
key:2 value:0x20
key:3 value:0x52a0
key:4 value:0x6cb9
代码中的遍历value
的实际类型是rune
类型,以十六进制打印出来就是字符的编码。
(3)遍历map
。
对于map
类型,for-range
在遍历时,key
和value
分别代表map
的索引键key
和索引键对应的值。下面的代码演示了如何遍历map
:
m := map[string]int{
"go": 100,
"web": 100,
}
for key, value := range m {
fmt.Println(key, value)
}
以上代码的运行结果如下:
go 100
web 100
(4)遍历通道(channel
)。
通道可以通过for-range
进行遍历。不同于slice
和map
,在遍历通道时只输出一个值,即通道内的类型对应的数据。
下面代码展示了通道的遍历方法:
c := make(chan int) // 创建了一个整型类型的通道
go func(){ // 启动了一个goroutine
c <- 7 // 将数据推送进通道
c <- 8
c <- 9
}()
for v := range c {
fmt.Println(v)
}
以上代码的运行结果如下:
7
8
9
以上代码的逻辑如下:
- 创建一个整型类型的通道并实例化;
- 通过关键字
go
启动了一个goroutine
; - 将数字传入通道,实现的功能是往通道中推送数据7、8、9;
- 结束并关闭通道(这段
goroutine
在声明结束后马上被执行); - 用
for-range
语句对通道c
进行遍历,即不断地从通道中接收数据知道通道被关闭。
在使用for-range
循环遍历某个对象时,往往不会同时使用key
和value
的值,而是只需要其中一个的值。这时可以采用一些技巧让代码变得更简单。
m := map[string]int{
"shirdon": 100,
"ronger": 98,
}
for _, value := range m {
fmt.Println(value)
}
以上代码的运行结果如下:
100
98