Go语言学习笔记
Go语言特色
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
Go 语言最主要的特性
- 自动垃圾回收
- 更丰富的内置类型
- 函数多返回值
- 错误处理
- 匿名函数和闭包
- 类型和接口
- 并发编程
- 反射
- 语言交互性
优势
提供了海量并行的支持,对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。
基础语法
Go 程序是通过 package 来组织的。
只有 package 名称为 main 的源码文件可以包含 main 函数。
一个可执行程序有且仅有一个 main 包。
通过 import 关键字来导入其他非 main 包。
可以通过 import 关键字单个导入:
import "fmt"
import "io"
也可以同时导入多个:
import (
"fmt"
"math"
)
通过 const 关键字来进行常量的定义。
// 常量定义
const PI = 3.14
通过在函数体外部使用 var 关键字来进行全局变量的声明和赋值。
// 全局变量的声明和赋值
var name = "gopher"
通过 type 关键字来进行结构(struct)和接口(interface)的声明。
// 一般类型声明
type newType int
// 结构的声明
type gopher struct{}
// 接口的声明
type golang interface{}
通过 func 关键字来进行函数的声明。
func main() {
Println("Hello World!")
}
可见性规则:
Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。
函数名首字母小写即为 private :
func getId() {}
函数名首字母大写即为 public :
func Printf() {}
数据类型
go 1.9版本对于数字类型,无需定义int及float32、float64,系统会自动识别。
package main
import "fmt"
func main() {
var a = 1.5
var b =2
fmt.Println(a,b)
}
布尔值的类型为 bool,值是 true 或 false,默认为 false。
//示例代码
var isActive bool // 全局变量声明
var enabled, disabled = true, false // 忽略类型的声明
func test() {
var available bool // 一般声明
valid := false // 简短声明
available = true // 赋值操作
}
变量
声明变量的一般形式是使用 var 关键字:
var identifier type
声明多个变量
var identifier1, identifier2 type
变量声明
第一种,指定变量类型,如果没有初始化,则变量默认为零值(变量没有做初始化时系统默认设置的值)。
- 数值类型(包括complex64/128)为 0
- 布尔类型为 false
- 字符串为 “”(空字符串)
- 以下几种类型为 nil:
var a *int var a []int var a map[string] int var a chan int var a func(string) int var a error // error 是接口
第二种,根据值自行判定变量类型。
var v_name = value
第三种,如果变量已经使用 var 声明过了,再使用 := 声明变量,就产生编译错误,格式:
v_name := value
// intVal := 1 相等于:
var intVal int
intVal =1
ps:
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
func main(){
g, h := 123, "hello"
...
}
空白标识符(一个只写变量,无法得到它的值):
package main
import "fmt"
func main() {
_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}
常量
常量(const)是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
常量的定义格式:
const identifier [type] = value
在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。
package main
import "fmt"
const (
a = 1
b
c
d
)
func main() {
fmt.Println(a)
// b、c、d没有初始化,使用上一行(即a)的值
fmt.Println(b) // 输出1
fmt.Println(c) // 输出1
fmt.Println(d) // 输出1
}
iota,特殊常量,可以认为是一个可以被编译器修改的常量(在同一个 const 常量组内递增,每当有新的 const 关键字时,iota 计数会重新开始)。
用法:
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
const xx = iota
const yy = iota
fmt.Println(a,b,c,d,e,f,g,h,i,xx,yy)
// 输出是 0 1 2 ha ha 100 100 7 8 0 0
}
运算符
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
* | 指针变量 | *a; 是一个指针变量 |
指针变量 * 和地址值 & 的区别:
指针变量保存的是一个地址值,会分配独立的内存来存储一个整型数字。当变量前面有 * 标识时,才等同于 & 的用法,否则会直接输出一个整型数字。
func main() {
var a int = 4
var ptr *int
ptr = &a
println("a的值为", a); // 4
println("*ptr为", *ptr); // 4
println("ptr为", ptr); // 824633794744
}
条件语句
Go 的 if 还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示:
package main
import "fmt"
func main() {
if num := 9; num < 0 {
fmt.Println(num, "is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has multiple digits")
}
}
/*
运行结果:
9 has 1 digit
*/
if 语句使用 tips:
- 不需使用括号将条件包含起来
- 大括号{}必须存在,即使只有一行语句
- 左括号必须在if或else的同一行
- 在if之后,条件语句之前,可以添加变量初始化语句,使用;进行分隔
- 在有返回值的函数中,最终的return不能在条件语句中
switch语句使用tips:
- 支持多条件匹配
switch{ case 1,2,3,4: default: }
- 不同的 case 之间不使用 break 分隔,默认只会执行一个 case。
- 如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。
switch{ case 1: ... if(...){ break } fallthrough // 此时switch(1)会执行case1和case2,但是如果满足if条件,则只执行case1 case 2: ... case 3: }
select语句:
类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
- 每个 case 都必须是一个通信
- 所有 channel 表达式都会被求值
- 所有被发送的表达式都会被求值
- 如果任意某个通信可以进行,它就执行,其他被忽略。
- 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:- 如果有 default 子句,则执行该语句。
- 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
package main import "fmt" func main() { var c1, c2, c3 chan int var i1, i2 int select { case i1 = <-c1: fmt.Printf("received ", i1, " from c1\n") case c2 <- i2: fmt.Printf("sent ", i2, " to c2\n") case i3, ok := (<-c3): // same as: i3, ok := <-c3 if ok { fmt.Printf("received ", i3, " from c3\n") } else { fmt.Printf("c3 is closed\n") } default: fmt.Printf("no communication\n") } } /* 执行结果: no communication */
循环语句
For循环的3种形式:
和 C 语言的 for 一样:
for init; condition; post { }
和 C 的 while 一样:
for condition { }
和 C 的 for(;😉 一样:
for { }
- init: 一般为赋值表达式,给控制变量赋初值;
- condition: 关系表达式或逻辑表达式,循环控制条件;
- post: 一般为赋值表达式,给控制变量增量或减量。
For-each range 循环:
这种格式的循环可以对字符串、数组、切片等进行迭代输出元素。
package main
import "fmt"
func main() {
strings := []string{"google", "runoob"}
for i, s := range strings {
fmt.Println(i, s)
}
numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
nameMap := make(map[string]string)
nameMap["first name"] = "Hayes"
nameMap["last name"] = "Peng"
for k, v := range nameMap {
fmt.Println(k, v)
}
}
/*
输出结果:
0 google
1 runoob
第 0 位 x 的值 = 1
第 1 位 x 的值 = 2
第 2 位 x 的值 = 3
第 3 位 x 的值 = 5
第 4 位 x 的值 = 0
第 5 位 x 的值 = 0
last name Peng
first name Hayes
*/
函数
函数返回多个值:
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Google", "Runoob")
fmt.Println(a, b)
}
参数传递方式:
传递类型 | 描述 |
---|---|
值传递 | 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。 |
引用传递 | 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。 |
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前,a 的值 : %d\n", a )
fmt.Printf("交换前,b 的值 : %d\n", b )
valueSwap(a, b);
fmt.Printf("值传递交换后,a 的值 : %d\n", a )
fmt.Printf("值传递交换后,b 的值 : %d\n", b )
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("引用传递交换后,a 的值 : %d\n", a )
fmt.Printf("引用传递交换后,b 的值 : %d\n", b )
}
func valueSwap(x, y int) {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
/*
输出结果:
交换前,a 的值 : 100
交换前,b 的值 : 200
值传递交换后,a 的值 : 100
值传递交换后,b 的值 : 200
引用传递交换后,a 的值 : 200
引用传递交换后,b 的值 : 100
*/
函数作为实参:
package main
import (
"fmt"
"math"
)
// 声明函数类型
type cb func(int) int
func main() {
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
/*
输出结果:
3
我是回调,x:1
我是回调,x:2
*/
函数闭包:
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
package main
import "fmt"
// 闭包使用方法
/*
func add(x1, x2 int) func(x3 int, x4 int)(int, int, int) {
i := 0
return func(x3 int, x4 int) (int, int, int){
i++
return i, x1 + x2, x3 + x4
}
}
*/
// 闭包使用方法,函数声明中的返回值(闭包函数)不用写具体的形参名称
func add(x1, x2 int) func(int, int) (int, int, int) {
i := 0
return func(x3, x4 int) (int, int, int) {
i += 1
return i, x1 + x2, x3 + x4
}
}
func main() {
add_func := add(1, 2)
fmt.Println(add_func(4, 5))
fmt.Println(add_func(1, 3))
fmt.Println(add_func(2, 2))
}
/*
输出结果:
1 3 9
2 3 4
3 3 4
*/
函数方法:
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。
关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针。
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c Circle
fmt.Println(c.radius)
c.radius = 10.00
fmt.Println("圆的面积 = ", c.getArea())
c.changeRadius2(20)
fmt.Println(c.radius)
c.changeRadius(20)
fmt.Println(c.radius)
change(&c, 30)
fmt.Println(c.radius)
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64) {
c.radius = radius
}
// 以下操作将不生效
func (c Circle) changeRadius2(radius float64) {
c.radius = radius
}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64) {
c.radius = radius
}
/*
输出结果:
0
圆的面积 = 314
10
20
30
所有参数都是值传递:slice,map,channel 会有传引用的错觉(比如切片,他背后对应的是一个数组,切片本身是一个数据结构,在这个数据结构中包含了指向了这个数组的指针。所以说,即便是在传值的情况下这个结构被复制到函数里了,在通过指针去操作这个数组的值的时候,其实是操作的是同一块空间,实际上是结构被复制了,但是结构里包含的指针指向的是同一个数组,所以才有这个错觉)
变量作用域:
- 局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
- 全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。
- 形式参数:形式参数会作为函数的局部变量来使用
package main
import "fmt"
/* 声明全局变量 */
var a int = 20
func main() {
/* main 函数中声明局部变量 */
var a int = 10
var b int = 20
var c int = 0
fmt.Printf("main()函数中 a = %d\n", a)
c = sum(a, b)
fmt.Printf("main()函数中 a = %d\n", a)
fmt.Printf("main()函数中 c = %d\n", c)
}
/* 函数定义-两数相加 */
func sum(a, b int) int {
a = a + 1
fmt.Printf("sum() 函数中 a = %d\n", a)
fmt.Printf("sum() 函数中 b = %d\n", b)
return a + b
}
/*
输出结果:
main()函数中 a = 10
sum() 函数中 a = 11
sum() 函数中 b = 20
main()函数中 a = 10
main()函数中 c = 31
*/
数组
声明数组
var variable_name [SIZE] variable_type
var balance [10] float32
初始化数组
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 声明 + 初始化
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// 如果数组长度不确定,可以使用 ... 代替数组的长度
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
多维数组
// 声明方式
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type{}
// 声明 + 初始化
a := [3][4]int{
{0, 1, 2, 3} , /* 第一行索引为 0 */
{4, 5, 6, 7} , /* 第二行索引为 1 */
{8, 9, 10, 11}, /* 第三行索引为 2 */
}
向函数传递数组
package main
import (
"fmt"
)
func main() {
var array = []int{1, 2, 3, 4, 5}
/* 未定义长度的数组(切片)只能传给不限制数组长度的函数 */
setArray(array)
/* 定义了长度的数组只能传给限制了相同数组长度的函数 */
var array2 = [5]int{1, 2, 3, 4, 5}
setArray2(array2)
}
func setArray(params []int) {
fmt.Println("params array length of setArray is : ", len(params))
}
func setArray2(params [5]int) {
fmt.Println("params array length of setArray2 is : ", len(params))
}
数组(没有所谓没有声明长度的数组存在)作为函数参数传递的是副本,函数内修改数组并不改变原来的数组。
package main
import "fmt"
func change(nums[3] int){
nums[0]=100
}
func main() {
var nums=[3]int{1,2,3}
change(nums) //nums并未被改变
fmt.Println(nums[0])
return
}
// 输出结果: 1
数组 与 切片 的区别
-
Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见
-
Go 语言中对数组的处理,一般采用 切片 的方式,切片包含对底层数组内容的引用,作为函数参数时,类似于 指针传递,函数中的修改对调用者可见
package main
import "fmt"
func main() {
// 数组
b := [...]int{2, 3, 5, 7, 11, 13}
boo(b)
fmt.Println(b) // [2 3 5 7 11 13]
// 切片
p := []int{2, 3, 5, 7, 11, 13}
poo(p)
fmt.Println(p) // [13 3 5 7 11 2]
}
func boo(tt [6]int) {
tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0]
}
func poo(tt []int) {
tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0]
}
另外地,也可向函数传递数组引用。
package main
import "fmt"
// Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见
func change1(nums [3]int) {
nums[0] = 4
}
// 传递进来数组的内存地址,然后定义指针变量指向该地址,则会改变数组的值
func change2(nums *[3]int) {
nums[0] = 5
}
// Go 语言中对数组的处理,一般采用 切片 的方式,切片包含对底层数组内容的引用,作为函数参数时,类似于 指针传递,函数中的修改对调用者可见
func change3(nums []int) {
nums[0] = 6
}
func main() {
var nums1 = [3]int{1, 2, 3}
var nums2 = []int{1, 2, 3}
change1(nums1)
fmt.Println(nums1) // [1 2 3]
change2(&nums1)
fmt.Println(nums1) // [5 2 3]
change3(nums2)
fmt.Println(nums2) // [6 2 3]
}
指针
&是取地址符,放到一个变量前使用就会返回相应变量的内存地址。
package main
import "fmt"
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
/*
输出结果:
a 变量的地址是: c000016060
ip 变量储存的指针地址: c000016060
*ip 变量的值: 20
*/
指针数组 && 数组指针
var arr [3]int
var parr [3]*int // 指针数组
var p *[3]int = &arr // 数组指针
for k, _ := range arr {
parr[k] = &arr[k];
}
// 输出地址比对
for i := 0; i < 3; i+=1 {
fmt.Println(&arr[i], parr[i], &(*p)[i]);
}
指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
/*
输出结果:
变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000
*/
指针作为函数参数
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);
// 同 a, b = b, a
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
*x, *y = *y, *x
}
/*
输出结果:
交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100
*/
结构体
struct 类似于 java 中的类,可以在 struct 中定义成员变量。
可以采用类似java构造器的方式来进行初始化
要访问成员变量,可以有两种方式:
- 通过 struct 变量.成员 变量来访问。
- 通过 struct 指针.成员 变量来访问。
不需要通过 getter, setter 来设置访问权限。
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* 打印 Book1 信息 */
fmt.Printf( "Book 1 title : %s\n", Book1.title)
fmt.Printf( "Book 1 author : %s\n", Book1.author)
fmt.Printf( "Book 1 subject : %s\n", Book1.subject)
fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)
fmt.Println(Book1)
}
/*
输出结果:
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com Go 语言教程 6495407}
{Go 语言 www.runoob.com 0}
Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
{Go 语言 www.runoob.com Go 语言教程 6495407}
*/
结构体是作为参数的值传递,如果想在函数里面改变结构体数据内容,需要传入指针
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func changeBook(book Books) {
book.title = "book1_change"
}
func changeBook1(book *Books) {
book.title = "book1_change"
}
func main() {
var book1 Books
book1.title = "book1"
book1.author = "zuozhe"
book1.book_id = 1
changeBook(book1)
fmt.Println(book1)
changeBook1(&book1)
fmt.Println(book1)
}
/*
输出结果:
{book1 zuozhe 1}
{book1_change zuozhe 1}
*/
结构体中属性的首字母大小写问题
- 首字母大写相当于 public。
- 首字母小写相当于 private。
注意: 这个 public 和 private 是相对于包(go 文件首行的 package 后面跟的包名)来说的。
敲黑板,划重点
当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON。
示例一:
type Person struct {
Name string //Name字段首字母大写
age int //age字段首字母小写
}
func main() {
person:=Person{"小明",18}
if result,err:=json.Marshal(&person);err==nil{ //json.Marshal 将对象转换为json字符串
fmt.Println(string(result))
}
}
/*
输出结果:
{"Name":"小明"} //只有Name,没有age
*/
示例二:
type Person struct{
Name string //都是大写
Age int
}
/*
输出结果:
{"Name":"小明","Age":18} //两个字段都有
*/
那这样 JSON 字符串以后就只能是大写了么? 当然不是,可以使用 tag 标记要返回的字段名。
示例三:
type Person struct{
Name string `json:"name"` //标记json名字为name
Age int `json:"age"`
Time int64 `json:"-"` // 标记忽略该字段
}
func main(){
person:=Person{"小明",18, time.Now().Unix()}
if result,err:=json.Marshal(&person);err==nil{
fmt.Println(string(result))
}
}
/*
输出结果:
{"name":"小明","age":18}
*/
切片
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
以下为具体实例:
package main
import "fmt"
func main() {
// make([]T, length, capacity)
var numbers = make([]int,3,5)
printSlice(numbers)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
输出结果:
len=3 cap=5 slice=[0 0 0]
*/
空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,实例如下:
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
输出结果:
len=0 cap=0 slice=[]
切片是空的
*/
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]
对于底层数组容量是 k 的切片 slice[i:j] 来说,新的切片长度和容量:
长度: j-i
容量: k-i
实例如下:
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==", numbers[4:])
numbers1 := make([]int,0,5)
printSlice(numbers1)
/* 打印子切片从索引 0(包含) 到索引 2(不包含) */
number2 := numbers[:2]
printSlice(number2)
/* 打印子切片从索引 2(包含) 到索引 5(不包含) */
number3 := numbers[2:5]
printSlice(number3)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
输出结果:
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
*/
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
当append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),如果大于那么 cap 不变,否则 :
当同时添加多个元素时:
- 若len[list]+len[params]为偶数,则cap=len[list]+len[params]
- 若len[list]+len[params]为奇数,则cap=len[list]+len[params]+1
当一个一个添加元素时:
-
len(list)+1<=cap: cap=cap
-
len(list)+1>cap: cap=2*cap
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
/* 允许追加空切片 */
numbers = append(numbers, 0)
printSlice(numbers)
/* 向切片添加一个元素 */
numbers = append(numbers, 1)
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
/*
输出结果:
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
*/
范围
package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
/*
输出结果:
sum: 9
index: 1
a -> apple
b -> banana
0 103
1 111
*/
Map(集合)
package main
import "fmt"
func main() {
var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "巴黎"
countryCapitalMap [ "Italy" ] = "罗马"
countryCapitalMap [ "Japan" ] = "东京"
countryCapitalMap [ "India " ] = "新德里"
/*使用键输出地图值 */
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [country])
}
/*查看元素在集合中是否存在 */
capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */
/*fmt.Println(capital) */
/*fmt.Println(ok) */
if (ok) {
fmt.Println("American 的首都是", capital)
} else {
fmt.Println("American 的首都不存在")
}
/*删除元素*/ delete(countryCapitalMap, "France")
fmt.Println("法国条目被删除")
fmt.Println("删除元素后地图")
/*打印地图*/
for country := range countryCapitalMap {
fmt.Println(country, "首都是", countryCapitalMap [ country ])
}
}
/*
输出结果:
France 首都是 巴黎
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
American 的首都不存在
法国条目被删除
删除元素后地图
Italy 首都是 罗马
Japan 首都是 东京
India 首都是 新德里
*/
递归函数
阶乘
package main
import "fmt"
func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result
}
return 1
}
func main() {
var i int = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}
/*
输出结果:
15 的阶乘是 1307674368000
*/
斐波那契数列
package main
import "fmt"
func fibonacci(n int) int {
if n < 2 {
return n
}
return fibonacci(n-2) + fibonacci(n-1)
}
func main() {
var i int
for i = 0; i < 10; i++ {
fmt.Printf("%d\t", fibonacci(i))
}
}
/*
输出结果:
0 1 1 2 3 5 8 13 21 34
*/
类型转换
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
/*
go 不支持隐式转换类型,如以下会报错
mean = sum/float32(count)
mean = float32(sum)/count
mean = sum/count
*/
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean)
}
/*
输出结果:
mean 的值为: 3.400000
*/
接口
package main
import (
"fmt"
)
type Phone interface {
call() string
}
type Android struct {
brand string
}
type IPhone struct {
version string
}
func (android Android) call() string {
return "I am Android " + android.brand
}
func (iPhone IPhone) call() string {
return "I am iPhone " + iPhone.version
}
func printCall(p Phone) {
fmt.Println(p.call() + ", I can call you!")
}
func main() {
var vivo = Android{brand:"Vivo"}
var hw = Android{"HuaWei"}
i7 := IPhone{"7 Plus"}
ix := IPhone{"X"}
printCall(vivo)
printCall(hw)
printCall(i7)
printCall(ix)
}
/*
输出结果:
I am Android Vivo, I can call you!
I am Android HuaWei, I can call you!
I am iPhone 7 Plus, I can call you!
I am iPhone X, I can call you
*/
错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
package main
import (
"fmt"
)
// 定义一个 DivideError 结构
type DivideError struct {
dividee int
divider int
}
// 实现 `error` 接口
func (de *DivideError) Error() string {
strFormat := `
Cannot proceed, the divider is zero.
dividee: %d
divider: 0
`
return fmt.Sprintf(strFormat, de.dividee)
}
// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
if varDivider == 0 {
dData := DivideError{
dividee: varDividee,
divider: varDivider,
}
errorMsg = dData.Error()
return
} else {
return varDividee / varDivider, ""
}
}
func main() {
// 正常情况
if result, errorMsg := Divide(100, 10); errorMsg == "" {
fmt.Println("100/10 = ", result)
}
// 当除数为零的时候会返回错误信息
if _, errorMsg := Divide(100, 0); errorMsg != "" {
fmt.Println("errorMsg is: ", errorMsg)
}
}
/*
输出结果:
100/10 = 10
errorMsg is:
Cannot proceed, the divider is zero.
dividee: 100
divider: 0
*/
panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错误,recover 用来捕获 panic 抛出的错误。
- 引发panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。
- 发生panic后,不再执行后面的代码,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句(defer 就类似 finally),然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数。
- panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic。defer里面的panic能够被后续执行的defer捕获。
- recover用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是defer只有在后面的函数体内直接被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。
多个panic只会捕捉最后一个:
package main
import "fmt"
func main(){
defer func(){
if err := recover() ; err != nil {
fmt.Println(err)
}
}()
defer func(){
panic("three")
}()
defer func(){
panic("two")
}()
panic("one")
}
/*
输出结果:
three
*/
注意:
- panic 在没有用 recover 前以及在 recover 捕获那一级函数栈,panic 之后的代码均不会执行;一旦被 recover 捕获后,外层的函数栈代码恢复正常,所有代码均会得到执行
- 利用 recover 捕获 panic 时,defer 需要再 panic 之前声明,否则由于 panic 之后的代码得不到执行,因此也无法 recover
package main
import (
"fmt"
)
func main() {
fmt.Println("外层开始")
defer func() {
fmt.Println("外层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "外层", err) // err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码
} else {
fmt.Println("外层没做啥事")
}
fmt.Println("外层完成recover")
}()
fmt.Println("外层即将异常")
f()
fmt.Println("外层异常后")
defer func() {
fmt.Println("外层异常后defer")
}()
}
func f() {
fmt.Println("内层开始")
defer func() {
fmt.Println("内层recover前的defer")
}()
defer func() {
fmt.Println("内层准备recover")
if err := recover(); err != nil {
fmt.Printf("%#v-%#v\n", "内层", err) // 这里err就是panic传入的内容
}
fmt.Println("内层完成recover")
}()
defer func() {
fmt.Println("内层异常前recover后的defer")
}()
panic("异常信息")
defer func() {
fmt.Println("内层异常后的defer")
}()
fmt.Println("内层异常后语句") //recover捕获的一级或者完全不捕获这里开始下面代码不会再执行
}
/*
输出结果:
外层开始
外层即将异常
内层开始
内层异常前recover后的defer
内层准备recover
"内层"-"异常信息"
内层完成recover
内层recover前的defer
外层异常后
外层异常后defer
外层准备recover
外层没做啥事
外层完成recover
*/
并发
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
同一个程序中的所有 goroutine 共享同一个地址空间。
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
// 输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行
/*
输出结果:
hello
world
world
hello
hello
world
hello
world
hello
*/
通道(channel)
通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
// 通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
/*
输出结果:
-5 17 12
*/
通道缓冲区
默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意: 如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
输出结果:
1
2
*/
遍历通道与关闭通道
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
/*
range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据之后就结束了。
如果上面的 c 通道不关闭,那么 range 函数就不会结束,从而在接收第 11 个数据的时候就阻塞了。
*/
for i := range c {
fmt.Println(i)
}
}
/*
输出结果:
0
1
1
2
3
5
8
13
21
34
*/