摘要:Go 语言的优势不必多说,通过本篇文章,让我们花时间来掌握一门外语,Let's Go!
关键字:Go 语言,闭包,基本语法,函数与方法,指针,slice,defer,channel,goroutine,select
Go 开发环境
针对 Go 语言,有众多老牌新厂的 IDE.本地需要下载 Go 安装包,无论 Windows 还是 Linux,安装差不多.这里推荐手动安装方式,
安装包下载地址: https://www.golangtc.com/download
解压缩存放相应位置(linux 可选位置 usr/local),设置环境变量 GOROOT 指向 Go 安装目录,并将 GOROOT/bin 目录放置 PATH 路径下
设置环境变量 GOPATH,这个目录就是告诉 Go 你的 workspace,一个 Go 工程对应一个 workspace.每个 workspace 内的结构一般包含 src,pkg,bin 三个目录,其实是仿照 Go 安装目录,建立了一个独立的 Go 环境,可以执行 bin 中我们自己构建的命令.
Go 语言开发相当于堆积木,Go 安装目录下已有的内容为积木的底座,为了防止我们构建的包与标准库有区分,我们需要独立的命名空间,例如普遍采用开发者本人 github 账户的命名空间.
GOPATH 和 GOROOT
为了避免混淆加深印象,这里再针对 GOPATH 和 GOROOT 进行一个区分详解.
GOROOT 是 GO 的安装目录,存放 GO 的源码文件.
GOPATH 是我们使用 GO 开发的工作空间,类似于 workspace 的概念,但由于 GO 是高复用型,就像叠积木那样,我们开发的 Go 程序与 GO 标准库中的无异,我们编译的 Go 命令也与 GOROOT/bin 中的源码命令同级,因此对于一个 GO 工程,我们就要创建一个工作间添加到 GOPATH 中去,这个工程中的新开发的包都在该工作间目录结构下.
一个 GO 工程工作间的目录结构包括: bin,src,pkg.
先说 src 目录,该目录是我们开发的 Go 代码的所在地,bin 是我们通过 go install 将 Go 源码编译生成的一个可执行文件的存放地,pkg 是 go get 获取的第三方依赖库,源码中使用到的第三方依赖包都会从 pkg 中去寻找,当然了也会在 $GOROOT/pkg 标准库中寻找.对了,在我看来,库和包的概念没有什么差异.
我们还可以直接使用 go build 编译我们的源码,那将会直接在源码位置生成一个可执行文件,而不是像 go install 那样将该可执行文件安装在 $GOPATH/bin 目录下.
我们应该将 GOROOT 和 GOPATH 均放到 $HOME/.profile 中去作为环境变量,同时要将 $GOROOT/bin 以及 $GOPATH/bin 均放到 PATH 中,以方便我们在任何位置直接访问 go 的命令以及我们自己生成的 go 命令.
hello, world
你的 helloworld 一定要交给我 ✿◡‿◡
可以来 https://play.golang.org/ 玩一玩,但我不推荐,下面我们来搞一个完整 helloworld.
我们已完成上面介绍的开发环境的搭建,然后我们进入到 GOPATH 目录下,并进入 src 下我们设置的命名空间目录,
liuwenbin@ubuntu1604:~/workspace/src/github.com$ mkdir hello
liuwenbin@ubuntu1604:~/workspace/src/github.com$ cd hello/
liuwenbin@ubuntu1604:~/workspace/src/github.com/hello$ vi hello.go
我们创建了一个包 hello,在包内又创建了一个 hello.go 文件,下面是具体 helloworld 代码编写内容:
package main
import "fmt"
func main() {
fmt.Println("hello,world")
}
这里简单啰嗦两句.
每个可执行的 Go 程序都需要满足:1,有一个 main 函数,2,程序第一行引入了 package main.
我们的代码满足可执行条件,下面继续:
liuwenbin@ubuntu1604: ~ / workspace / src / github.com / hello$ go install
执行 go install 将 Go 程序代码打包为可执行文件,保存在 GOPATH/bin 下,
liuwenbin@ubuntu1604:~/workspace/src/github.com/hello$ ls ~/workspace/bin
hello
经过检查,证实了可执行文件 hello 已被自动 install 成功.
liuwenbin@ubuntu1604: ~ / workspace / src / github.com / hello$~ / workspace / bin / hello hello,
world
执行成功.
liuwenbin@ubuntu1604:~/workspace/src/github.com/hello$ export PATH=$HOME/workspace/bin:$PATH
liuwenbin@ubuntu1604:~/workspace/src/github.com/hello$ cd
liuwenbin@ubuntu1604:~$ hello
hello,world
将 GOPATH/bin 加入到 PATH 当中,然后在任何位置键入 hello 都会执行我们的程序.
IDE
IDE 就选择官方主推的 JetBrains 家的 goLand 吧,亲测好用,至于激活码什么的,谷歌百度你懂的.
goLand 可以帮助我们:
时刻管理 Go 工程目录结构:包括源码位置,包管理一目了然,SDK 或第三方依赖显而易见.
统一管理环境变量,作用域可以是全局,工程以及模块.
代码开发语法高亮,自动补全,代码候选项,源码搜索,文件对比,函数跳转,初步代码结构审查,格式化,根据你的习惯设置更方面的快捷键,设置 TODO,任务列表.
代码编译执行可视化,断点调试 bug 易于追踪.
IDE 内部直接调取终端,不用切换.
可集成各种插件扩展功能,例如版本控制 Git,github 客户端,REST 客户端等.
多种数据库连接客户端可视化.
更炫酷的界面,多种配色主题可选.
自定义宏小工具集成到 IDE,更加方便扩展.
Go 基本语法
包
每个 Go 程序都是由包组成,程序的入口为 main 包,bin 中的自定义命令就是一个 Go 程序,入口为 main 包的 main 函数,该入口程序文件还会依赖其他库的内容,可以是标准库,第三方库或者自己编写的库.这时要通过关键字 import 导入.而导入的库的程序文件的包名一定是导入路径的最后一个目录,例如 import "math/rand","math/rand" 包一定是由 package rand 开始.
package main
import (
"fmt"
"math/rand"
)
func main() {
fmt.Println("rand number ", rand.Intn(10))
}
package 声明当钱包,import 导入依赖包,这与 Java 很相似.另外这里的 rand.Intn 方法也与其他语言一样是一个伪随机数,根据种子的变化而变化,如果种子相同,则生成的 "随机数" 也相同,这其实就是一种哈希算法.
打包:观察代码可以发现,这里的 import 结构与上面编写 helloworld 的 import "fmt" 相比发生了变化.这里导入了两个包,用圆括号组合了导入,这种方式称为 "打包".
它等同于
import "fmt"
import "math"
但是仍旧提倡使用打包的方式来导入多个包.
下面贴一个官方包的 api 地址: https://go-zh.org/pkg/ ,这里面的包除了标准库,还有一些其他的附加包,新增包等,我们都可以通过上面提到的方式进行导入,在我们自己的代码中复用他们.
函数
这里面最大的不同之处在于函数的参数类型是在变量名的后面的,相应的,返回值的类型也在参数列表的后面.
package main
import "fmt"
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("hello", "world")
fmt.Println(a, b)
}
相同类型的变量可以只在最后用一个类型表示.这里声明了返回值为两个 string 类型数据(string, string).我们可以在这里通过声明返回值的类型返回任意数量的值.
Go 语言的函数与其他语言最大的不同除了数据类型在变量名后面进行声明以外,函数的返回值也是可以被命名的.上面讲到了可以直接定义返回值的数量以及数据类型,除此之外,还可以进一步对返回值进行定义.
package main
import "fmt"
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func main() {
fmt.Println(split(17))
}
(x, y int) 定义了返回值的数量,类型,以及变量名,这些变量名在方法体内部处理进行赋值,方法在返回时 return 后面无需任何内容即可返回 x 和 y 的值.
变量
变量的声明定义创建以及初始化需要关键字 var
package main
import "fmt"
var i, j int = 1, 2
func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}
注意,var 后面加变量名,然后是变量类型,后面还可以直接等号加入初始化内容.var 定义的变量的作用域可以在函数内也可在函数外.
函数内部的短声明变量 :=
在函数内部,在明确类型的情况下,也即变量声明即初始化情况下,可以使用短声明变量的方式,省略了 var 关键字以及变量类型,例如 k := 3,与 var k int = 3 等同,但要注意同一个变量不能被 var 或者:= 声明两次,也即 var 或:= 只能作用于新变量上.但是要注意只有函数内部才可以使用.函数外每条语句必须是 var func 等关键字为开头.
基本类型
Go 语言的数据基本类型包括 bool,string,int,uint,float,complex.其中 bool 是布尔类型不多介绍,string 是字符串,注意开头 s 是小写.int 根据长度不同包括 int8 int16 int32(rune) int64.uint 为无符号整型类型,无符号整型就代表只能为正整数,根据长度也分为 uint8(byte) uint16 uint32 uint64 uintptr.float 浮点型包括 float32 float64,复数类型包括 complex64 complex128,Go 语言支持了复数类型,这是 java 所不具备的.
项 布尔类型 字符串 整型 无符号整型 浮点型 复数类型
关键字 bool string int uint float complex
包含 bool string int8/16/32(rune)/64 uint8(byte)/16/32/64 uintptr float32/64 complex64/128
值 true, false 字符串 正负整数 正整数 小数 复数
零值 false "" 0 0 0(注意浮点的零值也为 0,而不是 0.0) (0+0i)
注意:int,uint,uintptr 类型在 32 位系统上的长度是 32 位,在 64 位系统上是 64 位,经过测试可知,Go Playground 后台是 32 位系统,因为 int 溢出了.另外,复数的运算一般都是与数学运算相关联,与业务处理关系较少.所以常用的类型就是 bool,string,int,float 四种.
Go 数据类型的转换直接采用以类型关键字为函数名,参数为待转换变量的方式即可.
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
函数内,直接使用短声明变量的方式
i := 42
f := float64(i)
u := uint(f)
Go 作为功能强大的新型编程语言,也具备自动类型推导的功能.
package main
import "fmt"
func main() {
var v = 1
k:=3.1
fmt.Printf("v is of type %T\n", v)
fmt.Printf("k is of type %T\n", k)
}
v is of type int
k is of type float64
类型推导:即在未显示指定变量类型的时候,可以根据赋值情况来自动推导出变量类型.然而,当你在前面已经通过赋值推导出某变量的类型以后,再改变其值为其他类型就会报错.
另外,在 Println 表达式中,%v 代表值的默认形式,%T 代表值的类型.
常量
使用关键字 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)
}
Hello 世界
Happy 3.14 Day
Go rules? true
此外,还有数值常量,数值常量往往是高精度的值,例如
const (
Big = 1 << 100
Small = Big >> 99
)
我们看到了位运算符 <<和>>,这里再复习一些位运算的知识.首先定义运算符左侧为原值,右侧为操作位数,运算符 "<<"代表左移,即将原值用二进制方式表示,然后将其中的值左移相应位数,再还原回十进制表示结果,反之则为运算符">>".那么用一种更加容易理解的方式来讲是左移即为乘以 2 的 n 次方,n = 操作位数,右移即除以 2 的 n 次方,n = 操作位数.
循环
同样的,关键字也为 for,for 的循环结构也与 java 相似,有初始化语句,循环终止条件,以及后置变化语句(例如自增自减).不一样的地方是,这个循环结构没有圆括号,初始化变量的作用域在整个循环体内,另外,for 也可以相当于其他语言的 while 使用,即去掉初始化语句和后置变化语句,只有一个循环终止条件,同样没有圆括号,但是循环体必须用花括号包围 {}.下面看例子.
for i := 0; i < 10; i++ {
sum += i
}
// 如果初始化语句和后置变化都去掉的话,则省略分号;
for ; sum < 10; {
sum += sum
}
// 相当于while
for sum < 10 {
sum += sum
}
初始化语句和后置变化语句都可以被省略,如果终止条件语句也被省略,循环就成了死循环.简洁的表示为
for { }
判断语句
Go 的 if 语句也不要用圆括号括起来,但方法体还是要用花括号 {} 的.与 for 一样,if 语句也可以包含一个初始化语句,然后再接判断表达式,这个初始化变量的作用域仅在 if 语句内,包括与其成对的 else 语句.
package main
import (
"fmt"
"math"
)
func pow(x, n, lim float64) float64 {
if v := math.Pow(x, n); v < lim {
return v
}
return lim
}
func main() {
fmt.Println(
pow(3, 2, 10),
pow(3, 3, 20),
)
}
pow 函数复写了 math.pow(x,y float64),加入了一个限制 lim 参数,当加幂值结果超过 lim 的时候,返回 lim,未超过则返回结果.通过这个例子,可以看到 if 语句在判断表达式中加入了初始化语句.此外,main 函数中的 Println 输出多条信息的方式,与前面介绍的 import 打包,const 数值常量集都很相似,可以说明这种形式是 Go 的一种编程习惯.
switch 的逻辑同其他语言并没有太多出入.
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
输出:Good evening.
由于现在已经 17:37,确实过了 17 点,所以输出为晚上好是合理的.此外,这里面引用到了 time 包,获取了当前时间,time.Now(),同样的,这可以通过上面给出的标准包文档查看.
延迟执行
这是一个 Go 语言独特的内容,关键字为 defer,意思是 defer 声明的一行代码要它的上层函数执行完毕返回的时候再执行.
package main
import "fmt"
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
world
hello
defer 关键字的声明使得 world 的输出虽然写在 hello 输出的上方,但必须等待 hello 输出完毕以后再输出.
defer 下压栈
当 defer 关键字声明的代码不止一行的时候,就引入了 defer 下压栈的特性,这也是 Go 比骄强大的地方,根据下压栈的特点,后压入的那行代码会在上层函数执行完毕后先执行.
package main
import "fmt"
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
输出:
counting
done
9
8
7
6
5
4
3
2
1
0
指针
指针我们都熟悉,大学时期学习 C 语言的时候折磨我们好久,Go 语言中也支持指针,但它并不像我们印象中那么恐怖,因为它并不包含 C 的指针运算.
指针保存了变量的内存地址.
& 符号会【生成】一个指向其作用对象的指针.
* 符号表示指针指向的【底层的值】.
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // p为i的指针
fmt.Println(*p) // 通过指针显示的是i的值
*p = 21 // 通过指针修改的是i的值
fmt.Println(i)
p = &j // 没有:了,因为是第二次修改值,不是初始化,将p改为j的指针
*p = *p / 37 // 通过指针操作的是j的值,除以37的结果重新通过指针赋给j
fmt.Println(j)
}
73
21
42
结构体 struct
package main import "fmt"type Vertex struct {
X int Y int
}
func main() {
fmt.Println(Vertex {
1,
2
})
}
以 type 开头,用来声明创建一种类型,创建以后可以被 var 声明该类型的变量.
关键字 struct,它相当于一个字段的集合,使用方式与基本类型相似,也是写在变量名后面.
v := Vertex{1, 2}
fmt.Println(v.X)
// 输出1
可以用短声明方式定义一个变量,通过点获得相关字段的内容.
p := &v
p.X = 1e9
fmt.Println(v)
// 输出{1000000000 2}
结构体同样可以像一个普通变量那样有指针,通过指针可以操作结构体字段.
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 类型为 Vertex
v2 = Vertex{X: 1} // Y:0 被省略
v3 = Vertex{} // X:0 和 Y:0
p = &Vertex{1, 2} // 类型为 *Vertex
)
func main() {
fmt.Println(v1, v2, v3, p)
}
// 输出:{1 2} {1 0} {0 0} &{1 2}
通过对结构体的字段操作,用一个变量来接受,可以重新组装新的结构体.以上代码中,使用 var 圆括号列表的方式,分别定义了 v1,v2,v3 和 p 四个变量,前三个对原结构体的数据进行了不同的赋值,p 为结构体的指针,输出也是带有 & 符号的结果.
数组和 slice
类型 [n]T 是一个有 n 个类型为 T 的值的数组.
数组的声明方式:
var a [10]int
定义一个数组变量,变量名为 a,长度为 10,数据类型为 int.Go 的数组与其他语言一样,都是定长的,一旦声明无法自动伸缩,但 Go 提供了更好的解决方案,就是 slice.
[]T 是一个元素类型为 T 的 slice.
slice 与数组最大的区别就是不必定义数组的长度,它可以根据赋值的长度来设定自己的长度,而不是提前设定.
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
fmt.Println(len(s))
fmt.Println("s ==", s)
for i := 0; i < len(s); i++ {
fmt.Printf("s[%d] == %d\n", i, s[i])
}
}
6
s == [2 3 5 7 11 13]
s[0] == 2
s[1] == 3
s[2] == 5
s[3] == 7
s[4] == 11
s[5] == 13
通过 len(s) 方法可以获得 slice 当前的长度.此外,上面代码中 Printf 中的格式化字符串的 & d,与 C 和 java 相同,都代表是整型数字.
数组和 slice 都可以是二维的.
slice 可以内部重新切片 s[lo:hi],lo 是低位,hi 是高位,hi>lo,若 hi=lo 则为空.
slice 除了上面的直接字面量赋值以外,还可以通过 make 创建.func make([]T, len, cap) []T
a := make([]int, 5) // len(a)=5 cap(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
// slice内部继续切片,空为从头,这不是下标的概念,而是个数的概念.
b = b[:cap(b)] // len(b)=5, cap(b)=5,b原来的容量为5,重切以后的切片是b[:5]意思是b的前五个数组成的切片,顺序不变.
b = b[1:] // len(b)=4, cap(b)=4,b原来的容量为5,重切以后的切片是b[1:]意思是除去前一个数(即第一个数)剩余的数组成的切片,顺序不变.
slice 的零值是 nil.Go 语言中的空值用 nil 来表示.一个 nil 的 slice 的长度和容量是 0.
slice 是通过 append 函数来添加元素.
range 遍历 slice 和 map
package main
import "fmt"
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2^%d = %d\n", i, v)
}
}
2^0 = 1
2^1 = 2
2^2 = 4
2^3 = 8
2^4 = 16
2^5 = 32
2^6 = 64
2^7 = 128
注意使用 range 的格式,返回值 i,v 分别代表了当前下标,下标对应元素的拷贝.
利用下划线_作占位符
当我们不需要当前下标的时候,可以将 i 用下划线_代替.然而如果只想要下标,可以把, v 直接去掉,不必加占位符.
map
与其他语言一样,map 也是一个映射键值对的数据类型.在 Go 中,与 slice 相同的是,它也需要使用 make 来创建,零值为 nil,注意值为 nil 的 map 不可被赋值.
package main
import "fmt"
type Vertex struct {
Lat, Long float64
}
// []中的为key数据类型,[]外面紧跟着的是value的数据类型,这里value的数据类型是上面type新创建的struct类型.
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
注意,map 在赋值时必须有键名.赋值的时候可以省略类型名
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
修改 map 中一个元素的内容:m[key]=elem
获得 map 中的一个元素:elem=m[key]
删除元素:delete(m,key)
elem, ok = m[key] 判断 key 是否存在 m 中,如果没有 ok 为 false,elem=map 零值,如果有 ok 为 true,elem 为 key 对应的 value
package main
import "fmt"
func main() {
m := make(map[string]int)
// 赋值key为"Answer",值为42.
m["Answer"] = 42
// 检查key是否存在,此时是存在的.那么v=42.
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
// 删除key
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
// 注意下面没有冒号了,因为是第二次赋值,再次检查key是否存在,此时是不存在的.那么v=0,整型的零值是0.
v, ok = m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}
输出:
The value: 42 Present? true
The value: 0
The value: 0 Present? false
与 JavaScript 似曾相识?
函数值概念:函数也是值,可以像其他值一样被传递和操作,例如可以当作函数的参数和返回值,这非常强大,跟 JavaScript 的思想如出一辙.
闭包:当时学习 JavaScript 闭包概念的时候也是蒙圈,没想到 Go 语言也支持,下面我们再来复习一下.闭包是基于函数值概念的,它引用了函数体之外的变量,可以对该变量进行访问和赋值.
func adder() func(int) int {
sum: =0
return func(x int) int {
sum += x
return sum
}
}
adder 函数的返回值类型为 func(int) int,好叼哦.也就是说返回的是另一个函数,它没有函数名(有点匿名内部类的意思哈),该函数只有一个 int 型的参数,返回值为 int.继续我们来看函数体,声明并赋值给 sum 变量为 0,然后是 return 阶段的确返回了符合上面定义的一个函数.在这个返回函数的函数体内,我们直接使用到了外部的 sum 变量,对其进行了操作并返回,这就是闭包的概念(一个无名函数小子和一个大家闺秀变量的感情纠葛).
func main() {
pos,
neg: =adder(),
adder() for i: =0;
i < 10;
i++{
fmt.Println(pos(i), neg( - 2 * i), )
}
}
接着,我们在 main 函数中调用这个 adder 函数.首先声明并初始化变量 pos 和 neg 为 adder 函数值,然后定义一个循环,直接调用 pos 和 neg 变量并传参,相当于调用了 adder 函数.
以上就是函数值和闭包的概念的学习,根据以上知识完成斐波那契数列的练习:
package main
import "fmt"
// fibonacci 函数会返回一个返回 int 的函数.
func fibonacci() func() int {
back1,back2 := 0,1
return func() int{
temp := back1
// 重新赋值back1和back2的值,下面是关键代码
back1,back2 = back2, (back2+back1)
return temp
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
类的概念?Go 的方法
Go 中是没有类的概念的,但是可以实现与类相同的功能.在 java 中,如果一个方法属于一个类,我们是如何做的?直接在类文件中加入方法即可,外部调用的时候,我们可以通过类的实例来调用该方法,如果是静态类的话,可以直接通过类名调用.
那么在 Go 中是如何实现 "类的方法" 的?
方法,属于一个 "东西" 的函数被称为这个 "东西" 的方法.Go 中是通过绑定的方式来处理的.我们先 type 定义一个结构体类型
type Vertex struct {
X, Y float64
}
这段代码我们上面已经学习过了,应该没有任何疑问.接着,我们要创建一个 Vertex 类型的变量(java 中称为对象的实例,就看你怎么解读了),并且让这个变量拥有一个方法.
func(v * Vertex) Abs() float64 {
return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
我们慢慢来看,
正常的函数是 func Abs() float64, 但我们在 func 和函数名之间加入了一个东西,定义了一个 Vertex 指针类型的变量 v.
在该函数体中,我们可以直接把 v 当作参数来使用,因为 v 是一个结构体类型的对象指针,所以 v 可以调用结构体中的各个字段.(TODO:Go 语言圣经继续深入研究这一部分)
func main() {
v := &Vertex{3, 4}
fmt.Println(v.Abs())
}
我们在 main 函数中可以直接通过变量 v 调用上面的函数 Abs,此时函数 Abs 就是 v 的方法了.
注意,type 关键字可以创建任意类型
type MyFloat float64
MyFloat 就是一个类型,我们在下面可以直接使用,该类型仍然可以与函数绑定.
下面我们再来重申区分一下函数和方法:
函数是 func 后面直接跟函数名,跟任何类型都无关系.
方法是 func 后面加入类型的变量,然后再加函数名,这个类型的变量本身也是该方法的参数,同时该方法是属于该类型的,但要用类型的对象来调用(Go 没有静态方法).
上面我们分别使用了结构体指针和自定义类型,使用指针的好处就是可以避免在每个方法调用中拷贝值同时可以修改接收者指向的值.
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: % v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: % v, Abs: %v\n", v, v.Abs())
}
/* 输出:
Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25
*/
当 Scale 使用 Vertex 而不是 * Vertex 的时候,main 函数中调用 v.Scale(5) 没有任何作用,此时输出结果应该毫无变化 "After scaling: &{X:3 Y:4}, Abs: 5".因为接收者为值类型时,修改的是 Vertex 的副本而不是原始值.
当我们修改 Abs 的函数接收者为 Vertex 的时候,并不会影响函数执行结果,原因是这里只是读取 v 而没有修改 v,读取的话无论是指针还是值的副本都不受影响,但是修改的话就只会修改值的副本,然而打印程序打印的是原始值.
接口
Go 的接口定义了一组方法,同样的没有实现方法体.接口类型的值可以存放任意实现这些方法的值.
首先,我们来看一个接口是如何定义的:
type Abser interface {
Abs() float64
}
然后我们再 type 创建两个类型,并绑定与接口相同方法名,参数,返回值的方法.
type MyFloat float64 func(f MyFloat) Abs() float64 {
if f < 0 {
return float64( - f)
}
return float64(f)
}
type Vertex struct {
X,
Y float64
}
func(v * Vertex) Abs() float64 {
return math.Sqrt(v.X * v.X + v.Y * v.Y)
}
上面我们定义了一个 MyFloat 类型和 Vertex 类型,在他们类型定义的下方都绑定了方法 Abs() float64,并有各自具体的实现.
func main() {
var a Abser
f := MyFloat(-math.Sqrt2)
a=f
fmt.Println(a.Abs())
}
//输出:1.4142135623730951
最后我们在 main 函数中去做具体操作,首先定义一个接口类型的值 var a Abser,然后我们先定义一个刚刚我们创建的 MyFloat 类型的值 f,将 f 赋值给 a,接口类型值 a 就存放了实现了接口方法 Abs 的 MyFloat 类型的值 f,最后我们去用 a 调用 Abs 方法,实际上调用的是 f 的 Abs 方法.
func main() {
var a Abser
v := Vertex{3, 4}
a = &v // a *Vertex 实现了 Abser
//a = v //等于v为什么不行,而必须是v的指针?
fmt.Println(a.Abs())
}
// 输出:5
然后我们来测试上面创建的另一个类型 Vertex,创建并初始化 Vertex 的值变量 v,将 v 的指针赋值为接口类型值 a,用 a 调用 Abs 方法,实际上调用的是 * Vertex 的 Abs 方法.
解答代码注释里的问题:因为我们实现接口方法的时候,绑定的是 * Vertex 而不是 Vertex,所以必须是 Vertex 的指针类型才拥有该方法,如果使用 Vertex 的值类型而不是指针,则会报错 "Vertex does not implement Abser".
Go 的接口属于隐式接口,类型通过实现接口方法来实现接口,方法也不必像 java 那样必须全部实现,没有显示声明,也就没有 "implements" 关键字.隐式接口解耦了实现接口的包和定义接口的包:互不依赖.
Stringer 接口
type Stringer interface {
String() string
}
常用的一个接口就是 Stringer 接口,它就如同 java 复写 toString 方法,输出对象的时候不必显示调用 toString,而是直接输出该接口的实现方法.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func (p Person) String() string {
// Sprintf 根据于格式说明符进行格式化并返回其结果字符串.
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}
func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}
// 输出:Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
我们看到新创建的类型 Person,它实现了 Stringer 的 String 方法(注意这里开头 S 是大写,与基本类型 string 不同).我们在 fmt.Println 的时候会默认调用该方法输出.
发现一个问题:Person 的实现接口方法以及 main 函数初始化 Person 调用 String 方法输出,这整个过程都没有出现真正的原接口名称 "Stringer"!这是非常有意思的部分,我们日后要注意.
那么原因是什么?我们来分析一下,上面我们调用接口方法的时候是需要利用接口类型值来调用的(如 var a Abser,a.Abs()).然而这里由于特殊原因(跟其他语言一样,大家都不会显示调用 toString 吧),并没有显式使用
接口类型变量,所以全文没有出现接口名称,这种情况在之后的 Go 工作中,应该不少见,还望注意.
error
error 在 Go 中是一个接口类型,与 Stringer 一样.
type error interface {
Error() string // 注意接口方法为Error()
}
一般函数都会返回一个 error 值,调用函数的代码要对这个 error 进行判断,如果为空 nil 则说明成功,如果不为空则需要做相应处理,例如报告出来.
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
Go 语言的 io 包中的 Reader 接口定义了从数据流结尾读取的方法,标准库中有很多包对 Reader 的这个接口方法进行了实现,包括文件,网络连接,加密,压缩等.该 Read 方法的声明为:
func(T) Read(b[] byte)(n int, err error)
下面使用 strings 包中的 NewReader 实现方法,它的介绍是:
func NewReader(s string) *Reader
NewReader returns a new Reader reading from s. It is similar to bytes.NewBufferString but more efficient and read-only.
会返回一个新的读取参数字符串的 Reader 类型.
package main
import (
"fmt"
"io"
"strings"
)
func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)//定义一个8位字节数组用来存上面的字符串
for {// 无限循环here
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])// b[:n]输出字节数组b的所有值,n是最大长度
if err == io.EOF {//随着不断循环,上面字符串已经读取完毕,当前字节数组为空,返回EOF(已到结尾)错误
break// 手动置顶break跳出循环
}
}
}
HTTP web 服务器
主要通过调用标准库的 http 包来实现.
package http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
下面我们创建一个结构体类型,实现该接口方法:
package main
import (
"fmt"
"net/http"
"log"
)
type Hello struct{}
func (h Hello) ServeHTTP(
w http.ResponseWriter,
r *http.Request) {
fmt.Fprint(w, "hello, Go server!")
}
func main() {
var h Hello
err := http.ListenAndServe("localhost:4000", h)
if err != nil {
log.Fatal(err)
}
}
在编写这段代码过程中,针对 goland 有两点收获:
import 内容完全不必手写,goland 会全程帮助你自动补全代码.
每次 goland 自动补全的时候都会自动格式化你的代码.
此外,我们发现对于 Handler 接口的 ServeHTTP 方法,我们自定义的结构体类型 Hello 全程并未见到 Handler 的字样,这个问题我在前面已经研究过,这里的 Hello 的实例 h 直接作为参数传给了 http 的 ListenAndServe 方法,可能在这个方法内部才会有 Handler 的出现.
因此,这种我实现了你的接口方法,但根本不知道你是谁的情况在 Go 中十分常见.
我想深层原因就是 Go 并没有强连接的关系例如继承,显式 implements 关键字去实现,这是一种解耦的,松散的实现接口方法的方式,才会有这种情况的出现.这个特点称不上好坏,但需要适应.
下面让我们直接在 goland 中将该文件 run 起来,会发现在控制台中该程序处于监听状态.然后我们可以
通过浏览器去访问 http://localhost:4000/
通过终端 curl http://localhost:4000/
通过 goLand 的 REST client 输入网址 http://localhost:4000/,get 的方式 run
总之,最终会得到结果:hello, Go server! 一个简单的 web 服务器通过 GO 就搭建完成了.
并发
goroutine
goroutine 是由 Go 运行时 runtime 环境管理的轻量级线程.
goroutine 使用关键字 go 来执行,例如
go f(x, y, z)
意思为开启一个新的 goroutine 来执行 f(x,y,z) 函数.
一般来讲,多线程通信都需要处理共享内存同步的问题,Go 也有 sync 功能,但是不常用,后面继续研究.
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")
}
我们定义了一个函数 say,函数体为一个循环输出,每隔 100 微秒输出一遍参数值,共输出 5 次.
Go 的 time 包中定义了多个常量来表示时间,我们可以直接调用而不需要再自行计算.
package time
type Duration int64
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
接着,我们在 main 函数中调用了两遍 say 函数,不同的是第一行调用加入了关键字 go,这就使得这两行调用并不存在依次顺序,而是两个线程互不干扰的在跑.我们来看一下结果.
world
hello
hello
world
world
hello
hello
world
world
hello
可以发现,world 和 hello 的输出并没有显然顺序,而是交替输出.
然而经测试,这个输出虽然是交替但顺序不变,这说明了 goroutine 并不是 "完全的多线程",也就是说 goroutine 是通过程序控制内存资源的调配的,而不是真正意义的互不干扰独立的并行地享用各自的内存空间运行.
channel
我们在之前学习 java 的 nio 的时候就介绍过 channel,编程语言都是换汤不换药,各取所需,所以本质上区别不大,下面我们来具体介绍一下 Go 的通道.
channel 是有类型的管道,使用 chan 关键字定义,可以用 channel 操作符 <- 对其发送或者接收值.
我们要使用通道,记住这个次操作符即可,箭头就是数据流的方向,不过注意你只能调整箭头左右的对象,这两个对象至少有一个是 channel 类型的,而不能改变操作符的箭头方向,操作符只有这一个,方向就是从右向左.
与 map 和 slice 一样,channel 创建也要使用 make
ch: =make(chan int)
ch 是变量名,chan 声明了这是一个 channel,int 说明这个 channel 的数据类型是整型.chan 有点像 java 的 final 或 static 关键字的用法,它是用来修饰变量的,与数据类型不冲突.判断一个变量是不是通道值,就看它的定义中是否有关键字 chan 即可.
注意:在 channel 传输数据的操作中,只要另一端没有准备好,发送和接收都会阻塞,这使得 goroutine 可以在没有明确锁或竞态变量的情况下进行同步.
package main
import "fmt"
func sum(a []int, c chan int) {
sum := 0
for _, v := range a {
sum += v
}
c <- sum // 将和送入 c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
ch := make(chan int)
go sum(a[4:], ch)
go sum(a[:1], ch)
x, y := <-ch, <-ch // 从 ch 中获取
fmt.Println(x, y, x+y)
}
// 输出:7 4 11
函数 sum 很好理解,我们就把 channel c 当做普通的整型值,意思就是将第一个参数的整型数组的值之和传给 c.
main 函数中,先定义了一个整型切片 a,根据初始化值可以确定它的长度和容量均为 6.
然后借助 make 定义了一个通道变量 ch,它的数据类型是整型.
下面我们使用 goroutine 来 "多线程" 调用 sum 函数,除了都传入了通道变量 ch 以外,第一个调用传入的是 a 的后 6-4 个数组成的新切片 a[4:]等于 {4,0},第二个调用传入的是前 1 个数组成的新切片 a[:1] 等于{7}
因此可以得出,第一个调用 sum 以后,ch 接收到值为 4+0=4,第二个调用 sum 以后,ch 接收到值为 7.
那么下一行代码是如何执行的? x 和 y 应该如何分配 ch 的值.
x, y := <-ch, <-ch
这一行代码我也比较 confuse,首先来看 x 是先得到 ch 的值,y 是后得到 ch 的值.
那么关于 ch 的值到底在传给 x 和传给 y 这之间发生了什么?
回到 goroutine 的特性,我们上面已经分析了一波,它不是真正的独立多线程,而是有序的,有章法地通过语言底层逻辑来实现资源调配,那么经测试,我可以总结出来这两个调用的执行顺序是第一个调用的结果后传给 ch,第二个调用的结果先传给 ch.那么是否可以总结出来,第二个调用的结果给到了先接收的 x,第一个调用的结果给到了后接收的 y.
以上的分析完全是结果反推的,我不确定是否正确,但我是根据结果这么理解的.(TODO: 参照其他书籍的解释)
channel 的缓冲区概念
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 1
ch <- 2
ch <- 3
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
可以看到,在正常的创建 channel 变量 ch 的结构 ch := make(chan int) 的 make 后面加入了第二个参数 2.这个 2 代表了当前通道变量 ch 的缓冲区大小为 2
2 的意思是什么?
我们从长计议,再回到上面的 channel 的概念继续分析,
package main
import "fmt"
func add(i int,c chan int){
c<-i
}
func main() {
ch := make(chan int)
go add(1,ch)
fmt.Println(<-ch)
}
// 输出:1
channel 这种类型的变量必须伴随这 goroutine 的使用,而 goroutine 必须修饰的是函数,也就是说线程执行的一定是函数,而不能是一行代码.
你写 go c:=1 就是错的,go 只能用于函数不能用于一行代码.
所以,我写了一个 add 函数来做这行代码相同的事,然后用 go 来修饰.这才能通过 fmt.Println(<-ch) 打印出 ch 的值.
我们再继续测试.
func main() {
ch := make(chan int)
go add(3,ch)
go add(2,ch)
go add(5,ch)
go add(11,ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
输出情况如下:
11
3
2
5
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox859149002/main.go:17 +0x360
通过输出结果继续分析,我写了 4 个 go 修饰的 add 函数调用,然而下面使用 <-ch 了 5 次,第五次的输出报错了,
报错信息为:所有的 goroutine 都睡眠了,死锁,下面是通道接收报错,main 函数是在 tmp 临时目录下建立了一个沙盒 sandbox 加沙盒 id 的目录,在这个目录下执行 main 函数时,第 17 行出错.
第 17 行对应的就是第五次输出.
这说明了通道 channel 传输的次数一定要等于 go 调用函数的次数.
包含通道 channel 类型参数的函数必须要用 goroutine 来调用.
好,这种情况我们来评判一下是好是坏呢?我觉得这是一种规定,但是稍显死板,必须是相等的才行.那么 Go 也提供了一种机制来变通,就是上面提到的 channel 的缓冲区概念.下面来看代码,深入体验一下缓冲区的 "疗效".
func main() {
ch := make(chan int,4)
ch<-1
ch<- 100
go add(3,ch)
go add(2,ch)
go add(5,ch)
go add(11,ch)
add(12,ch)
add(222,ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
输出结果:
1
100
12
222
11
3
2
5
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
/tmp/sandbox119279221/main.go:25 +0x640
我们先来描述一下上面发生了什么,上面代码中我们定义了四次 goroutine 调用 add 函数,四次针对通道 channel 变量 ch 进行的普通赋值操作,然后下方对通道变量的接受者输出了九次.
下面我们来总结一下 Go 语言中通道 channel 缓冲区的特性.可以发现,如果没有缓冲区的话,不能对通道进行非线程操作,也就是说不使用 goroutine 调用函数来操作通道的话,就会报错.而有了缓冲区以后,通道可以按照你设定的缓冲区大小来做普通的非 goroutine 参与的非线程的同步操作,从上面的输出结果我们也能看出来了,非 goroutine 参与的代码都是按照先后顺序执行的,只有 goroutine 参与的是无序的,但是所有的 goroutine 参与的操作一定是在所有普通操作结束以后再执行的,1,100,12,222 就是普通操作的结果,后面的都是 goroutine 的操作结果.
最后一行的报错信息是因为通道 ch 被接受值的次数多于通道 ch 被发送值的次数一次,所以有一次报错,但这与缓冲区大小无关.
最后,让我尝试一下用精简的一句话来总结一下通道缓冲的概念.
通道缓冲定义了通道变量允许被最多普通操作的次数.
那么它的意义是什么?
我好像又绕回来了,上面讲过那么一大段通道缓冲的存在意义了.下面再来一句话解释通道缓冲区.
缓冲区大小是可以发送到通道而没有发送阻塞的元素数.
这样的解释更加清晰了,就是通道有了缓冲区可以存放(通道作为接受者)一定大小的数据而不是直接进入阻塞.
缓冲区的高级用法 range,close
可以通过 for range 语句来遍历缓冲区,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)
}
}
斐波那契函数我们前面练习过,这里对函数做了修改,加入了通道参数,用通道来替代 temp(不懂 temp 的来历的请翻到上面斐波那契函数).
temp 是需要返回的,但通道不需要,通道可以与线程共享数据.
我们先来看上面代码发生了什么?
通道变量 c 被定义了大小为 10 的缓冲区,goroutine 调用斐波那契函数向通道发送值,每发送一次,会被下面的 for range 循环的循环体中接收通道的值并输出,也就是说通道 c 接收到一个值就会立马在 goroutine 线程发送出去,所以 goroutine 调用斐波那契函数和下面的 for range 循环是并行的.直到 for range 将通道 c 的缓冲区遍历结束,通道 c 由于缓冲区大小的限制也不会继续再接收值了,这时就会被 close 掉.
总结几点注意:
close 方法是放在了斐波那契函数内尾部而不是想当然的放在 for 循环后面.原因是 Go 规定了只有发送者可以关闭通道,作为发送者的只有斐波那契函数,for 循环是作为接受者的,它无权对通道进行关闭操作.
我们在斐波那契函数中的循环次数为手动输入的通道的缓冲区大小,如果不是这样的话,发送次数超过了缓冲区大小就会报错.
for range 循环在对通道 c 进行遍历的时候,它并不会自动按照 c 的缓冲区大小来循环,而是通道 c 被关闭以后,触发了 for range 循环的中止,而如果不是这样的话,通道一般是不需要被 close 的.
向一个已经关闭的 channel 发送数据会引起 panic(可以理解为一种 error).
select
select 的用法有点像 switch,基于上面我们对通道的深入了解,结合 select 我们可以做很多事,select 可以根据判断执行哪个分支,下面看代码.
package main import "fmt"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
输出:
}
}
}
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)
}
先来看这段程序都发生了什么,我们又对斐波那契函数进行了改动,函数内有一个 for 死循环,这就要求我们在循环体中去设置中止办法.循环体中用到了 select 关键字,它就像 switch 那样,这里有两个 case 判断:
0
1
1
2
3
5
8
13
21
34
quit
第一个是判断 c 是否可以接收值,如果可以就执行第一个分支,那么 c 在什么情况下不能接收值呢?上面我们研究过多次了,这是没有缓冲区的通道,它的接收次数一定要与它的发送次数相等,当它的发送结束的时候,它也就不能再继续接收值了.
第二个判断是 quit 通道是否可以发送值,同样的道理,通道的发送次数一定与它的接收次数相等,当 quit 通道接收了值,这个判断的分支就可以被执行.
只要有 goroutine 的函数,执行时一定会与普通函数并行,无论这个普通函数的调用是写在它的前面还是后面.
没有缓冲区的通道,在代码中它的接收次数和发送次数一定是相等的,这个主动权当然是在 main 函数里,因为只有 main 函数才是真正开始执行的函数.
所以下面来看 main 函数.
main 函数定义了一个 go 修饰的匿名函数,函数体内是一个循环 10 次发送通道 c 的循环,然后是一次 quit 通道的接收.上面说了主动权在 main,所以 main 函数要求的这些次数必将在斐波那契函数中得以平衡(即发送次数与接收次数相等).所以下面的斐波那契函数并行地执行了对应的 10 次通道 c 的接收和 1 次 quit 通道的发送,这些操作放到 select 的判断中去就是执行 10 次的斐波那契数列,每次通道 c 接收到数列的一个值就会被 go 匿名函数发送打印出去,10 次结束以后,会接收 quit 通道的值,return 中止斐波那契函数内部的死循环.
select 操作很像 switch,所以 select 也有 default 判断的分支,当其他分支不满足的时候,就会走 default 分支,一般 default 分支会执行一个时间的休眠等待,等待外部其他函数的通道操作能够满足 select 的某些分支.
sync.Mutex
sync.Mutex 是一个互斥锁类型,它有 Lock 和 Unlock 一个上锁一个解锁的方法,Lock 和 Unlock 之间的代码只会被一个 goroutine 访问共享变量(共享变量不仅是通道,普通类型的实例也可以,例如 struct 类型),从而保证一段代码的互斥执行.目前没有什么太好的例子,以后有机会再学而时习之吧.
总结
总结里面依然不说 Go 的优势,只对本篇文章做一个总结,本篇文章的目标是一次系统性的从零到一的学习 Go 语言.我本想多看基本书概况总结他们来放到这篇文章中去,但我觉得学习分为理论和实践,比例约为 2:8,不能再多了,也由于项目紧留给我搞理论的时间实在不多,因此我就顺着官方文档这一支完完整整地捋下来,对其中每一个特性,语法细节都做了仔细的研究,开发环境的逐步搭建,也对源码进行了复现,甚至自己也开发了一些测试代码.当然,这篇文章远远不能称为 Go 语言的高级深入使用手册,只是一个入门到了解的过程,未来还有着长久的实践,在使用中会有更多的心得,到时有机会我再总结一篇深入版吧,可以基于《effective go》来写.
参考资料
《A Tour of Go》
其他更多内容请转到 醒者呆的博客园
来源: https://www.cnblogs.com/Evsward/p/go.html