# Golang
:Go 语言,一种编译型语言。
- 官方文档 (opens new window)
- 于 2009 年由 Google 公司发布。
- 属于强类型语言、静态语言。
- 提供了协程机制,处理并发任务的效率高。因此常用于服务器应用开发、云计算领域。
# 语法特点
- 语法与 C 语言相似,支持定义结构体、指针。
- 源文件的扩展名为 .go 。
- 每个语句的末尾以换行符或分号 ; 作为分隔符。
- 用
//
声明单行注释,用/*
、*/
声明多行注释。 - 支持定义函数,不支持定义类,但可以通过结构体(struct)实现面向对象编程。
- 会自动回收垃圾内存。
# 编译
安装编译器:
yum install golang
使用编译器:
go run test.go # 编译并执行一个源文件 run . # 指定一个目录时,会自动执行该目录下包含 main 包及 main 函数的源文件 build test.go # 编译一个源文件,这会生成一个名为 test 的可执行文件
# 程序示例
编写一个源文件 test.go :
package main // 声明当前文件属于一个名为 main 的包
import "fmt" // 导入名为 fmt 的包
func main() {
var x = "Hello World!"
fmt.Println(x)
}
- 启动一个 Golang 程序时,会找到其中名为 main 的包,执行其中名为 main 的函数。
- 包 fmt 提供了一些输入输出的函数,如下:
fmt.Print("Hello") // 打印一个值到终端 fmt.Println("Hello") // 打印一个值并换行 fmt.Println("Hello\nWorld", 1, 3.14) // 可以打印任意个值 fmt.Printf("%d, %d\n", 1, 2) // 格式化输出
- Golang 要求左花括号
{
不能独占一行,如下:func main() { // 编译时会报错:unexpected semicolon or newline before { fmt.Println("Hello") }
# 变量
用关键字 var 可以创建变量。
- 常见的几种格式如下:
var x int // 创建变量,在变量名之后声明其数据类型,但不赋值。此时变量采用默认值
var x int = 1 // 创建变量,声明数据类型,并赋值
var x = 1 // 创建变量,省略数据类型,并赋值。此时会根据赋值自动确定数据类型
- 可以同时创建多个变量:
var x, y int var a, b, c = 1, true, "Hello"
- 可以通过括号写成多行的格式:
var ( a int b bool c string )
var ( a int b bool = true c = "Hello" )
- 创建变量时,如果该变量名已存在,则编译时会报错:
variable redeclared
- 如果创建了一个局部变量,但并没有在其作用域内读取其值,则编译时会报错:
variable declared and not used
- 常见的几种格式如下:
在函数内可以用
:=
创建变量:x := 1 // 相当于 var x = 1
- 函数外的每个语句都必须以关键字开头,因此不能使用
:=
。
- 函数外的每个语句都必须以关键字开头,因此不能使用
用关键字 const 可以创建常量,格式与 var 相似:
const LENGTH int = 10 const a, b, c = 1, true, "Hello"
_
是一个特殊的标识符,可以作为变量被赋值,但不能被读取。因此通常将一些不需要保存的值赋值给它。如下:result, _ := do_sth() fmt.Println(_) // 不能读取 _ 的值,编译时会报错:cannot use _ as value
变量的作用域:
- 全局变量:在函数外、包内创建。
- 局部变量:在函数、for 语句块等区域内创建。
# 数据类型
# 基本类型
int // 整型
int8 // 别名为 byte
int16
int32 // 别名为 rune
int64
uint // 无符号整型 ,还有 uint8、uint16、uint32、uint64
uintptr // 无符号整型,用于存储指针
float32 // 浮点型
float64
complex64 // 复数,比如 c := 1 + 2i
complex128
bool // 布尔类型,取值为 true 或 false
string // 字符串类型,采用 UTF-8 编码
- int、uint、uintptr 在 32 位系统上容量为 4 bytes ,在 64 位系统上容量为 8 bytes 。
- 创建不同数据类型的变量时,其默认值如下:
- 数值类型,默认值为 0 。
- 布尔类型,默认值为 false 。
- 字符类型,默认值为 '' 。
- 字符串类型,默认值为 "" 。
- 通过定界符区分字符、字符串常量:
c := 'H' // 定界符为单引号,因此视作字符类型,存储为 rune 型 c := 'Hello' // 字符类型只能包含单个字符,这里编译时会报错:invalid character literal (more than one character) s := "Hello" // 定界符为双引号,因此视作字符串类型,存储为 string 型
# 类型转换
- 不支持自动类型转换:
i := 42 // 创建的变量为 int 型 i = 3.14 // int 型变量不能被 float 型常量赋值,此时会报错:constant 3.14 truncated to integer
f := 3.14 // 创建的变量为 float64 型 f = 42 // float64 型变量可以被 int 型常量赋值 f = i // 不同类型的变量不能相互赋值。此时会报错:cannot use i (type int) as type float64 in assignment
- 支持强制类型转换:
var i int = 42 var f float64 = float64(i) // 相当于 f := float64(i)
# 数组
- 数组(array):用
[n]type
的格式创建,长度固定。 - 创建数组:
var array [10]int // 创建一个指定长度的数组,不赋值 array2 := [10]int{0, 1, 2, 3} // 创建一个指定长度的数组,并赋值部分元素
- 数组中的元素必须属于同一数据类型。
- 访问数组:
array[0] = 10 // 给指定位置的元素赋值 array[0] // 读取指定位置的元素 len(inArr) // 获取数组的长度
# 切片
- 切片(slice):引用数组的某个区间的元素,也可以用
[]type
的格式创建,长度可变。- 修改数组的元素时,其切片看到的元素也会改变。反之也成立。
- 获取数组的切片:
array := []int{0, 1, 2, 3} array[0:3] // 获取区间 [0, 3) 的切片,这里会返回一个数组 [0 1 2] array[0:0] // 获取区间 [0, 0) 的切片,这里会返回一个数组 []
- 切片的上标、下标可以缺省,默认值分别为 0、len(array) ,因此支持以下写法:
array[0:] // 这里会返回一个数组 [0 1 2 3] array[:3] // 这里会返回一个数组 [0 1 2] array[:] // 这里会返回一个数组 [0 1 2 3]
- 切片的上标、下标出错的几种情况:
array[-1:0] // 上标、下标不能为负数,这里编译时会报错:invalid slice index -1 (index must be non-negative) array[1:0] // 上标不能大于下标,这里编译时会报错:invalid slice index: 1 > 0 array[0:6] // 下标不能大于数组长度,这里运行时会报错:runtime error: slice bounds out of range [:6] with capacity 5
- 切片的上标、下标可以缺省,默认值分别为 0、len(array) ,因此支持以下写法:
- 创建切片:
slice := []int{0, 1, 2, 3} // 这会先创建一个数组 [4]int{0, 1, 2, 3} ,再返回其切片
- 对切片追加元素:
slice = append(slice, 4) // 追加一个元素 slice = append(slice, 5, 6, 7) // 可以同时追加多个元素 fmt.Println(slice) // 这里会打印:[0 1 2 3 4 5 6 7]
- 调用 make 函数可以创建 slice、Map、channel 等类型的内存空间。
slice := make([]int, 4) // 创建一个切片,长度为 4
# 结构体
- 用关键字
type ... struct
可以定义一个结构体,从而自定义一种数据类型。 - 定义结构体:
type Book struct { // 定义一个结构体,名为 Book id int // 给该结构体定义一个成员,名为 id ,数据类型为 int title string author string }
- 使用结构体:
var book1 Book // 创建结构体 Book 的一个实例,名为 book1 book1.id // 读取结构体的成员。这里 id 还未赋值,所以采用 int 型变量的默认值 book1.id = 1 // 给成员赋值 fmt.Println(book1) // 打印结构体时,会按顺序打印各个成员的值。这里会打印:{0 }
book2 := Book{2, "Hello", "unknown"} // 创建一个实例,并赋值。此时如果只给部分成员赋值,则编译时会报错:too few values in Book literal
book3 := Book{title: "Hello", author: "unknown"} // 可以通过键值对的形式赋值,并且此时可以只给部分成员赋值 fmt.Println(book3) // 这里会打印:{0 Hello unknown}
# Map
- Map(映射)是包含一组键值对的集合,类似于 Python 的字典类型。
- 各键值对是无序存储的。
- 创建 Map :
m := make(map[string]int) // 创建一个 Map 变量,名为 m ,键值对的数据类型分别为 string、int m["A"] = 1 // 添加一个键值对 m["B"] = 2 delete(m, "B") // 从 Map 中删除一个 key 及其值。如果该 key 不存在,并不会报错
- 检查一个 key 是否存在:
value, ok := m["A"] if (ok) { fmt.Println("key 存在", value) } else { fmt.Println("key 不存在", value) }
- 读取一个 key 对应的值时,如果该 key 不存在,则读取到的值为其数据类型的默认值,但并不会报错。
- 遍历 Map :
for key := range m { fmt.Println(key, "=", m[key]) }
for key, value := range m { fmt.Println(key, "=", value) }
# 指针
使用指针:
x := 42 var p *int // 创建一个变量,并声明为 *int 型指针 p = &x // 用 & 运算符获取一个变量的地址,赋值给指针变量 fmt.Println(p) // 获取指针变量本身的值,即十六进制地址,这里会打印:0xc000086020 fmt.Println(*p) // 用 * 运算符取消指针的引用,读取其引用地址的值。这里会打印:42
- 创建指针时可简写为:
x := 42 p := &x
- 创建指针时可简写为:
取值为 nil 的指针称为空指针,相当于 C 语言的 null 指针。
- 刚创建的指针变量,默认值都为 nil 。
- 例:
p = nil fmt.Println(p) // 这里会打印: nil fmt.Println(*p) // 空指针不能读取其引用地址的值,这里运行时会报错:runtime error: invalid memory address or nil pointer dereference
与 C 语言相比,Golang 禁止了一些指针运算:
x := 42 y := 3.14 p := &x p = &y // 指针不能存储其它数据类型的地址,这里编译时会报错:cannot use &y (type *float64) as type *int in assignment p += 1 // 指针存储的值不能进行算术运算,这里编译时会报错:invalid operation: p += 1 (mismatched types *int and int)
使用结构体指针:
var book1 Book book_p := &book1 fmt.Println(book1) // 这里会打印:{0 } fmt.Println(book_p) // 这里会打印:&{0 } (*book_p).id // 用 * 运算符取消引用,再访问结构体的成员 book_p.id // 也可以用 book_p.id 直接访问结构体的成员,它会被 Golang 当作 (*book_p).id 处理
# 流程控制
# if 语句
例:
if x := 1; x < 2 { // 可以在条件表达式之前加上一个子句 fmt.Println(true) } else { fmt.Println(false) }
多层判断:
x := 1 if x == 1 { fmt.Println(1) } else if x == 2 { fmt.Println(2) } else { fmt.Println(0) }
# for 语句
例:
sum := 0 for i := 0; i < 10; i++ { sum += i }
- 当条件表达式的布尔值为 false 时,for 循环才结束。
可以省略初始化子句、后置子句,只留下条件表达式子句:
sum := 1 for ; sum < 10; { sum += sum }
还可以再省略分号,格式相当于 Python 的 while 循环:
sum := 1 for sum < 10 { sum += sum }
可以省略所有子句,写成无限循环:
for { }
可以将 for 与 range 搭配使用,进行遍历:
for index, value := range "Hello" { fmt.Println(index, value) } // 打印如下: // 0 72 // 1 101 // 2 108 // 3 108 // 4 111
- 每次遍历时会返回两个值:元素的索引、元素的值。可以只获取其中一个:
for index := range "Hello" { } for _, value := range "Hello" { }
- 每次遍历时会返回两个值:元素的索引、元素的值。可以只获取其中一个:
# 函数
例:
func fun1(x int, y int){ // 定义一个函数,名为 fun1 ,形参为 (x int, y int) fmt.Println(x, y) } fun1(1, 2) // 调用函数
- 函数可以没有形参,或声明多个形参。
- 函数可以没有返回值,或者返回多个值。
- 函数不支持嵌套,不能在一个函数内定义另一个函数。
如果函数有返回值,则必须在函数头末尾声明返回值列表:
func fun1() (int, int) { return 1, 2 } fmt.Println(fun1())
- 这里声明了返回值列表为
(int, int)
,因此函数必须返回两个 int 类型的值,否则编译时会报错。
- 这里声明了返回值列表为
可以在返回值列表中指定变量名:
func fun1() (a, b int) { a = 1 return // return 1, 2 } fmt.Println(fun1())
- 这里声明了返回值列表为
(a, b int)
,因此会在函数开始时就创建这两个局部变量。 - 如果 return 语句的内容为空,则会返回 a、b 两个变量作为返回值。
- 这里声明了返回值列表为
用关键字 defer 调用一个函数,会推迟执行它:
func fun1(){ defer fmt.Println("defer...") // 使用 defer 语句 fmt.Println("function end") } fun1()
- defer 语句只能在一个函数内使用,在该函数退出时才执行。
- 如果有多个 defer 语句,则按后进先出的顺序执行。
# package
- 导入包:
import "fmt" import "math"
- 可以通过括号写成多行的格式:
import ( "fmt" "math" )
- 可以通过括号写成多行的格式:
- Golang 根据包内的成员是否以大写字母开头,控制包外的访问权限。
- 以大写字母开头的成员会被导出(export),可以被外部调用,相当于 Java 的 public 权限。
- 其它成员不会被导出,对外部不可见,相当于 Java 的 protected 权限。
- 例:
fmt.Println(math.Pi) // 可以访问 fmt.Println(math.pi) // 不可以访问,编译时会报错:cannot refer to unexported name
# 协程
Golang 提供了 Goroutines 机制,用于创建轻量级的协程。还提供了 Channels 机制,用于协程之间的通信。
用关键字 go 调用一个函数,会创建一个协程去执行它:
func fun1(x int){ fmt.Println(x) } go fun1(1)
- 创建协程的当前线程,称为主线程。
- 协程会共享主线程的内存,但不会共享 stdin、stdout 。
- 当主线程退出时,其下所有协程会被自动终止。
协程之间可以通过 channel 类型的变量进行通信:
func fun1(ch chan int){ for i := 0; i < 100; i++ { ch <- i // 写入一个值到通道。如果通道的缓冲区没有可用空间,则一直等待写入,陷入阻塞 } close(ch) // 关闭通道。只有发送者能关闭通道 } func main() { ch := make(chan int, 10) // 用关键字 chan 创建一个通道,其数据类型为 int ,缓冲区可存储 10 个 int 型值 go fun1(ch) // 创建协程,并传递通道以便通信 for value := range ch { // 遍历通道。如果通道为空,则一直等待读取,陷入阻塞,除非通道被关闭 fmt.Println(value) } }
- 可以主动判断通道是否关闭:
value, ok := <-ch // 从通道取出一个值,如果 ok 为 false 则说明通道已被关闭
- 可以主动判断通道是否关闭:
可以用 for + select 语句同时读取多个通道:
for { select { case ch := <- 1: fmt.Println("写入一次") case <- quit: fmt.Println("终止执行") return default: fmt.Println("sleep ...") time.Sleep(1000 * time.Millisecond) } }
- 这里 select 语句会检查哪个 case 条件满足,执行相应的语句块,都不满足则执行 default 语句块。
- 如果没有 default 语句,则 select 语句会阻塞等待,直到某个 case 条件满足。