让我们从最简单的 HelloWorld 开始理解

1
2
3
4
5
6
7
package main

import "fmt"

func main() {
fmt.Println("Hello, World!")
}

声明包

每个 Go 程序都是由包构成的,每个项目都是从 main 包开始运行,最开头的 package main 声明了我们当前在 main 包中

导入包

使用 import 语句来导入包,例如这里导入了 fmt 包,并且在 main 函数中使用了它的函数 Println

在需要导入多个包,可以使用多个 import 语句

1
2
import "fmt"
import "math"

也可以选择使用括号括起来

1
2
3
4
import (
"fmt"
"math"
)

导出名

在语言中,常使用 publicprivate 关键字来说明一个对象是公用的还是私有的(对外是否可见)

而在 Go 中则简单地使用首字母大小写来表示这一点,如果一个名字以大写字母开头,那么它就是已导出的(外部可见),这一点在很多地方都能体现,例如包中的函数和结构体中的元素

这也解释了为什么Println 的首字母是大写的

让我们实操一下:

在项目中新建 .\sayhello\sayhello.go

1
2
3
4
5
6
7
package sayhello

import "fmt"

func Say(name string) {
fmt.Println("Hello", name, "!")
}

然后回到 main.go 来调用它

1
2
3
4
5
6
7
package main

import "main/sayhello"

func main() {
sayhello.Say("Tom")
}

函数

这里只是简单地介绍一下函数,更为深入的内容会在后面讲

声明函数

基本的函数声明模板如下

1
2
3
4
func 函数名 (参数列表) (返回值列表){
// 函数体
return 返回值列表
}

与 C 不同,Go 的参数是名称在前,类型在后,请看下面的例子

1
2
3
func add(x int, y int) int {
return x + y
}

如果连续多个参数的类型相同,那么就可以写在一起

1
2
3
func add(x, y int) int {
return x + y
}

多值返回

Go 扩展了 C 中的函数的返回值,现在函数可以返回任意多个参数

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

命名返回值

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量

这样一来,在最后可以直接用一句 return 来结束,定义的返回值就会自动返回

1
2
3
4
func add(x, y int) (ans int) {
ans = x + y
return
}

变量

声明及初始化

基本的变量声明模板如下

1
var 变量名 类型 = 表达式

其中,类型或表达式可以省略一个:

  • 若省略类型,就会按表达式自动推断类型
  • 若省略表达式,就会自动初始化为该类型的零值

零值就是字面意思,就像你在 C 中对 int 类型总会手动初始化为 0,只是在 Go 中,这一切都是自动化的,这可以简化很多代码

多个变量也可以一起声明和初始化,若省略类型,则会分别自动推导类型

1
2
var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

前面讲了函数可以同时返回多个参数,所以一组变量也可以这样初始化

1
var f, err = os.Open(name) // os.Open returns a file and an error

接收多个变量的时候,可以使用下划线(_)丢弃不需要的值

1
var f, _ = os.Open(name) // 不接收错误

varimport 一样,使用括号括起来

1
2
3
4
5
var (
ToBe bool = false
MaxInt uint64 = 1<<64 - 1
z complex128 = cmplx.Sqrt(-5 + 12i)
)

这种写法一般用于在函数外声明一堆包级变量

短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明

1
变量名 := 表达式

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用

上面的例子也可以这样写

1
f, err := os.Open(name) // os.Open returns a file and an error

赋值

Go 的赋值兼容了 C 中的所有操作,包括缩写运算符(如 +=),还有自增自减(++--)等

Go 在这方面创新了一个元组赋值,就像上面的多变量初始化一样,右边的几个变量分别赋值到左边去

这一特性最大的帮助就是使交换变量变得非常方便

1
x, y = y, x

在 C 中如果要交换两个变量的值,一般都要借助于一个临时变量,但 Go 使得这一操作变得非常优雅

来看看用 Go 来求最大公约数,是不是简洁了许多

1
2
3
4
5
6
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}

与初始变量时一样,你可以使用下划线来丢去部分值

1
_, err = io.Copy(dst, src) // 丢弃字节数

关于这个下划线,我还想多说一句

众所周知如果你声明了一个变量但是没用它是会报错的,有红色波浪线,看得很烦心

这时,你可以接着下划线假装“使用”了这一变量

1
_ = a

指针

Go 的指针摒弃了 C 中的运算功能(加减法),其他地方可以认为是相同的

类型

基本类型

Go 中的基本类型有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名,表示一个 Unicode 码点

float32 float64

complex64 complex128

类型转换

这点与 C 有较大区别,使用 T(v) 将值 v 转换为类型 T

1
2
3
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

并且,Go 不提供自动转换,在 C 中你可以直接将一个 int 赋给 float ,但你不能在 Go 中做到这种事情

类型别名

1
type mytype = string

类似于 C 中的 define,在编译时全部替换

没法自己添加方法,可以增强可读性

类型定义

1
type mytype string

基于旧类型生成新类型,相互可以强制转换,可以添加自己的方法

逻辑控制

for

Go 中的 for,与 C 中的基本相同,但是少了小括号,并且大括号变成必须的了

1
2
3
for i := 0; i < 10; i++ {
sum += i
}

与 C 一样,起始条件、结束条件和后置语句都是可选的

while

当你省略了起始条件和后置语句时,for 其实就变成了 while,所以 Go 中是没有while

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

而无限循环也就只剩下了一个 for

1
2
for {
}

if

类似地,无需小括号,而大括号是必须的

1
2
3
4
5
6
func sqrt(x float64) string {
if x < 0 {
return sqrt(-x) + "i"
}
return fmt.Sprint(math.Sqrt(x))
}

Go 允许你在条件表达式前先执行一个语句,这可以简化代码

1
2
3
4
5
6
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}

与你想的一样,这里的 v 的作用域仅限该 if 语句内

至于 elseelse if 的用法,可以参考下面的例子

1
2
3
4
5
6
7
8
var a int
if _, _ = fmt.Scan(&a); a > 10 {
fmt.Println("a > 10")
} else if a == 10 {
fmt.Println("a = 10")
} else {
fmt.Println("a < 10")
}

switch

对比 C 的 switch,你只需要更新了以下几点:

  • case 现在不一定要是常量,也可以是表达式(如果是表达式,那么中途跳出后下面的表达式并不会执行)

  • 每个分支都是默认 break 的,如果你不想跳出,可以以 fallthrough 语句结束

  • 支持多条件匹配

    1
    2
    3
    4
    switch a {
    case 1,2,3,4:
    default:
    }

来看几个例子

https://tour.go-zh.org/flowcontrol/9

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"runtime"
)

func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}

https://www.runoob.com/go/go-switch-statement.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import "fmt"

func main() {
/* 定义局部变量 */
var grade string = "B"
var marks int = 90

switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}

switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
}

defer

defer 语句会将函数推迟到外层函数返回之后执行

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
fmt.Println("counting")

for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

fmt.Println("done")
}

作用域

除下面的两点外,其他的都与 C 中的相同

  • 在同一个包内的变量和函数,在整个包内的所有文件中都可用
  • import 语句只对当前文件有效