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语言标准库为我们提供的、用于格式化输入输出的内容,在开发调试的过程中会经常用到。
在实际编码中,为了看起来直观,一般会在packageimport之间空一行。当然这个空行不是必需的,有没有都不影响程序执行。

在导入包的过程中要注意:如果导入的包没有被使用,则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()函数打印完成后会自动换行。lnline的缩写。
Java类似,每一行语句的结尾处不需要英文分号“;”来作为结束符,Go编译器会自动添加,当然也可以加上分号。

1.3 Go基础语法与使用

1.3.1 基础语法

1. Go语言编辑

Go语言由关键字、标识符、常量、字符串、符号等多种标记组成。

2. 行分隔符

在Go程序中,一般来说一行就是一个语句,不用像JavaPHP等其他语言那样需要在一行的最后用英文分号(;)结尾,因为这些工作都将由Go编译器自动完成。但如果多个语句写在同一行,则必须使用分号(;)将它们分隔开。但在实际开发中并不鼓励这种写法。
如下的写法是两个语句:

fmt.Println("Hello, Let's Go!")
fmt.Println("Go Web编成实战派从入门到精通")

3. 注释

在Go程序中,注释分为单行注释和多行注释。
(1)单行注释。单行注释是最常见的注释形式,以双斜线“//”开头的单行注释,可以在任何地方使用。形如:

// 单行注释

(2)多行注释。也被称为“块注释”,通常以“/*”开头,并以“*/”结尾。形如:

/*
多行注释
多行注释
*/

4. 标识符

标识符通常用来对变量、类型等程序实体进行命名。一个标识符实际上就是一个或是多个字母(A~Za~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)常量相关预定义标识符:truefalseiotanil
(2)类型相关预定义标识符:intint8int16int32int64uintuint8uint16uint32uint64uintptrfloat32float64complex128complex64boolbyterunestringerror
(3)函数相关预定义标识符:makelencapnewappendcopyclosedeletecomplexrealimagpanicrecover

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.0bool类型为falsestring类型为空字符串,指针类型为nil等。
变量的命名规则遵循”骆驼“命名法,即首个单词小写,每个新单词的首字母大写,例如:stockCounttotalPrice。当然,命名规则不是强制性的,开发这可以按照自己的习惯指定自己的命名规则。
变量的声明形式可以分为标准格式、批量格式、简短格式这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()函数使用了局部变量local1local2local3

代码 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语言的常量有一个不同寻常指出:虽然一个常量可以有任意一个确定的基础类型(例如intfloat64,或者是类似time.Duration这样的基础类型),但是许多常量并没有一个明确的基础类型。编译器为这些没有明确的基础类型的数字常量,提供比基础类型更高精度的算数运算。

Go语言有6种未明确类型的常量类型:无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

延迟明确常量的具体类型,不仅可以提供更高的运算精度,还可以直接用于更多的表达式而不需要显式的类型转换。

例如,无类型的浮点数常量math.Pi,可以直接用于任何需要浮点数或复数的地方:

var a float32 = math.Pi
var b float64 = math.Pi
var c complex128 = math.Pi

如果math.Pi被确定未特定类型(比如float64),则结果精度可能会不一样。同时在需要float32complex128类型值得地方,需要对其进行一个明确得强制类型转换:

const Pi64 float64 = math.Pi
var a float32 = float32(Pi64)
var b float64 = Pi64
var c complex128 = complex128(Pi64)

对于常量面值,不同的写法会对应不同得类型。例如00.00i\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,truefalse也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。

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结构。如果必须使用这种形式,则尽可能把先满足的条件放在前面。

关键字ifelse之后的左大括号{必须和关键字在同一行。如果使用了else-if结构,则前段代码块的右大括号}必须和else if语句在同一行。这两条规则都是被编译器强制规定的,如果不满足,则编译不能通过。

2. for循环

与多数语言不同的是,Go语言种的循环语句只支持for关键字,不支持whiledo-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循环同样支持用continuebreak来控制循环,但它提供了一个更高级的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循环中,如果循环被breakgotoreturnpanic等语句强制退出,则之后的语句不会被执行。

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)遍历数组、切片。
在遍历代码中,keyvalue分别代表切片的下标及下标对应的值。

下面的代码展示如何遍历切片,数组也是类似的遍历方法:

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的组合对字符串进行遍历。在遍历时,keyvalue分别代表字符串的索引和字符串中的一个字符。

下面这段代码展示了如何遍历字符串:

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在遍历时,keyvalue分别代表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进行遍历。不同于slicemap,在遍历通道时只输出一个值,即通道内的类型对应的数据。

下面代码展示了通道的遍历方法:

c := make(chan int)   // 创建了一个整型类型的通道
go func(){            // 启动了一个goroutine
	c <- 7            // 将数据推送进通道
	c <- 8
	c <- 9
}()
for v := range c {
	fmt.Println(v)
}

以上代码的运行结果如下:

7
8
9

以上代码的逻辑如下:

  1. 创建一个整型类型的通道并实例化;
  2. 通过关键字go启动了一个goroutine
  3. 将数字传入通道,实现的功能是往通道中推送数据7、8、9;
  4. 结束并关闭通道(这段goroutine在声明结束后马上被执行);
  5. for-range语句对通道c进行遍历,即不断地从通道中接收数据知道通道被关闭。

在使用for-range循环遍历某个对象时,往往不会同时使用keyvalue的值,而是只需要其中一个的值。这时可以采用一些技巧让代码变得更简单。

m := map[string]int{
	"shirdon": 100,
	"ronger": 98,
}
for _, value := range m {
	fmt.Println(value)
}

以上代码的运行结果如下:

100
98