概述
语法基础
包
-
每个 Go 程序都是由包构成的。程序从
main
包开始运行。package main // 导入路径"fmt"和"math/rand"来使用这两个包 // 此代码用圆括号组合了导入,这是“分组”形式的导入语句 // 推荐使用 import ( "fmt" // "math/rand"包中的源码均以 package rand 语句开始 "math/rand" ) /* // 也可以编写多个导入语句 import "fmt" import "math" */ func main() { fmt.Println("My favorite number is", rand.Intn(10)) }
- 包名与导入路径最后一个元素一致,采用
包名.函数名()
的形式调用包中的函数
- 包名与导入路径最后一个元素一致,采用
-
包的可见性规则:
- 在 Go 的包中,如果一个名字以大写字母开头,那么它就是已导出的,可见性为
public
。 - 如果一个名字为小写,则该名字是未导出的,可见性为
private
,但同一包内还是可见的。 - 在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。
- 在 Go 的包中,如果一个名字以大写字母开头,那么它就是已导出的,可见性为
-
导入包之后 未调用 其中的函数或者类型将会报出编译错误。
-
package 别名:当使用第三方包时,包名可能会非常接近或者相同,此时就可以使用别名来进行区别和调用
package main import ( // 为fmt包取一个别名io io "fmt" ) func main() { // 使用别名调用包 io.Print("Hello World!") }
-
还可以省略调用
// 包别名使用 . 号 import . "fmt" func main() { // 使用省略调用 Print("Hello World!") }
- 易混淆,不建议使用
- 不可以和别名同时使用
-
函数
-
函数可以没有参数或接受多个参数,而且类型在变量名之后。
package main import "fmt" // 函数返回值的类型放在花括号之前,右括号之后 // 若函数没有返回值,则忽略 func add(x int, y int) int { return x + y } // 函数参数可以使用可变长参数 // 可变参数应该是函数参数列表中的最后一个参数 // b 实际上是一个slice func fun(a strig, b ...int) { fmt.Println(a, b) } /* // add()函数中,形参x的类型可以省略 func add(x, y int) int { return x + y } */ func main() { fmt.Println(add(42, 13)) }
- 当两个或多个函数已命名形参类型相同时,除最后一个类型外,其它都可以省略。
- 实参与形参之间属于值拷贝。
-
函数可以返回任意数量的返回值。
// 多个返回类型用小括号包围,并以逗号分隔 func swap(x, y string) (string, string) { return y, x }
-
Go的返回值可被命名,它们会被视作定义在函数顶部的变量。没有参数的
return
语句返回已命名的返回值,即直接 返回。func swap(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return }
- 直接返回语句建议仅用在声明较短的函数中,在长的函数中则会影响代码的可读性。
-
函数也是值。它们可以像其它值一样传递。
package main import ( "fmt" "math" ) // 函数可以用作另一个函数的参数或返回值。 func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) } func main() { // 匿名函数,hypot是函数类型,指向了一个匿名函数 hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) } // 匿名函数的调用,hypot是float64类型,其值等于调用匿名函数后的返回值 /* hypot := func(x, y float64) float64 { return math.Sqrt(x*x + y*y) }(5, 12) */ fmt.Println(hypot(5, 12)) fmt.Println(compute(hypot)) fmt.Println(compute(math.Pow)) }
-
Go 函数可以是一个闭包。闭包是一个函数,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值。
package main import "fmt" // 函数 adder 返回一个匿名函数。 // 每个闭包都被绑定在其各自的 sum 变量上。 func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { // pos, neg 分别指向由adder()返回的匿名函数构造的两个副本(即是两个函数,仅仅是函数功能一致) // adder()函数的sum变量对pos与neg是共享的,即pos中的sum与neg中的sum指向的是同一块内存 pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println( pos(i), neg(-2*i), ) } }
变量
-
var
语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。package main import "fmt" var c, python, java bool func main() { var i int fmt.Println(i, c, python, java) }
var
语句可以出现在包或函数级别。
-
变量声明可以包含初始值,每个变量对应一个。
package main import "fmt" var i, j int = 1, 2 func main() { // 并行方式声明变量 // 声明同时执行初始化,变量类型可以省略 var c, python, java = true, false, "no!" // 简洁赋值语句 k : = 3 fmt.Println(i, j, c, python, java) }
- 如果初始化值已存在,则可以省略类型,变量会从初始值中获得类型。
- 在函数中,简洁赋值语句
:=
可在类型明确的地方代替var
声明。- 函数外的每个语句都必须以关键字开始 (
var
,func
等等),因此:=
结构不能在函数外使用。
- 函数外的每个语句都必须以关键字开始 (
基本类型
-
Go的基本类型如下:
类型 长度(字节) 默认值 说明 bool 1 false byte 1 0 uint8 rune 4 0 Unicode Code Point, int32 int, uint 4或8 0 32 或 64 位 int8, uint8 1 0 -128 ~ 127, 0 ~ 255, byte是uint8 的别名 int16, uint16 2 0 -32768 ~ 32767, 0 ~ 65535 int32, uint32 4 0 -21亿~ 21亿, 0 ~ 42亿, rune是int32 的别名 int64, uint64 8 0 float32 4 0.0 float64 8 0.0 complex64 8 complex128 16 uintptr 4或8 以存储指针的 uint32 或 uint64 整数 array 值类型 struct 值类型 string “” UTF-8 字符串,值类型 slice nil 引用类型 map nil 引用类型 channel nil 引用类型 interface nil 接口类型 function nil 函数类型 int
,uint
和uintptr
在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。- 当你需要一个整数值时应使用
int
类型,除非你有特殊的理由使用固定大小或无符号的整数类型。
-
同导入语句类似,变量声明也可以分组成一个语法块
var ( // 可以选择忽略变量类型 ToBe bool = false MaxInt uint64 = 1 << 64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i) ) // 一般类型也可分组为一个语法块 type ( newType int type1 float32 type2 string type3 byte )
-
没有明确初始值的变量声明会被赋予它们的零值。零值不等于空值。
- 数值类型的零值为
0
, - 布尔类型的零值为
false
, - 字符串的零值为
""
(空字符串)。
- 数值类型的零值为
-
表达式
T(v)
将值v
转换为类型T
。var i int = 42 var f float64 = float64(i) var u uint = uint(f) /* // 或者采用简洁赋值语句 i := 42 f := float64(i) u := uint(f) */
- 与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换,否则编译器将报错。
- 采用
<ValueA> [:]= <TypeOfValueA>(<ValueB>)
的形式进行类型转换。
- 采用
byte
与rune
分别是unit8
和int32
的别名,因此可以直接进行相互转换。
- 与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换,否则编译器将报错。
-
在声明一个变量而不指定其类型时(即使用不带类型的
:=
语法或var =
表达式语法),变量的类型由右值推导得出。// 当右值声明了类型时,新变量的类型与其相同: var i int j := i // j 也是一个 int // 不过当右边包含未指明类型的数值常量时, // 新变量的类型就可能是 int, float64 或 complex128, // 这取决于常量的精度 e := 42 // int f := 3.142 // float64 g := 0.867 + 0.5i // complex128
fmt.Printf("%Tn", v)
:利用%T
可以打印出变量v
的类型。
常量
-
常量的值在编译时就已经确定,赋值时等号右侧必须是常量或者常量表达式,且常量表达式中的函数必须是内置函数。
-
常量的声明与变量类似,只不过是使用
const
关键字,但不能用:=
语法声明。package main import "fmt" const Pi = 3.14 func main() { const World = "世界" fmt.Println("Hello", World) fmt.Println("Happy", Pi, "Day") const Truth = true fmt.Println("Go rules?", Truth) }
-
常量可以是字符、字符串、布尔值或数值。
-
常量也可以分组成一个语法块
const ( // 常量类型可以忽略 PI = 3.14 // 如果不提供初始值,则表示将使用上行的表达式 // 即const1=3.14,const3="const" const1 const2 = "const" const3 ) const ( // iota是常量的计数器,从0开始,组中每定义1个常量自动递增1 // 即 const4 = 0, const5 = 1, const6="B" const7 = 3 const4 = iota const5 const6 = "B" const7 = iota ) const { // 每遇到一个const关键字,iota就会重置为0 // 即const8 = 0 const8 = iota }
-
-
数值常量是高精度的值。一个未指定类型的常量由上下文来决定其类型。
package main import "fmt" const ( // 将 1 左移 100 位来创建一个非常大的数字 // 即这个数的二进制是 1 后面跟着 100 个 0 Big = 1 << 100 // 再往右移 99 位,即 Small = 1 << 1,或者说 Small = 2 Small = Bit >> 99 ) func needInt(x int) int { return x * 10 + 1 } func needFloat(x float64) float64 { return x * 0.1 } func main() { fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) }
int
可以存放最大64位的整数,根据平台不同有时会更少。
流程控制语句
-
Go只有一种循环结构:
for
循环package main import "fmt" func main() { sum := 1 for i := 1; i < 10; i++ { sum += i } /* // 初始化语句和后置语句是可选的 for ; sum < 1000; { sum += sum } // 还可以直接去掉分号,等价于C中的while语句 for sum < 1000 { sum += sum } // 如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑 for { } */ fmt.Println(sum) }
-
基本的
for
循环由三部分组成,它们用分号隔开:- 初始化语句:在第一次迭代前执行
- 初始化语句通常为一句短变量声明,该变量声明仅在
for
语句的作用域中可见。
- 初始化语句通常为一句短变量声明,该变量声明仅在
- 条件表达式:在每次迭代前求值
- 一旦条件表达式的布尔值为
false
,循环迭代就会终止。
- 一旦条件表达式的布尔值为
- 后置语句:在每次迭代的结尾执行
- 初始化语句:在第一次迭代前执行
-
Go 的 for 语句后面的三个构成部分外没有小括号, 大括号
{ }
则是必须的。 -
跳转语句
goto
、break
、continue
。// break配合标签用于跳出多层循环 func main() { LABEL: for { for i := 0; i < 10; i++ { if i > 2 { break LABEL } else { fmt.Println(i) } } } } // continue 配合标签用于外层循环的继续执行 func main() { LABEL: for i := 0; i < 10; i++ { for { fmt.Println(i) continue LABEL } } }
- 三个语法都可以配合标签使用
- 标签名区分大小写,若不使用会造成编译错误
break
与continue
配合标签可用于多层循环的跳出goto
是调整执行位置,与其它 2 个语句配合标签的结果并不相同
-
-
Go 的
if
语句与for
循环类似,表达式外无需小括号( )
,而大括号{ }
则是必须的。package main import ( "fmt" "math" ) func sqrt(x float64) string { if x < 0 { return sqrt(-x) + "i" } return fmt.Sprint(math.Sqrt(x)) } func main() { fmt.Println(sqrt(2), sqrt(-4)) }
-
if
语句可以在条件表达式前执行一个简单的语句。// 该语句声明的变量 v 作用域仅在 if 之内 // 且该变量会覆盖if之外的同名变量的声明 v := 10 if v := math.Pow(x, n); v < lim { return v // 返回的是math.Pow(x, n)的运算结果,v:= 10 被暂时覆盖 }
-
-
switch
是编写一连串if - else
语句的简便方法。它运行第一个值等于条件表达式的 case 语句。package main import { "fmt" "runtime" } func main() { fmt.Print("Go runs on ") // switch 也可以在条件表达式前执行一个简单的语句 switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.n", os) } }
-
不过 Go 只运行选定的
case
,而之后所有的case
都不会运行。-
实际上,Go 自动提供了在这些语言中每个 case 后面所需的
break
语句。除非以fallthrough
语句结束,否则分支会自动终止。// 不使用 fallthrough // 打印结果为: // a>=0 a := 1 switch { case a >= 0: fmt.Println("a>=0") case a >= 1: fmt.Println("a>=1") default: fmt.Println("None") } // 使用 fallthrough // 打印结果为: // a>=0 // a>=1 a := 1 switch { case a >= 0: fmt.Println("a>=0") fallthrough case a >= 1: fmt.Println("a>=1") default: fmt.Println("None") }
-
Go 的另
switch
的case
无需为常量,且取值不必为整数。 -
switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。
-
-
没有条件的 switch 同
switch true
一样。
-
-
defer
语句会将函数推迟到外层函数返回之后执行,类似其他语言中的析构函数。defer
支持匿名函数的调用。package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") } /* // 程序的运行结果为 hello world // fmt.Println("world"),在main函数执行完后才开始执行 */
-
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
-
即使函数发生严重错误,
defer
语句仍会在函数结束后执行。 -
如果外函数体内的某个变量作为
defer
语句调用的函数的参数时,则在定义defer
时即已经获得了拷贝,否则则是引用某个变量的地址。// 变量i作为defer语句调用的函数的参数 // 每次定义defer时,会获得一份关于i的 *值* 的拷贝 // 代码的执行结果为 /* 2 1 0 */ func main () { for i := 0; i < 3; i++ { defer func(x int) { fmt.Println(x) }(i) } } // defer语句调用的函数直接调用函数体外的变量i // 每次定义defer时,会获得一份关于i的 *地址* 的拷贝 // 代码的执行结果为 /* 3 3 3 */ func main() { for i := 0; i < 3; i++ { // defer语句将匿名函数推迟到for循环结束之后调用 // 而定义的所有defer都保存了同一个地址,即i的地址 // for循环结束后,i的值为3 // 因此执行的三个defer语句打印的结果均为3 defer func() { fmt.Println(i) }() } } // 测试:分析如下代码输出 func main() { var fs = [4]func(){} for i:= 0; i < 4; i++ { defer fmt.Println("defer i =", i) defer func() { fmt.Println("defer_closure i =", i) }() fs[i] = func() { fmt.Println("closure i =", i) } } for _, f := range fs { f() } }
-
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
package main import "fmt" func main() { fmt.Println("counting") for i := 0; i < 3; i++ { defer fmt.Println(i) } fmt.Println("done") } /* // 打印结果的顺序如下 // defer 语句的调用,按0~9的顺序入栈,之后按9~0的顺序出栈 counting done 2 1 0 */
-
指针
-
Go 拥有指针。指针保存了值的内存地址。
-
类型
*T
是指向T
类型值的指针。其零值为nil
。var p *int
-
&
操作符会生成一个指向其操作数的指针。i := 42 p = &i
-
*
操作符表示指针指向的底层值。fmt.Println(*p) // 通过指针 p 读取 i *p = 21 // 通过指针 p 设置 i
-
与 C 不同,Go 没有指针运算。
-
结构体
-
一个结构体(
struct
)就是一组字段(field)。package main import "fmt" type Vertex struct { X int Y int } func main() { v := Vertex{1, 2} p := &v p.X = 1e9 fmt.Println(v.X) }
-
在函数调用中,结构体类型的实参与形参之间是值拷贝。即改变形参的结构体内容不会影响实参
- 如果需要改变实参中的结构体内容,应该使用指针。
-
结构体字段可以通过结构体指针来访问。
-
结构体变量或指向结构体的指针均可使用
.
号来访问成员。 -
结构体文法通过直接列出字段的值来新分配一个结构体。
-
使用
Name: Value
语法可以仅列出部分字段。(字段名的顺序无关。)v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体 v2 = Vertex{X: 1} // Y:0 被隐式地赋予 v3 = Vertex{} // X:0 Y:0 p1 = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针),推荐使用 // 结构体支持指向自身的指针类型成员 // 支持匿名结构体,可用作成员或定义成员变量 v4 = struct{ Name string Age int } // 创建匿名结构体的指针 p2 = &struct{ Name string Age int } // 创建含匿名字段的结构体,也可以使用匿名字段指针 struct person { string int } // 在初始化这个结构体时,必须按定义时的类型顺序进行赋值 p3 = &person{"hpcds", 10}
-
-
相同类型的成员可进行直接拷贝赋值。
-
支持
==
与!=
比较运算符,但不支持>
或<
。- 若两个结构体的类型相同、结构体成员类型和值也相同时
==
的操作结果返回ture
,否则返回false
。
- 若两个结构体的类型相同、结构体成员类型和值也相同时
-
-
Go语言使用结构体的嵌入结构,来达到继承的效果 ( 但不是继承 )。
type human struct { Sex int Name string } type teacher struct { // 通过UML分析,teacher是human的一个子类 // 采用匿名字段的方式,将结构体类型human嵌入到teacher中 human Name string // 创建human的同名字段 Age int } type student struct { // 通过UML分析,student是human的一个子类 // 采用匿名字段的方式,将结构体类型human嵌入到student中 human Name string // 创建human的同名字段 Age int } func main() { // 嵌入结构默认将嵌入的结构体类型 (human) 的所有字段全部给被嵌入的结构体类型 t := teacher{Name: "joe", Age: 19, human: human{Sex: 0, Name: "teacher"}} s := student{Name: "joe", Age: 20, human: human{Sex: 1, Name: "student"}} fmt.Println(t.Sex) // 返回0 fmt.Println(s.Sex) // 返回1 // 当嵌入结构 (human) 的字段和被嵌入结构 (teacher、student) 具有同名字段时 // 1. 使用 被嵌入结构类型变量.嵌入结构类型.同名字段 返回嵌入结构 (human) 中的字段 (Name) 值 // 2. 使用 被嵌入结构类型变量.同名字段 返回被嵌入结构 (teacher、student) 中的字段 (Name) 值 fmt.Println(t.human.Name) // 返回 "teacher" fmt.Println(t.Name) // 返回 "joe" // 当嵌入结构 (human) 的字段和被嵌入结构 (teacher、student) 没有同名字段时 // 上述两种方式均返回调用第2种方式的结果 // 假设 human 结构体中没有定义 Name 字段,而teacher与student均定义了Name字段 /* fmt.Println(t.human.Name) // 返回 "joe" fmt.Println(t.Name) // 返回 "joe" */ }
- 当一个结构体中嵌入了多个结构体,而这些结构体中均存在同名字段时,程序会报错
数组
-
类型
[n]T
表示拥有n
个T
类型的值的数组。声明数组的表达式为var a [10]int
// 数组的长度是其类型的一部分 var a [2]string var b [1]string var c [2]string // b = a // Error c = a // Success // 数组初始化 // 数组默认用零值进行初始化 a := [2]int{} // {0,0},int型变量的零值为0 a := [2]int{1} // {1,0},设置数组的初识值,若初始值的数量小于数组长度,则剩余部分以零值填充 a := [20]int{2:2, 19:1} // {0,2,...,1},指定长度为20的数组中,索引为2的元素初始化为2 // 索引为19的元素初始化为1 // 则其余未指定初始值的元素以零值填充 a := [...]int{1, 2, 3} // {1,2,3},使用...可以根据给定的初始值的数量来创建数组, // 并以指定的初始值对数组进行初始化 a :=[...]int{2:2, 19:1} // {0,2,...,1},使用...可以根据{}中给定的最大索引值 (即19) // 来创建长度为最大索引值+1 (即20) 的数组 // 将索引为2 的元素初始化为2 // 索引为19的元素初始化为1 // 其余未指定初始值的元素以零值填充
-
数组不能改变大小。数组的长度是其类型的一部分,具有不同长度的数组为不同类型。
-
数组在Go中为值类型,可以使用
==
或!=
进行比较,但不可以使用<
或>
。a := [2]int{1,2} b := [1]int{1} c := [2]int{1,2} fmt.Println(a == b, a != b) // Error,类型不同,无法比较 fmt.Println(a == c, a != c) // true false
-
可以使用
new
来创建数组,此方法返回一个指向数组的指针。p := new([2]int) fmt.Println(p) // 打印结果为:&[0, 0] a := [2]int{} fmt.Println(a) // 打印结果为:[0, 0] fmt.Println(&a) // 打印结果为:&[0, 0]
-
Go支持多维数组
// 值初始化 a := [2][3]int { {1, 1, 1}, {2, 2, 2} }
-
切片
-
切片 (slice) 为数组元素提供动态大小的、灵活的视角。
package main import "fmt" func main() { primes := [6]int{2, 3, 5, 7, 11} // s 包含 primes 中下标从 1 到 3 的元素: var s []int = primes[1:4] fmt.Println(s) }
-
切片通过两个下标来界定,即一个上界和一个下届,二者以冒号分隔:
primes[low:high]
-
它会选择一个半开区间,包括第一个元素,但排除最后一个元素。
-
切片并不存储任何数据,它只是描述了底层数组中的一段。
- 更改切片的元素会修改其底层数组中对应的元素。
- 与它共享底层数组的切片都会观测到这些修改。
-
切片文法类似于没有长度的数组文法
// 数组文法 [3]bool {true, true, false} // 切片文法:以下方式会先创建一个和上面相同的数组,然后构建一个引用了它的切片 []bool{true, true, false}
-
切片下界的默认值为
0
,上界的默认值则是该切片的长度。var a [10]int // 以下切片是等价的 a[0:10] a[:10] a[0:] a[:]
-
切片的长度就是它所包含的元素个数,通过
len(s)
获取。 -
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。通过
cap(s)
获取。// len(s) = 6, cap(s) = 6, [2 3 5 7 11 13] s := []int{2, 3, 5, 7, 11, 13} // Reslice 操作 // 截取切片使其长度为 0 // len(s) = 0, cap(s) = 6, [] s = s[:0] // 拓展其长度 // len(s) = 4, cap(s) = 6, [2 3 5 7] s = s[:4] // 舍弃前两个值 // len(s) = 2, cap(s) = 4, [5, 7] s = s[2:]
- Reslice是指对一个slice进行切片,Reslice时索引以被slice的切片为准。
- 索引不可以超过被slice的切片的容量
cap()
值 - 索引越界不会导致底层数组的重新分配而是引发错误
-
切片的零值是
nil
。nil
切片的长度和容量为 0 且没有底层数组。 -
切片可以用内建函数
make([]T, len, cap)
来创建,这也是创建动态数组的方式。// make 函数会分配一个元素为零值的数组并返回一个引用了它的切片: a := make([]int, 5) // len(a) = 5 // 要指定它的容量,需向 make 传入第三个参数: b := make([]int, 0, 5) // len(b) = 0, cap(b) = 5 b = b[:cap(b)] // len(b) = 5, cap(b) = 5 b = b[1:] // len(b) = 4, cap(b) = 4
- 其中cap可以省略,则和len的值相同
- len表示存数的元素个数,cap表示容量
-
切片可包含任何类型,甚至包括其它的切片。
board := [][]string { []string{"_", "_","_"}, []string{"_", "_","_"}, []string{"_", "_","_"}, // 最后的, 不能忽略 } board[0][0] = "X" board[2][2] = "O" board[1][2] = "X" // 创建动态切片 //var arr [m][n]int 这样会报错提示无法使用变量 var arr [][]int for x := 0; x < m; x++ { //循环为一维长度 ar := make([]int, n) //创建一个一维切片 arr = append(arr, ar) //把一维切片,当作一个整体传入二维切片中 }
-
内建的
append()
函数为切片追加新的元素// append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾 // append 的结果是一个包含原切片所有元素加上新添加元素的切片 // 当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。 func append(s []T, vs ...T) []T
-
可以在slice尾部追加元素
-
可以将一个slice追加在另一个slice尾部
-
如果最终长度未超过被追加的slice的初始容量则返回原始slice,且追加的元素会覆盖slice指向的底层数组中的原始元素
a := [...]int{1,2,3,4,5,6} slice := a[:3] // slice的初始长度为3,初始容量为6, // 追加元素后slice的长度为6,未超过其初始容量 // 返回原始slice,且追加操作会覆盖底层数组a的元素 slice = append(s1, 1, 1, 1) // slice = {1, 2, 3, 1, 1, 1} a = {1, 2, 3, 1, 1, 1}
-
如果超过被追加的slice的初始容量则将重新分配数组并拷贝原始数据,slice会执向这个新分配的数组,而slice最初指向的底层数组中的元素则不会发生改变。
a := [...]int{1,2,3,4,5,6} slice := a[:3] // slice的初始长度为3,初始容量为6, // 追加元素后slice的长度为7,超过其初始容量, // 重新分配一个容量足够的数组,slice重新指向这个新数组 // slice最初指向的底层数组a的元素不发生改变,且slice不再指向a slice = append(s1, 1, 1, 1, 1) // slice = {1, 2, 3, 1, 1, 1, 1} a = {1, 2, 3, 4, 5, 6}
-
-
copy(slice1, slice2)
函数将slice1
中的元素拷贝到slice2
中,拷贝操作会修改slice2的底层数组-
拷贝元素的个数以
slice2
的长度为基准// slice1的长度 大于或等于 slice2的长度,则拷贝的元素个数为slice2的长度 a := [...]int{1,2,3,4,5,6,7,8,9} slice1 := a[:3] // slice1={1,2,3} slice2 := a[3:5] // slice2={4,5} copy(slice1, slice2) // slice2={1,2}, a={1,2,3,1,2,6,7,8,9} // slice1的长度 小于 slice2的长度,则拷贝的元素个数为slice1的长度,剩余的元素以slice2的原始元素填充 b := [...]int{1,2,3,4,5,6,7,8,9} slice1 := b[:3] // slice1={1,2,3} slice2 := b[3:] // slice2={4,5,6,7,8,9} copy(slice1, slice2) // slice2={1,2,6,7,8,9}, a={1,2,3,1,2,6,7,8,9}
-
-
-
创建多维动态数组
// 创建动态二维数组 dx := 10 dy := 10 a := make([][]int, dy) for i := 0; i < dy; i++ { // 需要循环对二维数组的子数组进行初始化操作 a[i] = make([]int, dx) }
-
for
循环的range
形式可遍历切片或映射。var pow = []int{1, 2, 4, 8, 16, 32, 64, 128} // 当使用 for 循环遍历切片时,每次迭代都会返回两个值。 // 第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。 for i, v := range pow { fmt.Printf("2**%d = %dn", i, v) }
-
可以将下标或值赋予
_
来忽略range
的某个返回值。for i, _ := range pow for _, value := range pow // 若你只需要索引,忽略第二个变量即可。 for i := range pow
-
映射 (map)
- 以
key-value
形式存储数据- key必须是支持
==
或!=
比较运算的类型,不可以是函数、map或slice- Map查找比线性搜索快很多,但比使用索引访问数据的类型慢100倍
- Map使用
make()
创建,支持:=
这种简写方式
make(map[keyType]valueType, cap)
,cap表示容量,可省略- 超出容量时会自动扩容,但尽量提供一个合理的初始值
- 使用
len()
获取元素个数- 键值对不存在时自动添加,使用delete()删除某键值对
- 使用
for range
对map和slice进行迭代操作
for i, v := range mySlice {}
for k, v := range myMap {}
-
映射将键映射到值。映射的零值为
nil
。nil
映射既没有键,也不能添加键。// 声明了一个键为string类型,值为Vertex类型的映射 var m map[string]Vertex // make 函数会返回给定类型的映射 m = make(map[string]Vertex) // 映射的文法与结构体相似,不过必须有键名 var m = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, "Google": Vertex{ 37.42202, -122.08408, }, // 若顶级类型只是一个类型名,你可以在文法的元素中省略它, 即可以写成如下形式 // "Bell Labs": {40.68433, -74.39967}, // "Google": {37.42202, -122.08408}, }
-
修改映射
// 在映射 m 中插入或修改元素 m[key] = elem // 获取元素 elem = m[key] // 删除键值对 delete(m, key) // 通过双赋值检测某个键是否存在: // 若 key 在 m 中,ok 为 true ;否则,ok 为 false。 // 若 key 不在映射中,那么 elem 是该映射元素类型的零值。 elem, ok = m[key] // 若 elem 或 ok 还未声明,你可以使用短变量声明: elem, ok := m[key]
-
映射的映射
m := make(map[int]map[int]string, 5) // 第一次调用某个外层映射的key时需要先初始化 m[0] = make(map[int]string, 10) m[1] = make(map[int]string, 10) // 通过双赋值检测某个键是否存在: elem, ok = m[2] if !ok { // 若ok=false,则说明键2不存在,应先初始化 m[2] = make(map[int]string, 10) /* 省略对m[2]赋值填充操作 */ elem = m[2] }
方法和接口
方法
-
Go 没有类。但可以为结构体类型定义方法。
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqet(v.X * v.X + v.Y * v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
- 方法就是一类带特殊的 接收者 (receiver) 参数的函数。即方法只是个带接收者参数的函数。
- 方法接收者在它自己的参数列表内,位于
func
关键字和方法名之间。 - 不存在方法重载。
-
也可以为非结构体类型声明方法。
// 带 Abs 方法的数值类型 MyFloat。 type MyFloat float64 func (f MyFloat) Abs() float64 { return math Sqrt(v.X * v.X + v.Y * v.Y) } func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }
-
你只能为在同一包内定义的类型的接收者声明方法。即接收者的类型定义和方法声明必须在同一包内。
- 不能为其它包内定义的类型(包括
int
之类的内建类型)的接收者声明方法。
- 不能为其它包内定义的类型(包括
-
可以为指针接收者声明方法。即对于某类型
T
,接收者的类型可以用*T
的文法。(T
不能是像*int
这样的指针。)func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f } func main() { v := Vertex{3, 4} v.Scale(10) p := &v p.Scale(10) fmt.Println(v.Abs()) }
- 可以使用值或指针来调用方法,编译器会自动完成转换。
- 使用值接收者,那么
Scale
方法会对原始Vertex
值的副本进行操作。 - 必须用指针接受者来更改
main
函数中声明的Vertex
的值。
-
使用指针接收者的原因:
- 方法能够修改其接收者指向的值。
- 可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
-
所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。
-
Method Value 与 Method Expression
type TZ int func (a *TZ) Print() { fmt.Println("TZ") } func main () { var a TZ // Method Value // 通过声明的类型变量调用相应的方法,即 类型变量.方法名() a.Print() // Method Expression // 直接通过类型而不是变量来调用相应的方法,并将该类型的变量作为方法的第一个参数传给这个方法 // 即 类型.方法名(类型变量) (*TZ).Print(&a) }
- 从某种意义上来说,方法是函数的存在某种互通,因为receiver其实就是方法所接收的第1个参数 (强制性的)
-
如果外部结构和嵌入结构存在同名方法,则优先调用外部结构的方法
-
类型别名不会拥有底层类型所附带的方法
-
方法可以调用结构中的非公开字段
接口
-
接口类型 是由一个或多个方法签名定义的集合。
-
接口类型的变量可以保存任何实现了这些方法的值。
// 接口 type Abser interface { Abs() float64 } // 实现接口方法 type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) } type Vertex struct { X, Y float64 } // 注意,这里是*Vertex实现了Abser接口,而不是Vertex // 即Vertex没有实现Abser接口 // 接口调用不会做receiver的自动转换 func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } // 调用接口方法 func main() { var a Abser f := MyFloat(-math.Sqrt2) v := Vertex{3, 4} a = f // a MyFloat 实现了 Abser a = &v // a *Vertex 实现了 Abser // 下面一行,v 是一个 Vertex(而不是 *Vertex) // 所以没有实现 Abser。 // a = v fmt.Println(a.Abs()) }
- 类型通过实现一个接口的所有方法来实现该接口,无需显式声明实现了哪个接口,接口的隐式实现。这称为Structural Typing。
- 若这个声明了一个接口,但没有定义任何方法,则包中声明的所有
struct
都默认实现了这个接口
- 若这个声明了一个接口,但没有定义任何方法,则包中声明的所有
- 接口只有方法声明,没有实现,也没有数据字段。
- 接口支持匿名字段方法。
- 接口调用不会做receiver的自动转换
- 隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
- 因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
- 类型通过实现一个接口的所有方法来实现该接口,无需显式声明实现了哪个接口,接口的隐式实现。这称为Structural Typing。
-
接口可以匿名嵌入其他接口,或嵌入到
struct
中,以达到接口继承的效果。type Connecter interface { Connect() } // 效果类似于其他语言中的,USB接口继承Connecter接口 type USB interface { Name() string Connecter }
-
将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个副本的指针,既无法修改副本的状态,也无法获取指针
// 实现Connect接口和USB接口 func (pc PhoneConnecter) Name() string { return pc.name } func (pc PhoneConnecter) Connect() { fmt.Println("Connect:", pc.Name()) } func main() { // 接口也可实现类似OOP中的多态 pc := PhoneConnecter{"PhoneConnecter"} var a Connecter a = Connecter(pc) // PhoneConnecter对象实现了Connecter接口,因此可以将PhoneConnecter对象赋值给Connecter接口 // a中存储的是pc的副本,而不是pc本身 a.Connect() // 输出为 “Connect: PhoneConnecter” pc.name = "pc" // 修改对象的值 a.Connect() // 输出仍为 “Connect: PhoneConnecter” }
-
接口可以用作函数的参数或返回值。
func describe (a Abser) { fmt.Printf("(%v, %T)n", a, a) } func main() { f := MyFloat(-math.Sqrt2) v := Vertex{3, 4} // 打印结果为: (-1.4142135623730951, main.MyFloat) describe(f) // 打印结果为: (&{3 4}, *main.Vertex) describe(&v) }
- 在内部,接口值可以看做包含值和具体类型的元组:
(value, type)
- 接口值保存了一个具体底层类型的具体值。且接口值调用方法时会执行其底层类型的同名方法。
- 在内部,接口值可以看做包含值和具体类型的元组:
-
即便接口内的具体值为
nil
,方法仍然会被nil
接收者调用。因为保存了 nil 具体值的接口其自身并不为 nil。func (v * Vertex) Abs() float64 { if v == nil { fmt.Println("<nil>") return } return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main { var i Abser // describe(i) // (<nil>, <nil>) // i.Abs() // Runtime error var v *Vertex i = v describe(i) // (<nil>, *main.Vertex) i.Abs() // <nil> }
-
只有当接口存储的类型和对象都为nil时,接口才等于nil
func main () { var a interface{} fmt.Println(a == nil) // 返回 true var p *int = nil a = p fmt.Println(a == nil) // 返回 false }
-
nil
接口值既不保存值也不保存具体类型。 -
为
nil
接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
-
-
指定了零个方法的接口值被称为 空接口:
interface {}
package main import "fmt" func main() { var i interface{} describe(i) // (<nil>, <nil>) i = 42 describe(i) // (42, int) i = "hello" describe(i) // (hello, string) } func describe(i interface{}) { // fmt.Print 可接受类型为 interface{} 的任意数量的参数。 fmt.Printf("(%v, %T)n", i, i) }
- 空接口的值为
nil
。 - 空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法 。)
- 空接口被用来处理未知类型的值。
- 空接口的值为
类型断言
-
类型断言 提供了访问接口值底层具体值的方式:
t := i.(T)
。- 该语句断言接口值
i
保存了具体类型T
,并将其底层类型为T
的值赋予变量t
。 - 若
i
并未保存T
类型的值,该语句就会触发一个panic
。
- 该语句断言接口值
-
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
- 若
i
保存了一个T
,那么t
将会是其底层值,而ok
为true
。 - 否则,
ok
将为false
而t
将为T
类型的零值,程序并不会产生panic
。
package main import "fmt" func main() { var i interface{} = "hello" s := i.(string) fmt.Println(s) // hello s, ok := i.(string) fmt.Println(s, ok) // hello true f, ok := i.(float64) fmt.Println(f, ok) // 0 false f = i.(float64) fmt.Println(f) // panic: interface conversion: interface {} is string, not float64 }
- 若
-
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
-
类型选择与一般的
switch
语句相似,不过类型选择中的case
为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。switch v := i.(type) { case T: // v 的类型为 T case S: // v 的类型为 S default: // 没有匹配,v 与 i 的类型相同 }
- 类型选择中的声明与类型断言
i.(T)
的语法相同,只是具体类型T
被替换成了关键字type
。 - 在默认(即没有匹配)的情况下,变量
v
与i
的接口类型和值相同。
- 类型选择中的声明与类型断言
-
反射
-
反射可大大提高程序的灵活性,使得
interface{}
有更大的发挥余地 -
反射使用
TypeOf
和ValueOf
函数从接口中获取目标对象信息package main import ( "fmt" "reflect" ) type User struct { Id int Name string Age int } func (u User) Hello() { fmt.Println("Hello world.") } func Info(o interface{}) { // 获取接口的类型信息 t := reflect.TypeOf(o) // 打印类型的名称 fmt.Println("Type: ", t.Name()) // Kind()取出类型对象,接着判断是不是struct类型 if k := t.Kind(); k != reflect.Struct { fmt.Println("XX") return } // 打印所包含的字段以及字段的值 v := reflect.ValueOf(o) fmt.Println("Fields:") for i := 0; i < t.NumField(); i++ { // 获取字段对象 f := t.Field(i) // 获取字段对象的值 val := v.Field(i).Interface() fmt.Printf("%6s: %v = %vn", f.Name, f.Type, val) } // 打印类型所包含的方法信息 for i := 0; i < t.NumMethod(); i++ { m := t.Method(i) // 从打印出的信息可以看出 // Go对结构体方法的底层实现为:receiver是方法的第一个参数 fmt.Printf("%6s: %vn", m.Name, m.Type) } } func main() { u := User{1, "OK", 12} Info(u) // 打印除了 User 类型的信息如下 /* Type: User Fields: Id: int = 1 Name: string = OK Age: int = 12 Hello: func(main.User) */ }
-
反射会将匿名字段作为独立字段(匿名字段本质)
type Manager struct { // 嵌入了User,一个匿名字段 // 实际上,Go创建了一个Manager结构, // 该结构包含了一个 User类型的字段和一个string类型的字段 // 即匿名的User字段是一个独立的字段 User title string } func main() { m := Manager{User: User{1, "OK", 12}, title: "123"} t := reflect.TypeOf(m) // 打印字段 // reflect.StructField{Name:"User", PkgPath:"", Type:(*reflect.rtype)(0xd5f0e0), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:true} // ** Anonymous: true 表示该字段是一个匿名字段 fmt.Printf("%#vn", t.Field(0)) // reflect.StructField{Name:"title", PkgPath:"main", Type:(*reflect.rtype)(0xbd5bc0), Tag:"", Offset:0x20, Index:[]int{1}, Anonymous:false} fmt.Printf("%#vn", t.Field(1)) // 获取User字段中的Id字段 // 使用FieldByIndex()方法传入字段的索引来获取相应的字段 // ** User 相对 Manager而言的索引为 0 // ** Id 相对 User 而言的索引为0 // ** 因此向FieldByIndex()方法传入一个保存了索引的切片: // []int{ // 0, // User在Manager中的索引 // 0 // Id在User的索引 // } // 打印结果为: // reflect.StructField{Name:"Id", PkgPath:"", Type:(*reflect.rtype)(0xcd6500), Tag:"", Offset:0x0, Index:[]int{0}, Anonymous:false} fmt.Printf("%#vn", t.FieldByIndex([]int{0, 0})) // 获取 User 中的 Id 字段 fmt.Printf("%#vn", t.FieldByIndex([]int{0, 1})) // 获取 User 中的 Name 字段 }
-
想要利用反射修改对象状态,前提是
interface.data
是settable
,即pointer-interface
// 传入 指针才 可以修改原对象的值 func main() { x := 123 v := reflect.ValueOf(&x) // 传入 x 的地址 v.Elem().SetInt(999) fmt.Println(x) // 打印结果为 999 } // 考虑更复杂的情况 func main() { u := User{1, "OK", 12} Set(&u) fmt.Println(u) } func Set(o interface{}) { // ValueOf()返回 reflect.Value 类型 v := reflect.ValueOf(o) // 判断类型是否为 指针,并且判断其内容是否能被修改 if v.Kind() != reflect.Ptr || !v.Elem().CanSet() { // 如果不是指针,或者内容不能更改,则 执行 if 之后的操作时会报错,因此直接返回 fmt.Println("XXX") return } else { // 取出类型对象 // Elem()返回的也是reflect.Value,与 ValueOf()的返回类型相同,因此可以直接赋值 v = v.Elem() } // 根据 字段 名称获取字段对象 f := v.FieldByName("Name") // 若找到了这个字段,IsValid() 返回true,否则返回 false if !f.IsValid() { fmt.Println("BAD") return } // 并判断字段对象是否是string类型 if f.Kind() == reflect.String { // 设置新值 f.SetString("Bye Bye") } }
-
reflect.ValueOf()
是一个函数,可以视为反射的入口点。当拥有 non-reflection 的值 (例如string
或int
) 时,可以使用reflect.ValueOf()
获取它的reflect.Value
描述符。 -
Value.Elem()
是reflect.Value
的方法。因此,仅已定义reflect.Value
时,才可以调用它。使用Value.Elem()
来可以获取由原始reflect.Value
包装的值所指向的值 (reflect.Value
) 。// 个人理解: // 假设一个对象Obj,使用 reflect.ValueOf(Obj) 返回的只是Obj的一个引用对象, // 使用 reflect.ValueOf(Obj).Elem() 才能获得Obj本身 var i int = 3 var p *int = &i fmt.Println(p, i) v := reflect.ValueOf(p) fmt.Println(v.Interface()) // This is the p pointer v2 := v.Elem() fmt.Println(v2.Interface()) // This is i's value: 3
-
-
通过反射可以动态调用方法
package main import ( "fmt" "reflect" ) type User struct { Id int Name string Age int } func (u User) Hello(name string) { fmt.Println("Hello", name, ", my name is", u.Name) } func main() { u := User{1, "OK", 12} v := reflect.ValueOf(u) // 根据方法名称获取相应方法 mv := v.MethodByName("Hello") // 将传入方法的实参通过reflect.ValueOf()方法转化为reflect.Value类型 // 接着按方法定义的顺序放入一个reflect.Value类型的数组中 // 最后调用 Call() 方法,输入实参数组,即可执行相应方法 args := []reflect.Value{reflect.ValueOf("joe")} mv.Call(args) }
Stringer
-
Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值。// 可以理解为Java的Object类中的toString()方法 type Stringer interface { String() string } type Person struct { Name string Age int } func (p Person) String() string { return fmt.Sprintf("%v (%v years)", p.Name, p.Age) } func main() { // Arthur Dent (42 years) Zaphod Beeblebrox (9001 years) a := Person{"Arthur Dent", 42} z := Person{"Zaphod Beeblebrox", 9001} fmt.Println(a, z) }
Error
-
Go没有异常机制。只有错误类型 (Error),而且有
panic/recover
模式来处理错误package main import "fmt" func A() { fmt.Println("Func A") } func B() { // 使用 recover() 处理B()中引发的panic // 注意,必须在引发panic语句之前使用defer语句调用recover() // 因为引发panic的语句之后的所有语句都不会再执行 defer func() { // 当B()中没有引发panic时,recover() 会返回nil if err:= recover(); err != nil { fmt.Println("Recover in B") } }() // 引发panic panic("Panic in B") } func C() { fmt.Println("Func C") } func main() { A() // 顺利执行 B() // 引发painc C() // 若B()中的painc没有使用recover()处理,则此函数不会执行 }
panic
可以在任何地方引发,使用内建函数panic()
引发recover
只有在defer
调用的函数中有效
-
Go 程序使用
error
值来表示错误状态。// error 类型是一个内建接口 type error interface { Error() string }
-
通常函数会返回一个
error
值,调用的它的代码应当判断这个错误是否等于nil
来进行错误处理。 -
error
为 nil 时表示成功;非 nil 的error
表示失败。package main import ( "fmt" "time" ) type MyError struct { When time.Time What string } func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s", e.When, e.What) } func run() error { return &MyError{ time.Now(), "it didn't work", } } func main() { if err := run(); err != nil { // at 2009-11-10 23:00:00 +0000 UTC m=+0.000000001, it didn't work fmt.Println(err) } }
-
Reader
-
io
包指定了io.Reader
接口,它表示从数据流的末尾进行读取。package main import ( "fmt" "io" "strings" ) func main() { // 创建strings.Reader r := strings.NewReader("Hello, Reader!") // 以每次 8 字节的速度读取它的输出 b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n = %v err = %v b = %vn", n, err, b) fmt.Printf("b[:n] = %qn", b[:n]) if err == io.EOF { break } } } /* // 打印结果如下 n = 8 err = <nil> b = [72 101 108 108 111 44 32 82] b[:n] = "Hello, R" n = 6 err = <nil> b = [101 97 100 101 114 33 32 82] b[:n] = "eader!" n = 0 err = EOF b = [101 97 100 101 114 33 32 82] b[:n] = "" */
-
io.Reader
接口有一个Read
方法:func (T) Read(b []byte) (n int, err error)
Read
用数据填充给定的字节切片并返回填充的字节数和错误值。- 在遇到数据流的结尾时,它会返回一个
io.EOF
错误。
图像
-
image
包定义了Image
接口:package image type Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int) color.Color }
Bounds
方法的返回值Rectangle
实际上是一个image.Rectangle
,它在image
包中声明。color.Color
和color.Model
类型也是接口,但是通常因为直接使用预定义的实现image.RGBA
和image.RGBAModel
而被忽视了。这些接口和类型由image/color
包定义。
并发
- 并发主要由切换时间片来实现“同时”运行,在并行则是直接利用多核实现多线程的运行**,
- Go 可以设置使用核数,以发挥多核计算机的能力。
Go程 (Goroutine)
-
Go 程 (goroutine) 是由 Go 运行时管理的轻量级线程。即一个官方实现的超级线程池。
// goroutine 调用非常简单,使用go关键字调用一个函数 // 就新建一个线程来运行这个函数 package main import ( "fmt" "time" ) func main() { go Go() // 不定义以下函数时,函数会迅速结束而导致线程被关闭,从而看不到效果 time.Sleep(2 * time.Second) } func Go() { fmt.Println("Go Go Go!!!") }
- 号称高并发的原因:利用每个实例 4-5KB 的栈内存占用和由于实现机制而大幅减少的创建和销毁开销。
go f(x, y, z)
会启动一个新的Go程并执行f(x, y, z)
f
、x
、y
和z
的求值发生在当前Go程中,而f
的执行发生在新的Go程中
- Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。
- goroutine 奉行通过通信来共享内存,而不是共享内存来通信。GO通过
channel
机制通信来共享内存
信道 (channel )
-
channel
是 goroutine 沟通的桥梁,大都是阻塞同步的package main import "fmt" func main() { c := make(chan bool) go func() { fmt.Println("Go Go Go!!!") // 向信道传入数据 c <- true }() // 信道中没有内容 // 主程序会阻塞,直到有线程向信道中传入数据 <-c }
-
channel是带有类型的管道,你可以通过它用信道操作符
<-
来发送或者接收值。ch <- v // 将 v 发送至信道 ch v := <- ch // 从 ch 接收值并赋予 v
- 箭头就是数据流的方向
-
信道在使用前必须创建,通过
make()
函数进行创建ch := make(chan int)
- 默认情况下,发送和接收操作在另一端准备好之前都会阻塞。
- 这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。
-
信道可以是 带缓存的。将缓存长度作为第二个参数提供给
make
来初始化一个带缓存的信道:ch := make(chan int, 100)
-
缓存长度为0,即无缓存时,发送方发送数据后会阻塞,无缓存时 channel 必须保证发送方的数据被接收者接收后才能继续执行。
// 有缓存 func main() { c := make(chan bool) go func() { fmt.Println("Go Go Go!!!") <-c }() // 主程序执行到这里时会被阻塞 // 因为信道中还存在数据以等待接收方接收 c <- true } // 有缓存 func main() { c := make(chan bool, 1) go func() { fmt.Println("Go Go Go!!!") <-c }() // 主程序执行到这里时会继续执行,main函数结束执行 // 线程与信道会因main函数的结束而关闭 c <- true }
-
缓存未填满之前,这个 channel 是异步的。
-
仅当信道的缓冲区填满后,向其发送数据时才会阻塞。
-
当缓冲区为空时,接受方会阻塞。
-
-
发送者可通过
close
关闭一个信道来表示没有需要发送的值了。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) for i := range c { fmt.Println(i) } }
-
接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完:
v, ok := <- ch
- 之后
ok
会被设置为false
。
- 之后
-
循环
for i := range c
会不断从信道接收值,直到它被关闭。- 若没有在代码的某个地方使用
close()
关闭 channel,则引发死锁,并会抛出panic
。
- 若没有在代码的某个地方使用
-
只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序panic。
-
信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭。
-
-
channel 可以设置单向或双向通道
-
设置CPU的核数
import "runtime" // GOMAXPROCS()函数设置CPU核数 // NumCPU()返回设备的CPU总核数 // 必须在使用goroutine之前调用 runtime.GOMAXPROCS(runtime.NumCPU())
Select 语句
-
select
语句使一个 Go 程可以等待多个通信操作。 -
select
会阻塞到某个分支可以继续执行为止,这时就会执行该分支。 -
当多个分支都准备好时会随机选择一个执行。
-
当
select
中的其它分支都没有准备好时,default
分支就会执行。- 为了在尝试发送或者接收时不发生阻塞,可使用
default
分支:
package main import ( "fmt" "time" ) func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x+y case <- quit: fmt.Println("quit") return default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
- 为了在尝试发送或者接收时不发生阻塞,可使用
sync
-
sync.Mutex
- 信道非常适合在各个 Go 程间进行通信。
- 但是如果线程间并不需要通信且仍能保证Go程能够访问一个共享的变量,从而避免冲突,可以使用互斥锁 (Mutex)
- 我们可以通过在代码前调用
Lock
方法,在代码后调用Unlock
方法来保证一段代码的互斥执行。 - 我们也可以用
defer
语句来保证互斥锁一定会被解锁。
- 我们可以通过在代码前调用
-
sync.WaitGroup()
package main import ( "fmt" "runtime" "sync" ) func Go(wg *sync.WaitGroup, index int) { a := 1 for i := 0; i < 10000000; i++ { a += i } fmt.Println(index, a) // 一个任务完成,用Done()标记 wg.Done() } func main() { runtime.GOMAXPROCS(runtime.NumCPU()) // 创建任务组 wg := sync.WaitGroup{} // 设置任务数 wg.Add(10) for i := 0; i < 10; i++ { go Go(&wg, i) } wg.Wait(); }
- 使用
sync.WaitGroup()
创建一个任务组,调用Add()
设置任务组中要完成的任务数 - 使用
Wait()
阻塞程序,等待任务组结束 - 每完成一次任务,用
Done()
进行标记,总的待完成任务数就会减少一个 - 当待完成任务数减至0,任务组结束。
- [注]
sync.WaitGroup
在传参时采用的是值拷贝,因此需要传入指针。
- 使用
拓展阅读
- Go语言之旅
- Go编程基础 (视频)
- Go在谷歌:以软件工程为目的的语言设计
- 评:为什么我不喜欢Go语言式的接口
- Concurrency Is Not Parallelism
- Go语言_并发篇
- goroutine背后的系统知识
- Advanced Go Concurrency Patterns
- What exactly does runtime.Gosched do?
最后
以上就是甜蜜向日葵为你收集整理的go语言入门笔记语法基础方法和接口并发拓展阅读的全部内容,希望文章能够帮你解决go语言入门笔记语法基础方法和接口并发拓展阅读所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复