Go语言是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,好称“大道至简”,是新时代的“C语言”。

Go语言基本语法

变量

go语言语法规定,定义的局部变量若没有被调用会发生编译错误

变量声明格式如下:

1
//var 变量名 变量类型
2
	var a int
3
//批量声明
4
var(
5
	a int
6
	b string
7
	c []float32
8
	d func() bool
9
	e struct {
10
		x int
11
		y string
12
	}
13
)

未初始化的变量将被默认初始化:

  1. 整型和浮点型变量默认值为0。
  2. 字符串默认值为空字符串。
  3. 布尔型默认值为 false
  4. 函数,指针变量,切片默认值为 nil

go语言拥有多种初始化方式:

1
//标准格式
2
var 变量名 变量类型 = 表达式
3
//编译器自动推断类型格式
4
var 变量名 = 表达式
5
//短变量声明格式
6
变量名 := 表达式
7
8
var a int = 10
9
var b = 10
10
b := 10

短变量声明模式只能用于函数体内,该变量名必须是没有定义过的变量,若定义过,将发生编译错误。多个短变量声明和赋值时,至少有一个新声明的变量出现在左侧,那么即便其他变量可能是重复声明的,编译器也不会报错价格。

1
var a = 10
2
a := 20 //重复定义变量a
3
4
var a = 10
5
a, b := 100, 200 //ok

go语言提供了多重赋值功能,可以实现变量交换,需要注意的是,多重赋值时,左值和右值按照从左到右的顺序赋值。

1
//传统写法
2
var a int = 10
3
var b int = 20
4
vat tmp int
5
tmp = a
6
a = b
7
b = tmp
8
9
//一种取代中间变量的算法
10
a = a ^ b
11
b = b ^ a
12
a = a ^ b
13
14
//go的多重赋值
15
b, a = a, b

匿名变量_既不占用命名空间,也不会分配内存。

1
func GetData() (int, int) {
2
	return 10, 20
3
}
4
5
a, _ = GetData()
6
_, b = GetData()

数据类型

  1. 基本数据类型:整型,浮点型,复数型,布尔型,字符串,字符(byte,rune)。
  2. 复合数据类型: 数组,切片(slice),映射(map),函数,结构体,通道(channel),接口(interface),指针。
整型类型 字节数 取值范围 说明
int8 1 -128~127 有符号8位整型
uint8 1 0~255 无符号8位整型
int16 2 -32768~32767
uint16 2 0~65535
int32 4
uint32 4
int64 8
uint64 8
int 4/8 取决于平台
uint 4/8 取决于平台
uintptr 4/8 取决于平台 用于存放一个指针
符号类型 字节数 说明
float32 4 32位浮点型
float64 8 64位浮点型
复数类型 字节数 说明
complex64 8 64位的复数型,由float32类型的实部和虚部联合表示
complex128 16 128位的复数类型,由float64类型的实部和虚部联合表示
字符类型 字节数 说明
byte 1 表示utf-8字符串的单个字节的值,uint8的别名类型
rune 4 表示单个unicode字符,int32的别名类型

打印格式通常使用 fmt 包,通用的打印格式有:

  1. %v 值的默认格式表示。
  2. %T 值的类型的Go语法表示。
  3. %t bool类型的true/false。
  4. %b 整型的二进制。

类型转换

Go语言采用数据类型前置加括号的方式进行类型转换,T(表达式)。Go语言中不允许字符串转 int

1
var a int = 100
2
b := float64(a)
3
c := string(a)

指针

指针存储另一个变量的内存地址的变量。一个指针变量可以指向任何一个值的内存地址。go语言中使用取地址符 & 来获取变量的地址,一个变量前使用 &,会返回该变量的内存地址。go语言的指针不能运算

1
var 指针变量名 *指针类型
2
var ip *int
3
var fp *float32

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil指针也称为空指针。

1
if(ptr != nil) {}
2
3
var ptr [3]*string //指针数组就是元素为指针类型的数组。
4
5
var ptr **int //指向指针的指针

常量

常量是一个简单的标示符,在程序运行时,不会被修改。常量中的数据只可以是布尔类型,数字型(整型,浮点型和复数型)和字符串。常量定义和未被使用,不会在编译时报错。

1
const 标示符 [类型] = 值  //可以省略说明符[type]
2
3
const B string = "hello"
4
const C = "world"
5
const C, D, E = value1, value2
6
7
const (
8
	a = 10
9
	b = 11
10
	c		//常量组中如果不指定类型和初始值,则与上一个非空常量的值相同
11
	d
12
)

iota常量

iota 特殊常量值,是一个系统定义的可以被编译器修改的常量值。可以理解为常量组中的常量计数器,只要有一个常量,则 iota 就加1,即可以被用作枚举值

1
const(
2
	a = iota	//第一个iota等于0
3
	b = iota
4
	c = iota
5
	d	//和上一个一样是iota,自增1
6
)

类型别名与类型定义

1
//type 新的类型名 类型
2
//type 类型别名 = 类型
3
4
type byte uint8
5
type rune int32
6
7
type byte = uint8
8
type rune = int32

运算符

算术运算符

+ - * / % ++ --

关系运算符

== != > < >= <=

逻辑运算符

&& || !

位运算符

位运算符 说明
&
|
^
<< 左移运算符,高位移出,低位补0。
>> 右移运算符,低位移出,高位补符号位,即正数补0,负数补1。

赋值运算符
== += -= *= /= %= <<= >>== &= ^= |=

其他运算符
& *

运算符优先级

  1. ^!
  2. */%<<>>&&^
  3. +-|^
  4. ==!=<<=>=>
  5. <-
  6. &&
  7. ||

流程控制

if条件判断语句

在go语言中,左括号必须在if或else的同一行。在if之后,条件语句之前,可以添加变量初始化语句,使用";"进行分割。

1
if 布尔表达式 {
2
	/* 布尔表达为 true 时执行 */
3
}
4
5
if 布尔表达式 {
6
7
} else {
8
9
}
10
11
if 布尔表达式 {
12
13
} else if {
14
15
16
} else {
17
18
}
19
20
if statement; condition {
21
22
}

switch分支语句

Go语言中的switch默认给每个case自带break,因此匹配成功后不会向下执行其他的case分支,而是跳出整个switch。可以添加fallthrough强制执行后面的case分支。fallthrough必须放在case分支的最后一行。

case后的值不能重复,但可以有多个值,这些值之间用,隔开,同时测试多个符合条件的值。

switch后的表达式可以省略,默认是switch true

switch类型转换

switch语句还可以被用于 type switch(类型转换)来判断某个interface变量中实际存储的变量类型。

1
package main
2
import "fmt"
3
func main() {
4
	var x interface{}
5
	switch i := x.(type) {
6
		case nil:
7
			fmt.Printf(" x 的类型 :%T",i)
8
		case int:
9
			fmt.Printf(" x 是 int 型")
10
		case float64:
11
			fmt.Printf(" x 是 float64 型")
12
		case func(int) float64:
13
			fmt.Printf(" x 是 func(int) 型")
14
		case bool, string:
15
			fmt.Printf(" x 是 bool 或 string 型")
16
		default:
17
			fmt.Printf("未知型")
18
	}
19
}

for循环语句

for是go语言中唯一的循环语句,go没有while,do…while循环。

1
for 初始语句init; 条件表达式condition; 结束语句post {
2
3
}
4
5
for 循环条件condition {
6
7
}
8
9
10
for {
11
12
}
13
14
for key, value := range oldMap {
15
	newMap[key] = value
16
}

循环控制语句

break continue goto

1
LABEL: statement
2
goto LABEL

函数

go语言的函数可以返回多个值,返回值可以是返回数据的数据类型,也可以是变量名+变量类型的组合。return后的数据,要保持和声明的返回值类型,数量,顺序一致。

1
func 函数名 (参数列表) (返回参数列表) {
2
	//
3
}
4
5
func funcName (param type1, param type2...) (output1 type1, output2 type2..){
6
	//
7
	return value1, value2...
8
}

在参数列表中,如果相邻变量是同类型,则可以将类型省略。go语言支持可变参数,在函数体中变参是一个切片。一个函数最多有一个可变参数。若参数列表中还有其他类型参数,则可变参数写在所有参数最后。

go语言支持递归函数,注意防止栈溢出。

1
func add(a, b int) {}
2
3
func myfunc(arg ...int) {}

在go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。可以通过关键字type来自定义类型。

1
package main
2
import "fmt"
3
4
type processFunc func(int) bool	//声明一个函数类型
5
6
func main() {
7
	slice := []int{1,2,3,4,5,7}
8
	fmt.Println("slice = ",slice)
9
	odd := filter(slice, isOdd)
10
	fmt.Println("odd num: ",odd)
11
	even := filter(slice, isEven)
12
	fmt.Println("even num: ",even)
13
}
14
15
func isEven(integer int) bool {
16
	if integer % 2 == 0 {
17
		return ture
18
	}
19
	return false
20
}
21
22
func isOdd(integer int) bool {
23
	if integer % 2 == 0 {
24
		return false
25
	}
26
	return true
27
}
28
29
func filter(slice []int, f processFunc) []int {
30
	var result []int
31
	for _, value := range slice {
32
		if f(value) {
33
			result = append(result, value)
34
		}
35
	}
36
	return result
37
}

匿名函数

Go语言支持匿名函数,即在需要使用函数时再定义函数。匿名函数没有函数名,只有函数体,函数可以作为一种类型被赋值给变量,匿名函数也往往以变量方式被传递。

1
func(参数列表) (返回参数列表) {
2
3
}
4
5
package main
6
import "fmt"
7
func main() {
8
	func(data int) {
9
		fmt.Println("hello", data)
10
	}(100)
11
}
12
13
func main() {
14
	f := func(data string) {
15
		fmt.Println(data)
16
	}
17
	f("hello world")
18
}
19
20
func main() {
21
	arr := []float{1,9,16,25,30}
22
	visit(arr, func(v float64){
23
		v = math.Sqrt(v)
24
		fmt.Printf("%.2f \n", v)
25
	})
26
}
27
28
func visit(list []float64, f func(float64)) {
29
	for _, value := range list {
30
		f(value)
31
	}
32
}

闭包

闭包(Closure)是词法闭包(Lexical Closure)的简称。闭包是由函数和其相关的引用环境组合而成的实体。在实现深约束时,需要创建一个能显示表示引用环境的东西,并将它与相关的子程序捆绑在一起,这样捆绑起来的整体被称为闭包。函数 + 引用环境 = 闭包

闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行的代码,这些代码在函数被定义后就确定来,不会在执行时发生变化,所以函数是一个实例。

闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。闭包在某些编程语言中被称为Lambda表达式。

函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有”记忆性”。函数是编译器静态的概念,而闭包是运行期动态的概念。对象是附有行为的数据,而闭包是附有数据的行为。

1
package main
2
import "fmt"
3
func main(){
4
	for i := 0; i < 5; i++ {
5
		fmt.Printf("i=%d \t",i)
6
		fmt.Println(add2(i))
7
	}
8
}
9
10
func add2(x int) int {
11
	sum := 0
12
	sum += x
13
	return sum
14
}
1
package main
2
import "fmt"
3
func main() {
4
	pos := adder()
5
	for i := 0; i < 10; i++ {
6
		fmt.Printf("i=%d \t", i)
7
		fmt.Printf(pos(i))
8
	}
9
	
10
	for i := 0; i < 10; i++ {
11
		fmt.Printf("i=%d \t", i)
12
		fmt.Printf(pos(i))
13
	}
14
}
15
16
func adder() func(int) int {
17
	sum := 0
18
	return func(x int) int {
19
		fmt.Printf("sum1=%d \t", sum)
20
		sum += x
21
		fmt.Printf("sum2=%d \t", sum)
22
		return sum
23
	}
24
}

闭包捕获来和它在同一作用域的其他常量和变量,所以当闭包在任何地方被调用,闭包都可以使用这些常量或变量。只要闭包还在使用这些变量,这些变量就依然存在,不关心这些变量是否已经超出作用域。

go语言中所有的传参都是值传递,都是一个副本。副本的内容有的是值类型,有的是引用类型。

Go语言容器

数组

数组是相同类型的一组数据构成的长度固定的序列,其中数据类型包含了基本数据类型,复合数据类型和自定义类型。

1
var 变量名 [数组长度] 数据类型
2
3
var nmus = [5]int{1,2,3,4,5}
4
5
var nums = [...]int{1,2,3,4,5}

数组的长度是数组的一个内置常量,通过将数组作为参数传递给 len() 函数,可以获得数组的长度。

1
var arrayName [x][y] variable_type

go语言中数组并非引用类型,而是值类型。将数组作为函数参数传递,是通过值传递,原始数组保持不变。

切片

切片是可变长度的序列,序列中每个元素都是相同类型。切片的数据结构可以理解为一个结构体,这个结构体包含三个元素:

  1. 指针,指向数组中切片指定的开始位置。
  2. 长度,即切片的长度。
  3. 容量,也就是切片开始的位置到数组的最后位置的长度。

声明一个未指定长度的数组来定义切片。切片不需要说明长度,采用该声明方式且未初始化的切片为空切片。默认长度为nil,且长度为0。

1
var identifier [] type

使用 make() 函数来创建切片。其中 capacity 为可选参数: make([]T, length, capacity)

1
var slice []type = make([]type, len)
2
3
slice := make([]type, len)

s := arr[startIndex:endIndex]将arr中从下标startIndex到endIndex-1下的元素创建为一个新的切片,长度为endIndex-startIndex。缺省endIndex时,表示一直到arr的最后一个元素。缺省startIndex时,表示从arr的第一个元素开始。

1
s := [] int {1,2,3}
2
3
arr := [5] int {1,2,3,4,5}
4
s := arr[:]

切片的长度是切片中元素的数量。切片的长度可以通过 len() 方法获取,切片的容量可以通过 cap() 函数获取。数组计算cap()与len()的结果相同。

切片没有自己的数据,它是底层数组的一个引用。对切片所做的任何修改都将反应在底层数组中。数组是值类型,切片是引用类型。

函数 append() 用于向切片中追加新元素。可以向切片里追加一个或者多个元素,也可以追加一个切片。append() 会改变切片所引用的数组的内容,会影响到引用同一个数组的其他切片。当容量不够时,会新建一个内存地址来存储元素。

函数 copy() 会复制切片元素,将源切片中的元素复制到目标切片中,返回复制元素的个数。复制前后的切片不存在联系。

map

map是引用类型,map 是由hash表实现的,所以对 map 的读取顺序不固定。map 是无序的,不能通过index获取,而必须通过key获取。map 长度不是固定的,和切片一样可以扩展。内置的 len()函数同样适用于map,返回map拥有的键值对的数量,但 map 不能通过 cap()函数计算容量。

1
var 变量名 map[key类型]value类型  //未初始化的map的默认值是nil
2
3
变量名 := make(map[key类型]value类型)  //该声明方式不初始化map,map也不等于nil

value, ok := map[key]获取key/value是否存在。

delete(map, key)函数用于删除集合中的某个元素,删除函数不返回任何值。

结构体

类型名是标识结构体的名称,在同一个包内不能重复。同类型的成员属性可以写在一行。结构体只定义了一种内存布局,只有当结构体实例化时,才分配内存。

1
type 类型名 struct {
2
  成员属性1 类型1
3
  成员属性2 类型2
4
  成员属性3, 成员属性4 类型3
5
  ...
6
}

内置函数 new() 对结构体进行实例化后形成结构体指针。结构体是值类型。值类型是深拷贝,为新对象分配内存,引用类型是浅拷贝,只是复制了对象的指针。

结构体的语法糖

语法糖(Syntactic Sugar)指计算机中添加某种语法,对语言的功能没有影响,但是更方便程序员使用。

1
type Emp struct {
2
  name string
3
  age int8
4
  sex byte
5
}
6
7
func main() {
8
  emp := make(Emp)
9
  (*emp).name = "hello"
10
  (*emp).age = 30
11
  (*emp).sex = 1
12
  //语法糖写法
13
  emp.name = "world"
14
  emp.age = 30
15
  emp.sex = 1
16
}

匿名结构体与匿名字段

匿名结构体就是没有名字的结构体,无须通过type关键字定义就可以直接使用。匿名结构体由结构体定义和键值对初始化两部分组成。

1
变量名 := struct {
2
 
3
}{ }

匿名字段就是在结构体中没有名字的字段,只包含一个没有字段名的类型。同一个类型只有一个匿名字段。结构体采用匿名结构体字段可以模拟继承关系。

结构体嵌套可以模拟聚合关系(一个类作为另一个类的属性)和继承关系(一个类作为另一个类的子类)。

方法

方法有接受者,函数无接受者。接受者类似与thisself。接受者可以是struct和非struct类型,可以是指针类型或非指针类型。若方法的接受者不是指针,实际获取的是一份拷贝。只要接受者不同,方法名可以相同。

1
func (接受者变量 接受者类型) 方法名(参数列表) (返回值列表) {
2
3
}

方法的继承

匿名字段实现了一个方法,那么包含该匿名字段的struct也能调用该方法。

1
type Human struct { 
2
  name, phone string
3
  age int
4
}
5
6
type Student struct{
7
  Human
8
  school string
9
}
10
11
type Employee struct {
12
  Human
13
  company string
14
}
15
16
func main() {
17
  s1 := Student{Human{"hello", "123*****"}, "school"}
18
  e1 := Employee{Human{"world", "133****"}, "company"}
19
  s1.SayHi()
20
  e1.SayHi()
21
}
22
23
func (h *Human) SayHi() {
24
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
25
}

方法的重写

方法重写指一个包含了匿名字段的struct也实现了该匿名字段实现的方法。

1
type Human struct { 
2
  name, phone string
3
  age int
4
}
5
6
type Student struct{
7
  Human
8
  school string
9
}
10
11
type Employee struct {
12
  Human
13
  company string
14
}
15
16
func main() {
17
  s1 := Student{Human{"hello", "123*****"}, "school"}
18
  e1 := Employee{Human{"world", "133****"}, "company"}
19
  s1.SayHi()
20
  e1.SayHi()
21
}
22
23
func (h *Human) SayHi() {
24
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
25
}
26
27
func (h *Student) SayHi() {
28
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
29
}
30
31
func (h *Employee) SayHi() {
32
  fmt.Printf("hey,my name is %s, %d old, tell me %s\n", h.name, h.age, h.phone)
33
}

接口

在go语言中,接口是一组方法的签名,接口指定了类型应该具有的方法,类型决定了如何实现这些方法。当某个类型为接口中的所有方法提供了具体的实现,这个类型就被称为实现了该接口。go语言的类型都是隐式实现接口的,任何定义了接口中所有方法的类型都被称为隐式的实现了该接口。

1
type 接口名字 interface {
2
  方法1 ([参数列表]) [返回值]
3
  方法2 ([参数列表]) [返回值]
4
  ...
5
  方法3 ([参数列表]) [返回值]
6
}
1
type Phone interface {
2
  call()
3
}
4
5
type AndroidPhone struct {
6
7
}
8
9
type IPhone struct {
10
11
}
12
13
func (a AndroidPhone) call() {
14
  fmt.Printf("I am Android Phone\n")
15
}
16
17
func (a IPhone) call() {
18
  fmt.Printf("I am IPhone\n")
19
}
20
21
func main() {
22
  var phone Phone //定义接口类型
23
  phone = new(AndroidPhone)
24
  phone.call()
25
  phone = AdnroidPhone{}
26
  phone.call()
27
  phone = new(IPhone)
28
  phone.call()
29
  phone = IPhone{}
30
  phone.call()
31
}

duck typing

go语言没有implementsextends关键字,这类编程语言叫做duck typing编程语言。duck typing是描述事物的外部行为而非内部结构。使用duck typing的编程语言往往被归为”动态语言”或”解释型语言”。

go语言采取的方式为:

  1. 结构体类型T不需要显示地声明它实现了接口I。只要类型T实现了接口I规定的所有方法,它就自动的实现了接口I。
  2. 将结构体类型的变量显示或隐式地转换为接口I类型的变量i。可以在编译时检查参数的合法性。
1
type SayHello interface {
2
  SayHello() string
3
}
4
5
type Duck struct {
6
  name string
7
}
8
9
type Person struct {
10
  name string
11
}
12
13
func (d Duck) SayHello() string {
14
  return d.name + "ga ga ga !"
15
}
16
17
func (p Person) SayHello() string {
18
  return p.name + "hello!"
19
}
20
21
func main() {
22
23
}

多态

go语言的多态是在接口的帮助下实现的-定义接口类型,创建实现该接口的结构体对象。

空接口

空接口没有任何方法。任意类型都可以实现该接口。空接口表示任意数据类型。例如,定义一个map,key是string,value是任意数据。定义一个切片,其中存储任意类型的数据。

1
map := make(map[string]interface{})
2
3
slice := make([]interface{}, 0, 10)

接口对象转型

1
instance, ok := 接口对象.(实际类型)
2
3
接口对象.(type)

包(package)

异常处理

error

1
type error interface {
2
  Error() string
3
}

error 本质是一个接口类型,包含一个 Error() 方法,错误值存储在变量中,通过函数返回。

1
res, err := Sqrt(-100)
2
if err != nil {
3
4
}

结构体只要实现了Error() string这种格式的方法,就代表实现了该错误接口,返回值为错误的具体描述。go语言errors包对外提供了可供用户自定义的方法,errors包下的New()函数返回error对象,errors.New()函数创建新的错误。

1
package errors
2
3
func New(text string) error {
4
  return &errorString{text}
5
}
6
7
type errorString struct {
8
  s string
9
}
10
11
func (e *errorString) Error() string {
12
  return e.s
13
}
14
15
func Errorf(format string, a ...interface{}) error {
16
  return errors.New(Sprintf(format, a...))
17
}

自定义错误

  1. 定义一个结构体,表示自定义错误的类型。
  2. 让自定义错误类型实现error接口:Error() string
  3. 定义一个返回error的函数。
1
package main
2
import (
3
  "time"
4
  "fmt"
5
)
6
7
type MyError struct {
8
  When time.time
9
  What string
10
}
11
12
func (e MyError) Error() string {
13
  return fmt.Sprintf("%v : %v", e.When, e.What)
14
}

defer

defer用于延迟一个函数或者方法的执行,defer语句只能出现在函数或方法的内部。在函数中可以存在多个defer语句,defer语句会按照逆序执行。

defer语句常被用来处理成对操作,例如:打开-关闭,链接-断开,加锁-释放锁。

延迟函数的参数在执行延迟语句时被调用,而不是执行实际的函数被调用时执行。

1
func main() {
2
  str := "hello world!"
3
  fmt.Printf("原始字符串:\n %s\n", str)
4
  fmt.Println("反转后的字符串: ")
5
  ReverseString(str)
6
}
7
func ReverseString(str string) {
8
  for _, v := range []rune(str) {
9
   defer fmt.Printf("%c", v)
10
  }
11
}

panic与recover

panic()内建函数,中断程序的执行。recover()仅在延迟函数中有效。

并发

  1. 并发(Concurrency)在微观层面上,任务不会同时进行。
  2. 并行(Parallelism)多个任务一定是同时运行的。

Goroutine

Go语言通过go关键字来启动一个goroutine

  1. go的执行是非阻塞的,不会等待。
  2. go后面的函数的返回值会被忽略。
  3. 调度器不保证多个goroutine的执行次序。
  4. 没有父子goroutine的概念,所有的goroutine是平等地被调用和执行。
  5. go程序在执行时会单独为main函数创建一个goroutine,遇到其他go关键字时在创建其他的goroutine
1
package main
2
3
import (
4
  "runtime"
5
  "time"
6
)
7
8
func main() {
9
  go func() {
10
    sum := 0
11
    for i := 0; i < 10000; i++ {
12
      sum += 1
13
    }
14
    println(sum)
15
    time.Sleep(1 * time.Second)
16
  }()
17
  
18
  println("NumGoroutine=", runtime.NumGoroutine())
19
20
  time.Sleep(5 * time.Second)
21
}

select

sync

反射