Golang学习笔记
Golang学习笔记
这个学习笔记是最早在1.初,版本左右的时候写的,和当前最新的版本可能会有较大的差异.
因为成文比较早,文章里面又有很多自己的见解,有些东西当时理解的不太透彻可能写错了.已经对部分地方做出了补充和修改, 有遗漏的地方,海涵.
该笔记是根据<学习go语言>和<go语言实战>两本书看完之后一边敲代码一边写笔记记录的,毕竟纯粹的看和边看边写,感觉是不一样的..
后期也融合了我在慕课网学习的视频中的一些东西,基本上没什么难度,主要是了解一下go的特性和一些常用的包.
代码在这里goSpider代码,代码是后期写爬虫的时候写的,和笔记有点出入可以作为补充来看.
Golang常用命令
- go version 查看Golang版本号
- go help 查看go命令帮助
- go env 查看Golang环境变量配置,可以查看包括GOPATH,GOROOT等变量配置的配置.
- go run 编译并运行go程序
- go build 编译go程序
- go test 运行测试文件
- go fmt 格式化代码
- go clean 清理编译文件
- go get 下载包(被墙了)
- go install 编译并安装包和依赖
- go list 列出包
- go tool 运行golang提供的工具
- go bug 启动错误报告模式
安装Golang
GoPath的环境变
-
GOPATH 一般就是我们自己定义的系统环境变量路径
-
GOROOT 一般就是go语言自己设定的路径, 存放一些go语言自带的包之类的
Linux和Unix默认位置 ~/go
下面
window下环境变量默认位置 %USERPROFIL%\go
官方推荐: 所有项目和第三方库都放在同一个GOPATH下
也可以将不同的项目放在不同的GOPATH下面
go语言编译的时候回去到不同的GOPATH的路径中取寻找自己依赖的包
go语言自带的包会到自己原来自己的src目录中取找, 这个我们不用管
我们自己import 的包是到我们定义的GOPATH中取找
设置环境变量
export GOPATH=/Users/liuhao/go
一要用的话也可以把这个也设置进去
export PATH="$GOPATH/bin:$PATH"
一般go会默认设置环境变量,我们在编写代码时,使用import时,系统的库他回去自动找自己的$GOROOT
库.
我们写的库的话,会到$GOPATH中取查找.
intellij IDEA自动清掉文件中错误的import
如果直接用goland这个IDE的话不用搞这些,格式化代码的时候会自动去掉
老版本idea是在系统设置中的 language & framework 里面的go 里面有一个On save的几个选项
新版本idea需要在plugin 安装 file watch 然后在 设置中的tools 中的file watch 中添加
我的添加目录案例: /Users/liuhao/go/bin/goimports 然后文件自动保存或手动保存时就会清掉无用的imports
- nothing 这个就不说了
- go fmt 只做格式化代码用
- go imports 不仅格式化代码, 而且还会对错误的Import进行清除和格式化
在这之前呢需要我们下载和安装这个第三方的库,否则你也没有goimports文件,自动保存的时候会暴一个Can't find goimports
in GOPATH.....
使用go get 获取第三方库
go get 获取golang.org 是不行的貌似被墙了
要用gopm 来获取无法下载的包
安装gopm
go get -v github.com/gpmgo/gopm
这是我们回到~/go 中的我们自己的GOPATH的目录 ls一下
可以看到$GOPATH下的src下多了一个github.com 的目录,和我们的目录放在一起
~/go ⌚ 2:06:05
$ tree -L 2
.
├── bin
│ └── gopm
└── src
├── github.com
└── learnGo
这时候我们到~/go/bin/gopm 运行该文件
~/go/bin/gopm get -g -v -u golang.org/x/tools/cmd/goimports
这是安装成功之后, $GOPATH 下的src中会多一个golang.org的目录和我们的目录放在一起
~/go ⌚ 2:19:31
$ tree -L 2
.
├── bin
│ └── gopm
└── src
├── github.com
├── golang.org
└── learnGo
很显然我们在编码时的目录也是在src中运行的, bin是可执行文件
在~/go/bin 目录下执行 go install 安装goimports 安装之后会在~/go/bin目录下生成一个可执行的文件
~/go/bin ⌚ 2:26:38
$ go install ../src/golang.org/x/tools/cmd/goimports
之后配置完之后, 我们在保存文件时,打码会自动按照标准尽心格式化, 无效的import也会被删除掉
- go get 命令演示
- 使用gopm 来获取无法下载的包
- go build来编译 我们编写的go文件,但是会建立在当前目录下
- go install 产生Pkg文件和可执行文件 将我们编写的go文件安装到~/go/bin下
使用go install ./... 安装当前目录下所有的go的包
go install 的时候一个package的main函数只能有1个,所以go语言的main函数都要在自己的一个目录下面 - go run 直接编译运行
~/go ⌚ 2:38:05
$ tree -L 2
.
├── bin
│ ├── goimports
│ └── gopm
├── pkg
│ └── darwin_amd64
└── src
├── github.com
├── golang.org
└── learnGo
- src 有很多第三方的包和我们的自己的项目放在里面, 每个人站一个目录
- pkg 和src是对应的是我们build出来的一些中间过程,我们不用去管他
- bin 就是我们生成的可执行文件
src
git repository 1
git repository 1
pkg
git repository 1
git repository 1
bin
可执行文件1,2,3,4...
官方在线环境(被墙了)
手动安装
如果手动安装的话需要在编译前手动设置GOPATH和GOROOT环境变量
- GOPATH为要保存的项目路径
- GOROOT为要下载包的保存路径
Mac下安装
brew install golang
- GOPATH会在家目录下的go目录下
- GOROOT会在/usr/lib/go-版本号
Ubuntu下安装
sudo apt install golang
- GOPATH会在家目录下的go目录下
- GOROOT会在/usr/local/Cellar/go/1.11.2/libexec下
第三方工具GVM
该方式安装支持单个操作系统安装多个go版本
测试是否安装成功
go doc hash
Golang基础
文件必备结构
package main //必须,独立文件必须叫main
import "fmt" //引入包,该包用于格式化IO
//主函数,独立文件必须有这个名字的函数,代码运行开始会首先调用该函数和C一样
func main() {
//打印字符串内容
fmt.Println("你好")
}
变量
注意
go定义的变量如果不使用会报错
import
导入的包不使用也会报错
64位的整数和浮点数总是64位的,不管是不是64位的系统上.
变量类型全部都是独立的,混合使用变量赋值会引起编译器报错,即使int
和int32
这种也不行.
字符串一旦给变量赋值,就不能再修改了,go中字符串是不可变的.只能通过转换为rune
数组类做
首字母大写的包全局变量为可被其他文件导入的变量公有变量,函数内的变量不存在这个概念
变量的定义和赋值
go语言变量定义时,类型在变量名的后面.
变量声明在函数外部时必须使用var
来定义,在函数内可以通过var
或:=
声明同时赋值(函数外不能使用:=
).
通过:=
形式定义一个变量时可以不设置类型,不规定类型时为不确定类型.系统自动通过变量的值来推类型.
当定义了一个变量不设置值,他的默认赋值为其类型的默认值.意味着int
为0,string
为空字符串.
int
类型 赋值可以使用8进制,16进制或者科学计数法:077,0xFF,1e3或者6.022e23这些都是合法的.
变量大写
var age int
var name string
age = 20
name = "json"
一般不需要用分号分割,但是如果想把多个写在一行就需要用分号分割.
var age int; var name string;
age = 20
name = "json"
在go语言中,变量声明和赋值时两个过程,但是也可以写在一起.
var age int = 20
var name string = "json"
在函数内可以,使用短标签 := 进行定义变量,但是在后面赋值时不可再用 := 只能用=
这种方式定义的变量的类型, 是由变量的值推演出来的.
age :=20 //int类型
name = "json" //string类型
多个变量可以成组声明,const
和import
同样也可以这样.
var (
age int
name string
)
有相同类型的多个变量可以在一行内同时声明
var age, height int
age = 20
height = 180
//也可以使用平行赋值
age, height := 20,180
特殊变量名"_"(下划线)
一个特殊的变量名是_(下划线),任何赋值给他的值都会被丢去
比如下面,将180赋值给height
将20丢弃
这种形式,在调用函数时非常有用,比如某个函数可以返回多个函数.
用变量接收的时候,加入哪个不想要的话,就可以通过这种方式来丢弃
因为go中的变量如果声明而不使用的话会报错
_, height := 20,180
比如下面这里声明了但是不使用,编译的时候就会报错
package main
func main(){
var i int
}
内建函数
go预定了少量的函数,无需引入任何包就可以使用它们.
- close 用于关闭channel通信
- new 用于各种类型创建时的内存分配
- panic 用于异常处理时,抛出错误
- recover 用于异常处理时,接收错误
- delete 用于在map中删除实例
- make 用于内建类型(map,slice,channel)创建时的内存分配
- len 用于返回字符串,slice,数组的长度
- cap 用于获取数组和slice的cap长度
- append 用于追加slice
- copy 用于复制slice
- print 底层打印函数,不带换行符
- println 底层打印函数,打印带换行符
- complex 处理复数
- real 处理复数
- imag 处理复数
GoLang关键字(保留字)
- break
- case
- chan
- const 定义常量
- continue
- default
- func 定义函数和方法
- defer
- go 并行操作(执行goroutine)
- else
- goto 跳转到当前函数内定义的标签
- fallthrough
- if
- for
- import
- interface 定义接口
- map
- package
- range
- return 从函数中返回
- select 选择不同类型的通讯
- struct 用于抽象数据类型
- switch
- type 类型定义
- var 定义变量
内建数据类型
序号 | 类型和描述 |
---|---|
1 | 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 |
2 | 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
3 | 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
4 | 派生类型: 包括: (a) 指针类型(Pointer) (b) 数组类型 (c) 结构化类型(struct) (d) Channel 类型 (e) 函数类型 (f) 切片类型 |
(g) 接口类型(interface) (h) Map 类型 |
布尔类型 bool
这个没什么好说的,和其他语言一样.
布尔类型标识由预定义的常量true
和false
代表的布尔判定值.
布尔类型是bool.
数字类型
序号 | 类型和描述 |
---|---|
整型 | 整型型 |
1 | uint8 无符号 8 位整型 (0 到 255) |
2 | uint16 无符号 16 位整型 (0 到 65535) |
3 | uint32 无符号 32 位整型 (0 到 4294967295) |
4 | uint64 无符号 64 位整型 (0 到 18446744073709551615) |
5 | int8 有符号 8 位整型 (-128 到 127) |
6 | int16 有符号 16 位整型 (-32768 到 32767) |
7 | int32 有符号 32 位整型 (-2147483648 到 2147483647) |
8 | int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
浮点型 | 浮点型 |
1 | float32 IEEE-754 32位浮点型数 |
2 | float64 IEEE-754 64位浮点型数 |
3 | complex64 32 位实数和虚数 |
4 | complex128 64 位实数和虚数 |
其他数字类型 | 其他数字类型 |
1 | byte 类似 uint8 |
2 | rune 类似 int32 |
3 | uint 32 或 64 位 |
4 | int 与 uint 一样大小 |
5 | uintptr 无符号整型,用于存放一个指针 |
int
类型表示不规定长度,会根据你的硬件类型决定适当的长度
这意味着在32硬件上,是32位的.在64位硬件是64位的.
注意 int
是32或64位之一,不会定义成其他值.uint
情况相同(带u代表无符号)
如果想要明确长度,你可以使用int32
或者uint32
等等.
完整的整数类型列表(有符号和无符号,无符号带u)是:
- int8
- int16
- int32
- int64
- byte (是uint8的别名)
- uint8
- uint16
- uint32
- uint64
浮点类型的值有:
- float32
- float64
- 没有float类型,只有上面这两种
64位的整数和浮点数总是64位的,不管是不是64位的系统上.
需要注意的时这些类型全部都是独立的
并且混合使用这些类型向变量赋值会引起编译器报错
package main
func main() {
var a int
var b int32
a = 15
b = a + a
b = b + 5
}
//执行结果
//./test1.go:8:4: cannot use a + a (type int) as type int32 in assignment
赋值可以使用8进制,16进制或者科学计数法:077,0xFF,1e3或者6.022e23这些都是合法的.
常量 constant
const age = 20
生成age这个常量.也可以使用iota
生成枚举类型.
首字母大写的包全局常量为可被其他文件导入的公有常量,函数内的常量不存在这个概念
常量在go中,也就是constant. 在编译时被创建,只能是数字,字符串或布尔值.
常量的数值可以作为各种类型使用,所以下面计算的时候需要float时可以自动转换
常量可以规定类型也可以不规定类型,不规定类型时为不确定类型
常量定义后不使用,编译时不会报错
const age int = 20
const name string = "json"
const one, two = 1,2
const (
cpp = 0
java = 1
python = 2
golang = 3
)
枚举 constant
go中的枚举类型没有专门的定义范式
只能通过定义常量时通过iota
关键字定义的形式做到的.
通过iota
声明的枚举类型,默认情况下从上到下自动递增.
第一个iota
表示未0,因此a等于0,当iota再次在新的一行使用时,它的值增加了1,因此b的值为1
const (
a = iota //默认为0
b = iota // 默认为1
)
也可以通过下面这样,省略重复的=iota
:
const (
a = iota //默认为0
b //默认追加一个iota,值为1
)
如果需要,也可以明确指定常量的类型:
const (
a = 0 //根据go规则,未定义类型则从值来推类型,所以该类型为int
b string = "0" //string类型
)
枚举类型也可以设置步长
const (
a = iota * 10
b
c
d
)
//结果为: 0,10,20,30
枚举类型也可以参与运算,作为自增值的种子
const (
a = 1 << iota * 10
b
c
d
e
f
)
//结果为10,20,40,80,160,320
字符串 string
字符串在go中是utf-8的由双引号(")包裹的字符序列.
如果使用单引号(')包裹则标识一个字符(utf-8编码),这在go中不是string
一旦给变量赋值,字符串就不能再修改了,go中字符串是不可变的.
从头C来的开发,下面的情况在go中是非法的:
var s string = "hello"
s[0] = 'c' //修改第一个字符为c,这里会报错
在go中要修改字符,需要这样做
s :="hello"
c := []rune(s) //将s转换为rune数组
c[0] = 'c' //修改数组的第一个元素
s2 := string(c) //创建新的字符串s2保存修改
fmt.Printf("%s\n", s2) //打印结果
多行字符串,通过加号(+)来做字符串拼接,该方式可以解析转义字符,比如\n换行
这种方式,不管有多少行,输出结果时都是在同一行
s :="Strating part" + //加号只能在这里否则会出错
"Ending part"
多行字符,通过反引号(`)直接写多行字符,该方式不会解析转义字符,比如\n就不会换行
处于多行时会自动换行.
s := `Starting part
Ending part`
rune
这里的rune就是go的char
go语言中已经没有char了,只有rune
因为char里只有1字节,在多国语言时有很多坑
在utf-8里面有很多字符是3字节,因为GO采用了4字节的int32来做的rune
这个byte是8位的int, rune是32位的int 他们和整数都是可以混用的
文档里说的, 他们和整数来说就是一个别名
rune 是int32的别名, 用utf-8进行编码.
这个类型一般在,遍历字符串中的字符时使用
可以以循环每个字节(仅仅在使用US ASCII编码时与字符等价,但是go中没有这种编码).
因此为了获得实际的字符,需要使用rune类型
复数
go原生支持复数,他的变量类型是complex128
(64位虚数部分).
如果需要小一些,还有complex64和32
位的虚数部分.
使用复数的一个例子:
var c complex64 = 5+5i;
fmt.Printf("value is : %v", c)
//记过为:(5+5i)
错误 error
go有为了错误而存在的内建类型, error
下面定义了err为一个error
类型,err的值是nil
var err error
数组 array
array的定义由 [n]<type>
方式定义的, n代表array长度,
对array的元素赋值火索引是由方括号完成的:
var arr [10]int
arr[0] = 42
arr[1] = 13
println("this first element value %d\n", arr[0])
像是var arr=[10]int
这样的数组类型有固定的大小.
大小是类型的一部分, 由于不同的大小是不同的类型,因此数组不能改变大小.
数组同样是值类型的,当一个数组赋值给另一个数组,会复制所有的元素.
尤其是当向函数内传递一个数组时,他会获得一个数组的副本,而不是数组的指针.
可以这样声明一个简单的数组var a[3]int
,如果不用默认值零来初始化塔,
则用复合声明a:=[3]int{1,2,3}
也可以简写为 a :=[...]int{1,2,3}
go会自动统计元素的个数,注意所有项目必须都制定. 因为如果用多维数组,有一些内容必须录入
a := [2][2]int{[2]int{1,2}, [2]int{3,4}}
类似于
a := [2][2]int{[...]int{1,2},[...]int{3,4}}
之前版本当声明一个array时,必须在方括号内输入写内容,数组或者三个点
不过新版本的array,slice,map的复合声明变得更加简单.
使用复合声明的array,slice,map,元素复合声明的类型与外部一致就可以省略
上面的例子就可以改为
a := [2][2]int{{1,2},[3,4]}
切片 slice
slice和array接近, 但是在新的元素加入的时候可以增加长度.
slice总是指向底层的一个array. slice是一个指向array的指针,这是其与array不同的地方
slice是引用类型, 这意味着当赋值某个slice到另外一个变量,两个引用会指向同一个array.
例如一个函数需要一个slice参数,在其内对slice元素的修改也会提现在函数调用者中,折合传递底层的array指针类似.
通过: sl := make([]int, 10)
创建了一个保存有10个元素的slice.
需要注意的时底层的array并无不同.
slice总是与一个固定长度的array成对出现,该array影响slice的长度和容量.
如下,首先创建了m个元素长度的array, 元素类型intvar array[m]int
然后对这个array创建slice: slice :=array[0:n]
然后现在有:
len(slice) ==n
cap(slice) ==m
len(array) == cap(array)==m
array和slice的对比图
给定一个array或者其他slice,一个新的的slice通过a[I:J]
的方式创建.
这会创建一个新的slice,指向变量a,从需要I开始截取到需要J之前.铲毒为J-I.
//array[n:m]从array创建了一个slice,具有元素n到m-1
a :=[...]int{1, 2, 3, 4, 5} //定义一个具有5个元素的array
s1 := a[2:4] //截取下标2-3的元素,创建slice,包含元素3,4
s2 := a[1:5] //截取下标1-4的元素,创建slice, 包含元素2,3,4,5
s3 := a[:] //用array中所有的元素创建slice,这是a[0:len(a)]的简化写法
s4 := a[:4] //截取0-3,创建slice,这是a[0:4]的简化写法,得到1,2,3,4
s5 := s2[:] //从slice s2截取所有元素,创建新的slice,注意s5仍然指向array a
下面代码,可见slice一个数组,对一个指定下标进行赋值时超过了数组范围
package main
func main(){
//声明一个长度为100 的数组,值为int类型
var array [100]int
//截取该数组
slice := array[0:99]
slice[98] = 'a' //成功赋值
slice[99] = 'b' //这里会提示,index out of range
}
如果想要扩展slice,可以用append和copy.
- append 想slice追加新的值,并且返回追加后的新的与原来类型相同的slice.
- 如果该slice没有足够的容量存储追加的值,append分配一个足够大的新的slice来存放原有的slice的元素和追加的值.
- 因此返回的slice可能会指向不同底层的array.
s0 := []int{0,0}
//注意下下面这三点追加方式
s1 := append(s0, 2) //追加一个元素,s1 == []int{0,0,2}
s2 := append(s1, 3, 4, 7) //追加多个元素,s2 == []int{0,0,2,3,5,7}
s3 := append(s2, s0...) //追加一个slice, s3 ==[]int{0,0,2,3,5,7,0,0}
函数copy从源slice src复制元素到目标dst,并且返回复制的元素的个数.
源和目标可能会重复,复制的数量是len(src)和len(dst)中的最小值
//这时候这里还是数组,到下面开始剪切的时候才是slice
var a =[...]int{0,1,2,3,4,5,6,7}
//创建一个长度为6的slice
var s = make([]int,6)
n1 := copy(s,a[0:]) //n1 ==6, s== []int{0,1,2,3,4,5}
n2 := copy(s,s[2:]) //n2 ==4, s== []int{2,3,4,5,4,5}
map
许多语言都有类似的类型.go中有map类型.
map可以认为是一个用字符串做索引的数组(在最简单的形式下)
一般定义map的方法是 map[<from type>]<to type>
下面定义了map类型, 用于将string(英文月份的缩写)转换为int-那个月有几天.
monthdays := map[string]int{
"Jan":31,"Feb":28,"Mar":31,
"Apr":30,"May":31,"Jun":30,
"Jul":31,"Aug":31,"Sep":30,
"Oct":31,"Nov":30,"Dec":31, //这里结尾必须有一个逗号
}
注意, 当只需要声明一个map的时候, 使用make的形式:monthdays := make(map[steing]int)
当在map中索引(搜索)时,使用方括号取值,例如打印12月有几天: fmt.Printf("%d\n", monthdays["Dec"])
当对array,slice,string或者map循环遍历时,可以用range,每次调用他都会返回一盒键和对应的值
year :=0
for _,days :=range monthdays{ //这里的键没有用,因此使用_不赋值
year +=days
}
fmt.Printf("Numbers of days in a year :%d\n",year)
向map增加元素,可以这么做
monthdays["Undecim"] = 30 //添加一个月
monthdays["Feb"] = 29 //闰年时重写这个元素
检查这个元素是否存在,可以这样
var value int
var persent bool
value,present = monthdays["Jan"] //如果存在该下标,present值为true,否则为false
v,ok := monthdays["Jan"] //这样写更接近go的方式,如果存在ok为true否则为false
也可以通过下标从map中移除元素
通常来说语句delete(m,x)会删除map中由m[x]建立的实例
delete(monthdays,"Mar") //删除下标为Mar的元素
运算符
算术运算符
下表列出了所有Go语言的算术运算符。假定 A 值为 10,B 值为 20。
运算符 | 描述 | 实例 |
---|
- | 相加 | A + B | 输出结果 30
- | 相减 | A - B | 输出结果 -10
- | 相乘 | A * B | 输出结果 200
/ | 相除 | B / A | 输出结果 2
% | 求余 | B % A | 输出结果 0
++ | 自增 | A++ | 输出结果 11
-- | 自减 | A-- | 输出结果 9
关系运算符
下表列出了所有Go语言的关系运算符。假定 A 值为 10,B 值为 20。
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 | (A == B) 为 False |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 | (A != B) 为 True |
| 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 | (A > B) 为 False
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 | (A < B) 为 True
= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 | (A >= B) 为 False
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 | (A <= B) 为 True
逻辑运算符
下表列出了所有Go语言的逻辑运算符。假定 A 值为 True,B 值为 False。
运算符 | 描述 | 实例 |
---|---|---|
&& | 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 | (A && B) 为 False |
|| | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 | (A || B) 为 True |
! | 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
位运算符
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &, |, 和 ^ 的计算方式:
- 第一行: 当p=0,q=0时p & q=0,p | q=0, p ^ q=0;
- 第二行: 当p=0,q=1时p & q=0,p | q=1, p ^ q=1;
- .......以此类推
p | q | p & q | p | q | p ^ q
---|---|---|---|---|---
0 | 0 | 0 | 0 | 0
0 | 1 | 0 | 1 | 1
1 | 1 | 1 | 1 | 0
1 | 0 | 0 | 1 | 1
Go 语言支持的位运算符如下表所示。假定 A 为60,B 为13:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与,同时为“1”,结果才为“1”,否则为0。 | (A & B) 结果为 12, 二进制为 0000 1100 |
| | 按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或 。 参加运算的两个对象只要有一个为1,其值为1。 | (A | B) 结果为 61, 二进制为 0011 1101 |
^ | 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 可以作为二元运算符,也可以作为一元运算符, 作一元运算符表示是按位取反。 作二元运算符就是异或,相同为0,不相同为1 。 |
(A ^ B) 结果为 49, 二进制为 0011 0001 |
<< | 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。 |
A << 2 结果为 240 ,二进制为 1111 0000 |
| 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。
其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。 | A >> 2 结果为 15 ,二进制为 0000 1111
赋值运算符
下表列出了所有Go语言的赋值运算符。
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 | C = A + B 将 A + B 表达式结果赋值给 C |
+= | 相加后再赋值 | C += A 等于 C = C + A |
-= | 相减后再赋值 | C -= A 等于 C = C - A |
*= | 相乘后再赋值 | C *= A 等于 C = C * A |
/= | 相除后再赋值 | C /= A 等于 C = C / A |
%= | 求余后再赋值 | C %= A 等于 C = C % A |
<<= | 左移后赋值 | C <<= 2 等于 C = C << 2 |
= | 右移后赋值 | C >>= 2 等于 C = C >> 2
&= | 按位与后赋值 | C &= 2 等于 C = C & 2
^= | 按位异或后赋值 | C ^= 2 等于 C = C ^ 2
|= | 按位或后赋值 | C |= 2 等于 C = C | 2
其他运算符
下表列出了Go语言的其他运算符。
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
- | 指针变量。 | *a; 是一个指针变量
运算符优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 |
---|---|
7 | ^ ! |
6 | * / % << >> & &^ |
5 | + - | ^ |
4 | == != < <= >= > |
3 | <- |
2 | && |
1 | || |
结构控制
go语言的结构控制语句很少,没有do while或者while循环
只有:
- if 条件判断
- for 循环
- switch 也可以向for那样有初始值
- select 类型选择和多路通讯转接器
if语句
基本上就是这么个写法, go语言强制要求大括号,并且要求在同一行.
n := 0
if n > 0 {
return 1
} else if n < 0{
return 2
} else {
return 3
}
同时if和switch也支持初始化语句,通常用来设置一个局部变量
接收处理结果同时进行判断结果
//如首先接收os.Create创建结果,同时判断结果返回的错误是否为nil
if handle, err := os.Create("./nihao"); err == nil {
print("文件创建成功\n")
//同样接收handle.Write文件写入结果,同时判断返回的错误是否为nil
if _, err := handle.Write([]byte("11111")); err == nil {
print("文件内容写入成功\n")
} else {
print("文件内容写入失败\n")
}
handle.Close()
} else {
print("文件创建失败\n")
handle.Close()
}
上面初始化语句和使用逻辑运算符的性质一样的
//创建文件, 接收创建文件结果
handle, err := os.Create("./nihao")
//判断创建结果
if err == nil {
print("文件创建成功\n")
//写入内容, 接收写入内容结果
_, err := handle.Write([]byte("11111"))
//判断写入结果
if err == nil {
print("文件内容写入成功\n")
} else {
print("文件内容写入失败\n")
}
} else {
print("文件创建失败\n")
}
handle.Close()
也可以像通常那样使用逻辑运算符
if true && true {
print("true")
}
if !false {
print("false")
}
goto语句
谨慎使用goto语句
用goto跳转到当前函数(必须是当前函数)内定义的标签,标签名大小写敏感
//死循环函数
func myFunc() {
i := 0
Here: //标签名随意定
print(i)
i++
goto Here //跳转到该标签处继续执行
}
for语句
go语言的for循环有三种形式,只有其中一种要使用分号.
- 和C的for循环一样,中间要使用分号隔开
for init; coditoin; post{}
- 和C的while一样
for condition{}
- 和C的for()一样是死循环
for{}
简单的for循环,短声明
sum :=0
for i :=0; i < 10; i++ {
sum +=i
}
go没有逗号表达式,而++和-是语句而不是表达式
如果想在for中执行多个变量,应当使用平行赋值
//for循环带平行赋值
for i, j := 0, len(a) - 1; i < j; i, j = i + 1, j - 1 {
//这里也是平行赋值
a[i], a[j] = a[j],a[i]
}
brek退出循环和continue跳过循环
for i:=0; i<10; i++{
if i>5{
break//终止该循环
}
print(i)
}
循环嵌套时,可以在循环时指定标签.break指定标签决定哪个循环被终止
标签和for循环处不能有空格
//这里的标签和for循环处不能有空格
J:for j:=0; j<5; j++{
for i :=0; i<10; i++{
if i>5 {
break J;//退出J标签处的那个循环j的循环,而不是循环i的这个循环
}
print(i);
}
}
利用continue 让循环进入下一个迭代,而略过剩下的所有代码.
for i:=0; i<10;i++{
if i>5{
continue
}
print(i)
}
range关键字
关键字range可用于循环slice,array,string,map,channel
.
range是一个迭代器,当被调用时,从他循环的内容中返回一个键值对,range根据不同的内容,返回不同的东西.
对slice或者array循环时, range返回序号作为键,这个需要对应的内容作为值.
list := []string{"a","b","c","d","e","f"}
for k, v := range list{
println(k)
println(v)
}
在字符串string上直接使用range,这样字符串也会被打算成独立的Unicode字符串
并且起始位置按照UTF-8解析
//遍历这三个字符串
for pos, char := range "aΦx" {
fmt.Printf("character '%c' starts at byte position %d\n", char, pos)
}
//执行结果
//GOROOT=/usr/local/go #gosetup
//character 'a' starts at byte position 0
//character 'Φ' starts at byte position 1
//character 'x' starts at byte position 3
switch语句
go语言的switch
case
命中之后会默认自动进行break
switch 不需要加break, 如果不想break反而要用 fallthrough
switch 可以通过default设置一旦条件全部匹配失败后的默认值
case 可以没有case体
表达式不必是常量或整数,执行过程从上到下,直到找到匹配的项.
switch 后面可以没有表达式,只要在case中定义了就行.如果没有表达式,他会匹配true
,相当于if true
自动进入循环.
这里用switch方式来编写if-else if-else
的判断序列
//注意这里传参的时候要保持类型一致,否则无法运算
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
default:
println("no case")
}
return 0
}
switch条件被命中后不会继续向下运行,而是自动退出.下面的case 1
就不会执行
i :=0
switch i{
case 0: //这里是空的case体,但是0匹配到了i
case 1:
//后面的不会被执行
println("case 1")
default:
println("default")
}
想要匹配失败后继续向下的话,需要用fallthrough
i :=0
switch i{
case 0: fallthrough //这里是空case体,但是0匹配到了i
case 1:
//这里会被执行
println("case 1");
default:
println("default")
}
case可以是多个项,使用逗号分隔. 判断参数是否在罗列的多个项中
shouldEscape('=')
//判断传进来的符号是否在指定的列表中
func shouldEscape(c byte) bool{
switch c{
//这里就类似于,if c==' ' || c=='?'..........
case ' ', '?','&','=','#','+':
return true
}
return false
}
使用switch对字节数组进行比较
//声明参数,调用函数
var a = []byte{1,2,3,4,5}
var b = []byte{2,3,4,5,6}
//或者使用uint8,主要是考察一下byte实际上就是uint8
//var a = []uint8{1}
//var b = []uint8{2}
compare(a, b)
//比较返回两个字符数组字典书序先后的整数
//如果 a ==b 返回0, 如果a,b返回-1, 如果a>b 返回+1
func compare(a, b []byte) int{
for i :=0; i<len(a) && i<len(b); i++{
switch{
case a[i] > b[i]:
return 1
case a[i] < b[i]:
return -1
}
}
//长度不同则不相等
switch {
case len(a) < len(b):
return -1
case len(a) > len(b):
return 1
}
return 0 //字符串相等
}
函数
函数是构建 Go 程序的基础部件;所遇有趣的事情都是在它其中发生的。
首字母大写的包全局函数(包括type 和 func),相当于公有函数,可被其他文件导入
函数定义
自定义一个新的类型(类似其他语言的对象,但是go的这个用法更加的丰富)
type myType int
定义一个自定义函数
func (p mytype) funcname(q int) (r, s int) { return 0,0 }
- func 关键字,用于定义一个自定义函数
- (p mytype) 函数定义可以基于一个特定类型(自定义或预定义),这类函数成为method.该部分成为receiver而他是可选的
- funcname 是自定义函数的名称
- q int 自定义函数接收的参数和参数类型
- (r, s int) 指定自定义函数的返回值和返回类型,go可以返回多个值,如果不想对返回的参数命名,只需要提供返回类型,如:(int,int).如果只有一个返回值可以省略小括号.如果函数是一个子过程,并且没有任何返回值,可以不定义这个东西
- { return 0,0 } 这是函数体,注意return是一个语句
这里有两个例子, 一个函数没有返回值, 一个只是简单的将输入返回
//没有返回值
func subroutine(in int){
return
}
//返回输入的内容
func identity(in int)int{
reutn in
}
函数可以随意安排定义的顺序, 编译器会在执行前扫描每个文件.
所以函数原型在go中都是国企的旧物.go不允许函数嵌套,不过我们可以利用匿名函数实现他
递归函数和其他语言方式一样
func rec(i int){
if i== 10{
return
}
re(i+1)
//函数在这里,打印的结果是反的,在递归调用前一行打印才是正的
fmt.Printf("%d ", i)
}
//打印结果: 9 8 7 6 5 4 3 2 1 0
函数作用域
在go语言中,定义在函数外的变量都是全局的,实际上也只是包内全局而已.
定义在函数内部的变量, 对于函数来说是局部的.
一个局部变量和一个全局变量,在函数运行时,局部变量会覆盖全局变量.
package main
var a =6 //全局变量
func main(){
p()//输出结果为, 全局变量的值 6
q()//调用函数,对全局变量的值进行覆盖性修改
p()//输出结果为, 局部变量的值 5
}
func p(){
println(a) //仅仅打印变量
}
func q(){
a :=5 //局部变量,该函数被调用时变量a会被重新赋值
println(a)
}
//执行结果为 656
当函数调用函数时的变量作用域,局部变量的作用域仅限于变量定义的函数内
注意下面这个调用方式和上面的不同之处在于,上面的是逐个调用函数,下面这个是函数内调用函数
package main
var a int
func main(){
a = 5
println(a) //打印5,因为初始内容是5,这里也是第一次打印
f()
}
func f(){
a := 6
println(a)
g()
}
func g(){
println(a)
}
//输出内容将是 565,因为局部变量仅在执行定义的函数时才有效
函数返回多个值
go的函数和方法可以返回过个值
比如,Write函数返回一个计数值和一个错误,因为说可能由于设备异常,我们传入的字节,并不都被成功写入文件了.
OS包中的*File.Write是这样声明的
func (file *File) Write(b []byte) (n int, err error)
如文档所说的那样, 他返回写入的字节数和非nil的error,这是Go中常见的方式.
类似的方法,避免了传递指针模拟引用参数来返回值.
例如,从字节数组的指定位上取得数值,返回这个值和下一个位置.
func nextInt(b []byte, i int)(int, int){
n := 0
for ; i < len(b); i++{
n = n*10 + int(b[i]) - '0'
}
return n, i
}
//我们可以在输入的数组中扫描数字,同时调用该函数
a :=[]byte{'1','2','3','4'}
var n int
for i := 0; i < len(a); {
n,i = nextInt(a,i)
println(n)
}
没有元组作为原生类型,多返回值可能是最佳选择,我们可以精确的返回希望的值,而无需重载域空间到特定的错误信号上.
命名返回值
go语言的函数返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像输入参数那样.
如果对其命名,在函数开始时,他们会用其类型默认值进行初始化.
如果函数在return时没有指定变量,仅仅只是return了.那么和返回参数重名的变量的最后一次赋值将被返回.
函数定义, 函数名在前, 返回类型在后 可以传入一个匿名函数,可传入可变参数列表,可以返回多个参数,没有默认参数,可选参数,函数重载,操作符重载,
参数用逗号分隔, 参数也是一样名在前,类型在后 这里发红是因为在别的文件中也声明了该名字的函数
func nextInt(b []byte,pos int)(value, nextPos int){/* ... */}
由于命名结果会被初始化并关联于无修饰的return, 他们可以非常简单并且清晰(我觉得不好读).
我觉得,返回多个值, 多返回值不要乱用, 一般就是返回一个值,再返回一个错误信息
//函数定义时,返回值参数n,err都是有类型默认值的.n是0,err是nil
func ReadFull(r Reader, buf []byte) (n int, err error){
for len(buf) >0 && err ==nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:len(buf)]
}
//这里修饰return的话会直接返回名字叫n和err的变量的最后一次赋值的内容
return
}
在返回多个值的情况下, 给返回值定义名称,但是在起名字的, 需要好好想想, 要不然散在函数体中, 搞不清哪里调用,会很难读
func div(a, b int) (q, r int) {
//返回两个值的时候, 如果只想用一个的话, 另一个值可以用 _ 来做代表,意思就是不接受这个值
//里面不动,但是外面的返回值也需要用名字来接一下, 这里的q,r只是在函数体中的名字,在外面接的时候可以随意定,叫a,b也无所谓,但是最好保持一致
//这里还可以这样写
//return a / b, a % b
//但是最好这样写, 因为已经返回值的名称了定义了, 但是一旦函数体比较长, 这样写就会很难读, 比较长的时候, 最好就用上面那种
q = a / b
r = a % b
return
}
延迟代码 defer
在defer关键字同一行后面的代码,将会在函数调用结束时才执行
多个defer时遵循先进后出的原则
假设有一个函数,打开文件并且对其进行读写.在这样的函数中,经常有会可能会遇到错误的地方需要提前返回的.按照普通方式的话,就需要关闭正在每个运行接触的地方, 都关闭一次文件描述符.
这经常会导致产生下面的代码:
func ReadWrite() bool{
file.Open("file")
//做点什么
if failureX {
file.Close()//关闭文件描述符
return false
}
if failureY{
file.Close()//关闭文件描述符
return false
}
file.Close()//关闭文件描述符
return true
}
上面的代码因为要执行close操作,所以多了很多重复的代码.
go有defer语句,在defer后制定的函数会在函数退出前调用.
上面的代码可以被改下成下面这样,把Close对应的放置于Open后
func ReadWrite() bool{
file.Open("file")
defer file.Close()//关闭文件描述符
//做点什么
if failureX{
return false //如果在这里结束,会自动调用Close
}
if failureY{
return false //如果在这里结束,会自动调用Close
}
return true //执行完毕,会自动运行Close
}
可以将多个函数放入延迟列表中,会按照先进后出的顺序执行
for i :=0; i<5; i++{
//这里相当于定义了五个defaer,分别是defer println(1),defer println(2).....
defer println(i)
}
//输出结果为: 4 3 2 1 0
利用defer甚至可以修改返回值
通过在被调动函数前定义defer,让函数最后执行.我们再return.
//这里的这个defer 会导致函数延迟执行
defer func(n int){
//todo
}(5) //这里的括号是必须要有的
func f() (ret int){ //ret 初始化为0
defer func(){
ret++ //ret增加为1
}()//这里的括号是必须的
return 0 //返回值是1而不是0
}
可变参数
接收可变参数的函数是有着不定数量的参数,也就是说可以传入任意多个的参数
可变参数的变量,在函数内是一个和参数规定类型一样的slice.
//arg...int 告诉go这个函数接收任意数量的参数,但是必须是int型的
func myfunc(arg ...int){
//在函数中,变量arg是一个int类型的slice
for _, n :=range arg{
println(n)
}
}
如果不指定可变参数的类型,默认是空的接口interface{}
假设有另一个可变参数的函数叫myfunc2,下面举例如何向其他函数传递参数
func myfunc(arg ...int){
myfunc2(arg...) //按照原样传递,不动传进来的参数
myfunc2(arg[:2]...) //剪切传进来的参数,再做传递
}
函数作为值
就像其他在GO中的其他东西一样,函数也是值而已.
函数可以这样赋值给变量
func main(){
a := func(){ //定义一个没有函数名的函数(匿名函数),赋值给变量a
println("nihao")
} //这里没有()
a()//调用函数
}
如果使用fmt.Printf("%T\n", a)
打印变量a的类型,可以发现输出结果会是func()
函数作为值,也会被用在其他一些地方, 例如map,
var funcMap = map[int]func() int{
1: func() int{return 10},
2: func() int{return 20},
3: func() int{return 30},
}
匿名函数和闭包
在定义匿名函数时,就会即执行,不需要调用,不过可以传参
package main
func main(){
fun := func (p1 int,p2 int) int{
return p1 + p2
}(10,20) //
println(fun)
}
将匿名函数赋值给一个变量,再通过变量来调用匿名函数
package main
func main(){
fun := func(n1 int, n2 int) int{
return n1 + n2
}
println(fun(10,20))
}
全局匿名函数,是将匿名函数赋给一个全局变量,那么这个匿名函数在当前程序里都调用用.
package main
var (
fun = func(n1 int,n2 int) int{
return n1+n2
}
)
func main(){
println(fun(10,20))
}
闭包是指在创建时封装周围状态的函数。即使闭包所在的环境不存在了,闭包中封装的状态依然存在。
GO语言的匿名函数就是闭包,以下是《GO语言编程》中对闭包的解释
基本概念
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者
任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含
在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环
境(作用域)。
闭包的价值
闭包的价值在于可以作为函数对象或者匿名函数,对于类型系统而言,这意味着不仅要表示
数据还要表示代码。支持闭包的多数语言都将函数作为第一级对象,就是说这些函数可以存储到
变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
闭包和匿名函数一般一起用,可以使用闭包来访问函数中的局部变量(被访问操作的变量为指针指向关系,操作的是同一个局部变量)如:
package main
func closure(x int) (func(), func(int)) {
fmt.Printf("初始值x为:%d,内存地址:%p\n", x, &x)
f1 := func() {
x = x + 5
fmt.Printf("x+5:%d,内存地址:%p\n", x, &x)
}
f2 := func(y int) {
x = x + y
fmt.Printf("x+%d:%d,内存地址:%p\n", y, x, &x)
}
return f1, f2
}
func main() {
func1, func2 := closure(10)
func1()
func2(10)
func2(20)
}
回调函数
当函数作为值时,就可以很容易的传递到其他函数里,然后可以作为回调函数.
func prointit(x int){ //函数无返回值
fmt.Print("%v\n", x) //仅仅打印
}
这个函数的标识是func printit(int)
, 或者没有函数名的函数:func(int)
创建新的函数使用这个作为回调,需要用到这个标识
//该函数的意思是,变量y必须是int,变量f必须是一个函数
func callback(y int, f func(int)){ //变量f将会保存传入的函数
f(y) //调用回调函数f,传入变量y
}
完整的回调函数案例
//go语言是一个函数式编程语言, 他的函数中可以套函数 作为复合函数(回调函数)
//主要流程, 我们把收到的a,b参数放到op()中执行 然后返回一个Int
//apply中的函数, 我们把收到的两个a,b参数放在第一个函数op()中执行,op()返回的是一个int
//细节 ope()接收两个参数, 返回一个int
//applay 接收两个参数a,b返回一个int
func apply(op func(int, int) int, a, b int) int {
//输出函数调用名称
fmt.Printf("Calling %s with %d, %d \n", runtime.FuncForPC(reflect.ValueOf(op).Pointer()).Name(), a, b)
//将apply接收到的参数放到op()中执行
return op(a, b)
}
//通过传入一个匿名函数的方式来调用函数
fmt.Println(apply(func(a int, b int) int {
return int(math.Pow(float64(a), float64(b)))
}, 3, 4))
恐慌(Panic)和恢复(Recover)
go没有其他语言那样的异常处理机制,不能抛出异常.
但是作为替代,它使用了恐慌和回复(panic-and-recover)机制.
这东西应该作为最后的手段被使用,我们的代码中应该没有,或者很少用这种东西.
Panic
是个一个内建函数,可以立即中断原有的程序执行.
比如当函数F调用panic(),函数F的执行被中断,但是F中的defer(延迟函数)还是会正常执行,然后F返回到调用他的地方.
在调用他的地方,F的行为就像调用了panic()一样.这一过程继续向上返回并执行每一个defer,直到程序崩溃时的所有goroutine返回
恐慌可以直接通过panic()函数产生也可以由运行错误产生,比如访问越界的数组
Recover
是一个内建的函数,可以让进入恐慌流程的goroutine恢复过来,recover仅在defer(延时函数)中有效
在正常的执行过程中,调用recover会返回nil并且没有其他任何效果.
如果当前的goroutine陷入恐慌,调用recover可以捕捉到panic的输入值,并且恢复正常操作
下面是一个利用recover做的恐慌检测的案例,该函数检查作为其参数的函数在执行时是否会产生panic:
//定义一个新函数thrwsPanic接受一个函数作为参数,当这个函数产生panic就返回true,否则返回false
func throwsPanic(f func()) (b bool){
//定义了一个利用recover的defer的函数,如果当前的goroutine产生了panic,这个defer函数能够发现,当recover()返回值非nil值,设置b为true
defer func(){
if x := recover(); x != nil{
b = true
}
}()
f() //调用作为参数接收的函数
return //无需返回B的值,因为b是命名返回值
}
完整的错误处理案例:
package main
import (
"errors"
"fmt"
)
/**
panic
停止当前函数运行
一直向上返回,执行每一个defer
如果没有遇到recover 程序退出
recover
仅在defer中调用
在defer中可以通过recover()获取panic的值
如果无法处理,可重新panic
*/
func tryRecover() {
//匿名函数后面必须有一个()
defer func() {
r := recover()
//如果是一个错误信息,输出错误信息,只是一行错误信息,如果没有这个错误处理的话,整个输出都很难看
//这里用r.(error) 是类型断言,意思就是,我推断 r是一个error 的类型
if err, ok := r.(error); ok {
fmt.Println("Error occurred", err)
} else {
//如果不知道是一个什么东西,不知道怎么处理,直接输出
panic(fmt.Sprintf("I don't know what to do%s", r))
}
}()
//可以正常处理的错误信息,下面的不能正常处理,是因为上面匿名函数中做了错误类型判断
//但是对panic()恐慌的接收是没有任何问题的
//panic(errors.New("this is an error"))
//比如现在这里就会输出Error occurred runtime error: integer divide by zero
//否则就是一大堆的代码错误信息
//b := 0
//a := 5 / b
//fmt.Println(a)
//而这个直接输出的错误信息,则是上面的recover无法捕捉的
//所以直接走到 panic(fmt.Sprintf("I don't know what to do%s", r))
panic(123)
}
func main() {
tryRecover()
}
包 package
定义包
包是数据和函数的集合,使用package
关键字定义.
文件名不需要和包名一致,包名约定使用小写字符.
go语言的包可以由多个文件组成,但是只能有一个main文件main包main函数
package even
//首字母大写, 表示公有方法,可以在包外被调用
func Even(i int) bool{
return i% 2==0
}
//首字母小写, 表示私有函数
func odd(i int) bool{
return i%2 ==1
}
在$GOPATH
下简历一个目录,复制even.go到这个目录
mkdir $GOPATH/src/even
cp even.go $GOPATH/src/even
go build
go install
在其他程序中使用even这个包
在go中,当函数首字母大写时,函数会被从包中导出,因此调用Even没问题,调用odd就报错
package main
import(
"even"//导入刚才我们写的包
"fmt"//导入官方包
)
func main(){
i := 5
//调用刚才我们写的even包中的公有函数
fmt.Printf("is %d even? %v\n", i, enen.Even(i))
//调用刚才我们写的私有函数,报错.
fmt.Printf("is %d even? %v\n", i, enen.odd(i))
}
概括来说
- 公有函数的名字以大写字母开头
- 私有函数以小写字母开头
这个规则同样也适用于包中的其他名字(新类型, 全局变量)
注意:
大写的含义不管是代表英文字母,希腊语, 古埃及语什么的也是可以
包名
当包导入(通过import)时,包名成为了内容的入口
在import "bytes"
之后,导入包的文件,可以调用函数bytes.Buffer
.
任何使用这个包的人,都可以使用同样的名字访问到它的内容,因此这样的包名是好的,短,简洁好记.
根据规范,包名是小写的一个单词,不应该有下划线或混合大小写.
通过import bar "bytes"
可以给包名起一个别名他变成了bar.Buffer
另一个规范是包名就是代码的根目录名,在src/pkg/compress/gzip
的包,作为compress/gzip
导入,但是名字是gzip,不是compress_gzip也不是compressGzip
导入包将使用其名字引用到内容上,所以导入的包可以利用这个避免啰嗦.
例如,缓冲类型的bufio包的读取方法,叫做Reader,而不是BufReader,因为用户看到的是bufio.Reader这个清晰,简洁的名字.
更进一步将,由于导入的实例总是他们包名指向的地址,bufio.Reader不会与io.Reader冲突.
类似的,ring.Ring创建新实例函数,在go中定义构造函数通常叫做NewRing
但是由于Ring是这个包唯一的一个导出的类型,同时这个包也叫做ring,所以他可以只叫做New.
包的使用者看到的时ring.New,用包的结构帮你我们选择更好的名字.
导入包
import(
"fmt"
"strings"
)
编译器会先在go的安装目录,然后按顺序查找GOPATH变量列出的目录,寻找满足import语句的包
通过引入的相对路径来查找磁盘上的包,标准库中的包会在go安装目录查找,GO开发这创建的包会在GOPATH环境变量指定目录里查找
GOAPTH指定的这些目录就是开发者的个人工作空间
举个例子,如果GO安装在/usr/local/go
,并且环境变量GOAPTH设置为/home/myproject:/home/mylibraries/
,编译器就会按照下面的顺序查找net/http包
/usr/local/go/src/pkg/net/http # 这就是标准库源代码所在位置
/home/myproject/src/net/http
/home/mylibraries/src/net/http
远程导入
import "github.com/spf13/viper"
用导入路径编译程序时,go build 命令会使用GOAPTH设置,在磁盘上搜搜这个包.
事实上,这个导入路径代表一个URL, 指向GitHub上的代码库.
如果路径包含URL,可以使用go工具链从DVCS获取包,并把包的源代码保存在GOPATH指定的路径与URL匹配的目录中.
这个获取过长使用 go get命令完成, go get将获取任意指定的URL的包,或者一个已经导入的包锁依赖的其他包
由于go get的这种递归特性,这个命令会扫描某个包的源码树,获取能找到的所有依赖包
命名导入(为包起一个别名)
如果要导入多个包具有相同的名字,例如network/convert
和file/convert
.
就会同时导入两个名叫conver的包,这种情况下,重名的包可以通过命令导入来导入.
命名导入是指,在import语句给出的包路径的左侧定义一个名字,将导入的包命名为新的名字
package main
import(
"fmt"
myfmt "mylib/fmt"
)
func main(){
fmt.Println("standard library")
myfmt.Println("mylib/fmt")
}
当我们导入一个不再代码中使用的包时,go编译器会编译失败,并输出一个错误.
但是有时候可能需要导入一个包,但是又不用这个包.这时候可以使用空白标识符_(下划线)来重命名这个包.
自定义项目中的导入
如上面所说我们在import导入包的时候,go会从GOPATH的src目录和GOROOT中导入
而我们项目一般都部署在GOPATH的src目录中,比如我们有两个项目,goSpider和goWeb
我们在goWeb项目~/go/src/web
中import
的时候,可以清楚的看到import
路径中的第一级目录有goSpider和goWeb
也就是说在我们自己进行import我们自己写的包的时候,路径起始是从GOPATH的src开始的,即~/go/src/
下面是目录结构和导入案例,在main/main.go中导入demo三个目录里的文件.
//这是我们项目目录结构
~/go/src/web ⌚ 11:36:55
$ tree
.
├── demo
│ └── test.go
├── demo1
│ └── test.go
├── demo2
│ └── test.go
└── main
└── main.go
4 directories, 4 files
main/main.go内容
package main
import (
"web/demo"
"web/demo1"
"web/demo2"
)
func main() {
println(demo.Demo1())
println(demo1.Demo1())
println(demo2.Demo1())
}
demo/test.go几个文件内容,目录名和文件名可以不一样,一个包可能有多个文件,只要包名package 定义成一样的就行
package demo
func Demo1() int {
return 1111;
}
func demo2() int {
return 222
}
init函数
这个没啥好说的, 和PHP中的__construct性质一样
每个包中都可以有任意多个init函数,这些函数会在程序执行前被调用
package main
var name string = "nihao"
func main(){
println(name)//这里会输出haha,而不是nihao
}
//最先被调用
func init(){
name = "haha"
}
标识符
就像在其他语言中,go的命名也很重要.
甚至会有语义上的左右,例如,包外是否可见是否取决于首字母大小.
使用规则是让众所周知的缩写保持原样,而不是去尝试哪里应该大写.
Aoio,Getwd,Chmod
驼峰对那些有完整单词的会很好:ReadFileNewWriteMakeSLice
包文档
每个包都应该有包注释,在package前的一个注释块.
对于多文件包,包注释只需要出现在一个文件前,任意一个文件都可以.
包注释应当对文件进行介绍,并提供相关于包的整体信息.
这回出现在go doc生成的关于包的页面上, 并且相关的细节会一并展示
来自官方regexp包的例子:
/*
The regexp package implements a simple library for regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation '|' concatenation
*/
package regexp
每个定义并且导出的函数应当有一小段文字描述该函数的行为,来自fmt包的例子
// Printf formats according to a format specifier and writes to standard // output. It returns the number of bytes written and any write error
// encountered.
func Printf(format string, a ...interface) (n int, err error)
在函数的上边写的注释,生成文档
命令行版查看文档
在当前目录下使用go doc 可以查看的到当前函数的注释中的文档
如 go doc 如果要查看该实体的函数go doc 函数名,如: go doc push 和go doc Queue
使用go help doc 还可以看到go的包里面的函数说明,比如go doc fmt.Println
通过起一个网络服务还查看文档
godoc -http :6060 会起一个服务,然后通过localhost:6060可以查看一个页面,和官网差不多,有文档和包的说明
该页面还可以看到我们在项目中写的函数的文档, 不过我们的函数需要有注释才会有文档,否则即只有函数
函数上面的注释同一行后面的注释,就会被作为文档展示,注释没有指定格式,任意写
如果在注释的下一行,在开一行注释,并且开头4个空格,会被认为是代码案例展示
示例代码
package queue
import "fmt"
//Queue内的Pop的示例代码
func ExampleQueue_Pop() {
/**
//下面的Output:内容是作为测试时,我们期望的返回值,而且是必须要有的,而且这两行的注释也会被放到示例代码中展示
//示例代码不仅仅是示例代码,而且会检查Output的值是不是对的, 下面这个是故意写错的
//而且这里的注释不能够乱写, 这里写的注释都会被认为是文档的一部分
//注释如果写在Output:上面这个函数将不不能在IDE上点击左侧的箭头运行,箭头直接消失了
//所以我把 注释写在了函数内的第一行这里
//Output:下面正确的写法是应该写正确的结果,如下
1
2
false
3
true
*/
q := Queue{1}
q.Push(2)
q.Push(3)
fmt.Println(q.Pop())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
fmt.Println(q.Pop())
fmt.Println(q.IsEmpty())
//Output:
//1
//2
//false
//3
//true
}
示例代码要用到的另一个包
package queue
//通过定义别名来扩展其他的包
type Queue []int
//Pushes the element into the queue
// e.g q.Push(123)
func (q *Queue) Push(v int) {
//这里的*q是一个指针接受者, 指针接受者时可以改变里面的值的
// 后面使用*q 前面也要加一个*
*q = append(*q, v)
}
//Pops element from head
func (q *Queue) Pop() int {
//这里的括号是定界符
//这里是取出第0个元素进行返回
head := (*q)[0]
//将q的内存地址中的数据, 修改为将q的下标为1开始剪切到最后一个的数据
// 后面使用*q 前面也要加一个*
*q = (*q)[1:]
return head
}
//Returns if the queue is empty or not
func (q *Queue) IsEmpty() bool {
return len(*q) == 0
}
//文档查看与生成
//在函数的上边写的注释,生成文档
//命令行版查看文档
//在当前目录下使用go doc 可以查看的到Queue函数
//如 go doc 如果要查看该实体的函数go doc 函数名,如: go doc push 和go doc Queue
//使用go help doc 还可以看到go的包里面的函数说明,比如go doc fmt.Println
//通过起一个网络服务还查看文档
//godoc -http :6060 会起一个服务,然后通过localhost:6060可以查看一个页面,和官网差不多,有文档和包的说明
//该页面还可以看到我们在项目中写的函数的文档, 不过我们的函数需要有注释才会有文档,否则即只有函数
//函数上面的注释同一行后面的注释,就会被作为文档展示,注释没有指定格式,任意写
//如果在注释的下一行,在开一行注释,并且开头4个空格,会被认为是代码案例展示
//示例代码
//示例代码在queue_test中
//在我们补充完示例代码之后,在王网络服务查看文档时,该函数会有一个Example点开之后是示例代码,里面含有Code和Output
//但是如果//Output:下面的返回内容写的不对的话, 文档汇总是不会显示Output的
//如果要测试queue_test的话可以用IDE,点击函数名左侧的箭头,也可以在当前目录下使用go test来做
在我们补充完示例代码之后,在王网络服务查看文档时,该函数会有一个Example点开之后是示例代码,里面含有Code和Output
但是如果//Output:下面的返回内容写的不对的话, 文档汇总是不会显示Output的
如果要测试queue_test的话可以用IDE,点击函数名左侧的箭头,也可以在当前目录下使用go test来做
测试包
在go中为包编写单元测试,应当是一个良好的习惯.
编写测试需要包含testing包和程序go test命令,两者都有很好的文档.
got test程序调用了所有的测试函数,even包没有定义任何测试函数,执行go test 这样
got test
? even [no test file]# 提示没有测试文件
在测试文件中定义一个测试来修复这个.才是文件也在包目录中,被命名为*_test.go
这些测试文件捅go程序中的其他文件一样,但是go test只会执行测试函数
每个测试函数都要相同的标识,他的名字以test开头
func TestXXX(t *testing.T)
编写测试时需要告诉go tes测试是失败还是成功,测试成功则直接返回.
测试失败可以用下面函数来标识函数,这是非常重要的
func (t *T) Fail()
Fail标记测试函数失败,但仍然继续执行
func (t *T) FailNow()
FailNow标记测试函数失败,并且中断其执行.
这将会执行下一个测试,因为,当前文件的其他所有测试都被跳过.
func (t *T) Log(args ...interface{})
Log 默认格式对其参数进行格式化,与Print()类似,并且记录文本错误日志
func (t *T) Fatal(args ...interface{})
Fatal等价于Log()后跟随FailNow()
将这些拼凑到一起,就可以编写测试了,首先命名文件名为even_test.go
package even
import "testing"
func TestEven(t *testing.T){
if !Even(2){
t.Log("2 should be even")
t.Fail()
}
}
注意第一行使用了package even,测试使用与被测试的包相同的名字空间
这不仅仅是为了方便,也允许测试未导出的函数和结构.
然后倒入testing包,并且定义了这个文件中唯一的测试函数TestEven()
.
展示的Go代码应该没有任何惊异的地方: 检查了Even函数是否工作正常,现在执行测试
go test
o even 0.001s
测试成功并且报告OK,成功了
为了展示失败的测试,修改测试函数:
//Ntering the twilight zone
func TestEven(t *test.T){
if Even(2){
t.log("2 should be odd!")
t.Fail()
}
}
然后得到
Fail even 0.004s
FAIL TestEven(0.00 seconds)
2 should be odd
FAIL
在编写包的时候应当一遍写代码,一遍写文档和测试函数
这可以让我们的程序更加的好,也展示了我们的努力
测试案例1
package main
import (
"fmt"
)
type Human int
func (h Human) String() string {
return fmt.Sprintf("%s", "String echo")
}
func (h Human) chinese(name string) (age int, sex string) {
return 0, "nan"
}
//创建节点
type TradeNode struct {
value int
left, right *TradeNode
}
//为节点的属性设置参数, 同时返回节点的内存地址
func CreateNode(value int) *TradeNode {
return &TradeNode{value: value}
}
//打印节点
func (node TradeNode) print() {
fmt.Println(node.value)
}
func (node *TradeNode) setValue(value int) {
if node == nil {
fmt.Println("setting value to nil")
return
}
node.value = value
}
//遍历节点内容
func (node *TradeNode) traverse() {
//当node 为nil 的时候跳过
if node == nil {
return
}
node.left.traverse()
node.print()
node.right.traverse()
}
func main() {
//if err := os.Chmod("nihao.log", 777); err != nil {
// fmt.Println(err)
//}
//
//var h Human
//
//fmt.Println(h.chinese("zemin"))
var root TradeNode
//将节点的value设置为3
root = TradeNode{value: 3}
//将节点内存地址赋值给节点的左子树
root.left = &TradeNode{}
//将节点内存地址赋值给节点的右子树,同时赋值
root.right = &TradeNode{5, nil, nil}
root.right.left = new(TradeNode)
root.left.right = CreateNode(2)
root.right.left.setValue(4)
root.traverse()
//传入一个空的指针,传入没问题,但是里面设置值的时候,会导致拿不到实例的属性而报错
//var pRoot *TradeNode
//pRoot.setValue(200)
}
测试案例2
package main
import (
"fmt"
)
//定义接口
type adder interface {
add(string) int
}
//定义函数类型
type handler func(name string) int
//实现函数类型方法
func (h handler) add(name string) int {
return h(name) + 10
}
//函数参数类型接受实现了adder接口的对象(函数或结构体)
func process(a adder) {
fmt.Println("process:", a.add("taozs"))
}
//另一个函数定义
func doubler(name string) int {
return len(name) * 2
}
//非函数类型
type myint int
//实现了adder接口
func (i myint) add(name string) int {
return len(name) + int(i)
}
func main() {
//注意要成为函数对象必须显式定义handler类型
var my handler = func(name string) int {
return len(name)
}
//以下是函数或函数方法的调用
fmt.Println(my("taozs")) //调用函数
fmt.Println(my.add("taozs")) //调用函数对象的方法
fmt.Println(handler(doubler).add("taozs")) //doubler函数显式转换成handler函数对象然后调用对象的add方法
//以下是针对接口adder的调用
process(my) //process函数需要adder接口类型参数
process(handler(doubler)) //因为process接受的参数类型是handler,所以这儿要强制转换
process(myint(8)) //实现adder接口不仅可以是函数也可以是结构体
}
常用的包
标准的GO代码库中包含了大量的包,并且在安装GO的时候多数会伴随一起安装.
浏览$GOROOT/src/pkg
目录可以看到哪些包.
这里列举一些常用的系统包
- fmt 实现了格式化IO函数,这与C的printf和scanf类似.
%v 默认格式的值,当打印时,加好(%+v)会增加字段名
%#v go样式的值表达
%T 带有类型的Go样式的值表达 - io 这个包提供了原始I/O操作界面,他主要的任务是对OS包这与的原始IO进行封装,增加一些其他相关,使其具有抽象功能用在公众的几口上
- bufio 这个包实现了缓冲的IO,他封装于io.Reader和io.Writer兑现,创建了另一个兑现(Reader和Writer)在提供缓冲的同时实现了一些文本IOde功能
- sort 提供了对数组和用户定义集合的原始排序功能
- strconv 提供了将字符串转换为基本数据类型,或者从基本数据类型转化为字符串功能
- os 提供了与平台无关的操作系统功能接口
- sync 提供了基本的同步源于,例如互斥锁
- flag 实现了命令行解析
- encoding/json 实现了编码与解码RFC4627[5]定义的json对象
- test/template 数据驱动模板,用于生产文本输出,例如HTML
将模板管理到某个数据结构上进行解析,模板内容指向数据结构的元素(通常为结构的字段或者map的键)控制解析并且决定某个值会被现实,模板扫描结构以便解析,而游标@据诶电脑关了当前位置在结构中的值 - net/http 实现了http请求,响应和URL解析,并且提供了可扩展的http服务和基本的http客户端
- unsafe 包含了go程序中数据类型上所有不安全操作,通常不用这个
- reflect 实现了运行时反射,允许程序通过抽象类型操作对象.通常用于处理静态类型interface{}的值,并且通过Typeof解析出其他动态类型信息,通常会返回一个有接口类型Type的对象
- os/exec 该包用户执行外部命令
指针
go有指针,然而go的指针不能运算.
通过类型作为前缀来定义一个指针'*':var p *int
现在p也是一个指向整数值的指针
所有新定义的变量都被赋值为其类型的零值,而指针也一样,一个新的定义的或没有任何指向的指针,有值nil.
在其他语言中这叫做空(NULL)指针,在go中就是nil,让指针指向某些内容,可以使用取值符(&)来操作,&用来获取变量的地址
- &符号的意思是对变量取地址,如:变量a的地址是&a
- 符号的意思是对指针取值,如:&a,就是a变量所在地址的值,当然也就是a的值了
在其他语言中空指针是要报错的,go虽然支持空指针,但是指针最好都是有值的.
下面演示变量在内存中的地址:
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a )
}
这种指针赋值需要写两条语句,写起来麻烦,而且还要多定义一个变量.也是没办法的事,目前是这么要求的.
使用指针
指针使用流程:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
var p *int //指向整型的指针
fmt.Printf("%v\n",p)//结果为nil
var i int //定义一个整型变量i
p = &i //让p指向i
fmt.Printf("%v\n",p) //带引出来内容类似0x88adf
//从指针获取值是通过在指针变量前置'*'来实现的
p = &i //获取I的内存地址
*p = 8 //修改I的值
fmt.Printf("%v\n", *p) //结果为8
fmt.Printf("%v\n", i) //同上
/* 指针变量的存储地址 */
fmt.Printf("p 变量储存的指针地址: %x\n", *p )
var a int = 255
//们将 a 的地址赋给 b((a 的类型为 *int)
var b *int = &a //操作符用来获取一个变量的地址
fmt.Println(&b) //使用&,可以打印地址
fmt.Println(*b) //使用*b,可以打印值
前面已经说了,指针没有运算,如果写成这样*p++
他标识(*p)++
,先取内存地址的值,然后对这个值++
Go 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
空指针就是仅仅声明了了一个指针变量,仅仅声明的话,并没有产生所谓的地址指向,也就是说他的地址是nil.
nil 指针也称为空指针,nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。nil指针可以声明,但是如果要直接用的话会报错,如果非要用的话需要new()来分配内存.
一个指针变量通常缩写为 ptr。声明空指针
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr )
}
空指针直接使用,会报错
var age *int
*age = 100
空指针判断:
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
在函数中使用指针
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);
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
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 的值 : 200
交换后 b 的值 : 100
*/
在数组中使用指针(最好不要传递指向数组的指针给函数,而是使用切片
)
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
/**
a[0] = 10
a[1] = 100
a[2] = 200
*/
字符串就没必要用指针了, 因为字符串不可修改
内存分配
go是支持垃圾回收机制的,也就是说无需担心内存分配和回收.
go有两个内存分配语句new和make,但是他们勇于不同的类型,做不同的工作
- new 返回指针类型,分配了零值填充的T类型的内存空间,并且返回其地址,一个*T类型的值.
- make只能创建slice,map和channel,并且返回一个有初始值的T类型,而不是*T.
new分配内存
new 返回指针
内建函数new本质上和其他语言中的同名函数功能一样,new(T)
分配了零值填充的T类型的内存空间,并且返回其地址,一个*T类型的值.
用Go的术语说,他返回了一个指针,指向新分配的类型T的零值,要记得new 返回指针
可以使用new创建一个数据结构的实例,并且可以直接工作.
如:bytes.Buffer
的文档所属的Buffer的零值是一个准备好了的空缓冲.
类似的sync.Mutex也没有明确的构造函数init方法,取而代之,sync.Mutex的明智被定义为非锁定的互斥量.
零值是非常有用的,例如这样定义
type SyncedBuffer struct{
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer的值在分配内存或定义之后立刻就可以用,在这个片段中,p和v都可以在没有任何更进一步处理的情况下工作
p := new (syncedBuffer) //type *SyncedBuffer,已经可以用了
var v SyncedBuffer //同上
使用new来为空指针分配空间,让他可以赋值
// 这种声明方式 p 是一个 nil 值
var i *int //nil指针参与计算会报错
// 改为new,为指针开辟空间
var i *int = new(int)
// 如果是一个自定的类型,可以使用这样
type Name struct {
First, Last string
}
var n *Name = &Name{}
make分配内存
make 返回初始化后的(非零)值.
内建函数make(T,args)和new(T)有这不同的功能.
make只能创建slice,map和channel,并且返回一个有初始值的T类型,而不是*T
本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化.
例如,一个slice,是包含指向数据(内部array)的指针,长度和容量的三项描述.在这些项目被初始化之前,slice为nil.
对于slice,map和channel,马克初始化了内部的数据结构,填充适当的值.
例如,make([]int,10,100)分配了100个整数的数组,然后用长度10和容量100创建了slice结构指向数组的前10个元素.
区别是,new([]int)返回指向新分配的内存的指针,而零值填充的slice结构是指向nil的slice值.
这个例子展示了new和make的不同
var p *[]int = new ([]int) //new 分配slice结构内存 *p ==nil,这时候已经可以用了
var v []int = make([]int,100) //make 指向一个新分配的有100个整数的数组
var p *[]int = new ([]int) //new 不必要的复杂例子
*p = make([]int, 100,100)
v := make([]int ,100) //更常见的例子
一定要记住make仅适用于,map,slice和channel,并且返回的不是指针,应当用new获得指定的指针
- new分配; make初始化
- 上面两端可以总结为
- new(T)返回*T指向一个零值T
- make(T)返回初始化后的T
- 当然make仅适用于,slice,map和channel
构造函数和复合声明
- 有时候零值不能满足需求,必须要有一个用于初始化的构造函数,例如这个来自OS包的例子
func NewFile(fd int, name string) *File{
if fd < 0{
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
- 许多冗长的内容,可以使用符合声明时上面的例子更加简单,每次只使用一个表达式创建一个新实例.
func NewFIle(fd int, name string) *File{
if fd < 0{
return nil
}
f :=File{fd,name,nil,0}
return &f
}
- 返回本地变量的地址没有问题,在函数返回后,相关的存储区域依然会存在.s
事实上,从符合声明获取分配的实例的地址更好,因此可以最终将两行缩短为一行
return &File{fd,name,nil,0}
- 所有的项目(字段)都必须按顺序全部写上.然而通过读子元素用字段:值承兑的标识, 初始化内容可以按任意顺序出现,并且可以省略初始化为零值的字段
因此可以这样`
return &File(fd:fd,name:name)
- 在特定情况下,如果复合声明不包含任何字段,他创造特定类型的零值,表达式new(File)和&File{}是等价的
复合声明统一可以用于创建array,slice和map,通过指定适当的索引和map键来标识字段.
在这个例子中,无论是Enone,Eio还是Einval初始化都能很好的功能,只要确保他们不同就好
ar := [...]string {Enone:"no error",Einval:"invalid argument"}
sl := []string {Enone : "no error", Einval: "invalied argument"}
ma := map[int]string{Enone:"no error",Einval:"invalied argument"}
定义自己的类型
首字母大写的包全局函数(包括type和func),相当于公有函数,可被其他文件导入
当type类型名和方法名首字母均为大写时,也是公有函数
go允许自定义新的类型,通过关键字type实现:
type foo int
创建了一个新的类型foo作用和int一样,创建更加复杂的类型需要用到struct关键字
这有一个数据结构中记录某人姓名和年龄,并且使其成为一个新的类型的例子
最后直接打印整个结构
package main
import "fmt"
type NameAge struct{
name string //私有
age int //私有
}
func main(){
a := new(NameAge)
a.name = "pete"
a.age = 20
fmt.Printf("%v\n", a)
}
//结果为&{peter 20}
如果想打印某一个,或者某几个结构中的字段,要使用.
fmt.Printf("%s", a.name) //格式化字符串
结构字段
之前已经提到结构中的字段被称为field,没有字段的机构为struct{}
比如有四个字段
struct{
x,y int
A *[]int
F func{}
}
如果省略字段的名称,可以创建匿名字段
struct{
T1 //字段名是T1
*T2 //字段名是T2
P.T3 //字段名是T3
x,y int //字段名是x和y
}
注意字母大写的字段可以被导出,也就是说.在其他包中可以进行读写.
字段名以小写字母开头为当前包的私有.包的函数定义时和函数类似的的
方法
当type类型名和方法名首字母均为大写时,也是公有函数
可以对新定义的类型创建函数以便操作,定义可以通过两种方式
- 创建一个函数接收这个类型的参数
func doSomething(in1 *NameAge, in2 int){/*...*/}
- 创建一个工作在这个类型上你的函数,(类似函数调用)
func (in1 *NameAge) doSomething(in2 int){/*...*/}
//这是方法调用,可以类似这样用
var n *NameAge
n.doSomething(2)
至于使用函数还是方法,完全由我们随意.但是若需要满足接口,就必须要用方法.如果没有这样的需求,那就完全由习惯来决定是使用函数还是方法了.
但是下面的情况一定要注意
- 如果x可获取地址,并且&x的方法中包含了m, x.m()是(&x).m()更短的写法
根据上面所述,这意味着下面的情况不是错误
var n NameAge
n.doSomething(2)
这里Go会查找NameAge类型的变量n的方法列表,没有找到就会再查找*NameAge类型的方法列表,并且会转化为(&n).doSomething(2)
下面的类型定义中有一些微小但是很重要的不同之处
//Mutex 数据类型有两个方法 Lock和Unlock
type Mutex struct {/*Mutex字段*/}
func (m *Mutex) Luck() {/*Lock实现*/}
func (m *Mutex) Unlock() {/*Unlock实现*/}
现在用两种不同的风格创建了两个数据类型
- type NewMutex Mutex
- type PrintableMutex struct
现在NewMutux 等同于Mutex,但是他没有任何Mutex的方法,也就是说他是空的
但是PrintableMutex已经从Mutex集成了方法集合
*PrintableMutex的方法集合包含了Lock和Unlock方法,被绑定到其匿名字段Mutex中
类型转换
有时候需要将一个类型转换为另一个类型,在go中可以做到,不过有一些规则.
首先,将一个值转换为另一个是由操作符(看起来像函数 :byte())来完成的,并且不是所有的转换都是被允许的
而且一部分转换会出现问题,比如int想string转换,因为go是utf8编码的,所以会出现结果有问题这时候就要用到了strconv包.
案例如下:
var a int = 100
s := string(a)
d := strconv.Itoa(a)
println(s)//结果为字母d
println(d) //结果为100
以下是合法的转换表,float64 同 float32 类似
+ From | xb []byte | xi []int | xr []rune | s string | f float32 | i int |
---|
- To | | | | | |
- []byte |X |O |O |[]byte(s) |O |O
- []int |O |X |O |[]int(s) |O |O
- []rune |O |O |X |[]rune(s) |O |O
- string |string(xb) |string(xi) |string(xr) |X |O |O
- float32 |O |O |O |O |X |float32(i)
- int |O |O |O |O |int(f) |X
- 从string到字节或者ruin的slice
mystring := "hello this is string"
byteslice := []byte(mystring)
转到byte slice,每个byte 保存字符串对应字节的整数值.注意go的字符串是UTF-8编码的,一些字符可能是1,2,3或者4个字节结尾
runeslice := []rune(mystring)
转换到rune slice 每个rune保存Unicode编码的指针,字符串中的每个字符对应一个整数
- 从字节转换为整形的slice到string
b := []byte{'a','b','c'} //复合声明
s := string(b)
i := []rune{257,222,1024}
r := string(i)
对于数值,定义了下面的转换
-
将证书转换到指定的长度(bit): uint8(int)
-
从浮点数到证书: int(float(32)),这回剪切浮点数的小数部分
-
其他类型 float32(int)
-
用户定义类型的转换
- 如何在自定义类型之间进行转换
这里有两个类型,Foo和Bar,而Bar是Foo的一个别名
type foo struct{int} //匿名字段
type bar foo //bar是fool的别名
- 然后进行转换
var b bar = bar{1} //声明B为bar类型
var f foo = b //赋值B到F
- 最后一行会引起错误
connot use b(type bar) as type foo in assignment
- 可以通过转换来修复
var f foo = foo(b)
注意转换那些字段不一致的结构是相关苦难的,同时注意,转换B到int统一会出错,整数与有整数字段的结构并不一样
接口
在go中关键字interface被赋予了多种不同的含义,每个类型都有接口,意味着对那个类型定义了方法集合.
- 这段代码定义了具有了一个字段和两个方法的结构体
type S struct{i int}
func (p *S) Get() int {return p.i}
func (p *S) Put(v int) {p.i =v}
- 也可以定义成接口类型,仅仅是方法集合,不实现方法.
这里定义了一个接口I
type I interface{}
Get() int
Put(int)
- 对于接口I,S是合法的实现,因为他定义了I所需要的两个方法.
注意,即便是没有明确定义S实现了I,这也是正确的
Go程序可以利用这个特点来实现接口的另一个含义,就是接口值
//定义一个函数接受一个接口类型作为参数
func f(p I){
//p实现了接口I,必须有Get()方法
fmt.Println(p.Get)
//Put()方法是类似的
p.Put(1)
}
- 这里的变量p保存了接口 类型的值.因为S实现了I,可以调用f向其传递S类型的值的指针.
var s S
f(&s)
获取s的地址,而不是S的值的原因,是因为在s的指针上定义了方法,
这并不是必须的,可以定义让方法接收值,但是这样的话,Put方法就不会像期望的那样工作了
实际上,无需明确一个类型是否实现了一个接口,意味着go实现了叫做duck typing[33]模式.
因为如果可能的话,go编译器将对类型是否实现了接口进行实现静态检查.
然而,Go确实有纯粹动态的方面,如可将一个接口类型转换到另一个.通常情况下,转换的检查时在运行时进行的.
如果是非法转换,当在已有接口值中存储的类型值不匹配将要转换到的接口--程序会抛出运行时错误.
在Go中的接口有这与许多其他编程语言类似的思路:C++中的抽象基类,Python中的duck typing
然而没有其他任何语言联合了接口值,惊天类型检查,运行时动态转换,以及无需明确定会类型适配一个p接口.
- 定义另外一个类型同样实现了接口I
type R struct{i int}
func (p *R) Get() int{return p.i}
func (p *R) Put(v int) {p.i =v}
- 函数f现在可以接受R个S类型的变量,假设需要再函数f中知道实际的类型
在go中可以使用type switch得到
func f(p I){
//类型判断,在switch语句中使用(type),保存类型到变量t
switch t := p.(type){
case *S: //p的实际类型是S的指针
case *R: //p的实际类型是R的指针
case S: //p的实际类型是S
case R: //p的实际类型是R
default: //实现了I的其他类型
}
}
在switch之外使用(type)是非法的.类型判断不是唯一的运行时得到类型的方法.
为了在运行时得到的类型,同样可以使用'comma,ok'来判断一个接口类型是否实现了某个特定接口
if t, ok := something.(I); ok{
//对于某些实现了接口I的
//t是其所拥有的类型
}
//确定一个变量实现了某个接口,可以使用
t := something.(I)
空接口
由于每个类型都能匹配到空接口: interface{},我们可以创建一个接受空接口作为参数的普通函数
这里的接口,类型,接上面的1,2,3,4,5等
func g(something interface{}) int{
//.(I)是类型断言, 这句话的意思是,如果有这个类型,则调用Get()函数
return something.(I).Get()
}
在这个函数中的return something.(I).Get()
是有一点敲门的.
值something具有类型interface{}
,这意为着方法没有任何约束:他能包含任何类型.
.(I)是类型断言,用于转换something到I类型的接口.如果有这个类型,则可以调用Get()函数
因此,如果创建一个S类型的新变量,也可以调用g(),因为S统一实现了空接口
s = new(S)
fmt.Println(g(s))
调用g的运行不会出现问题,并且答应0,如果调用g()的参数没实现I会带来一个麻烦
i := 5f
fmt.Println(g(i))
这能编译,但是运行时会报错
方法
方法就是有接受者的函数
可以在任意类型上定义方法(除了非本地类型,包括内建类型:int类型不能有方法)
然而可以新建一个拥有方法的整数类型:
type Foo int
func (self Foo) Emit(){
fmt.Printf("%v", self)
}
type Emitter interface{
Emit()
}
对于那些非本地(定义在其他包的)类型也是一样:
扩展内建类型错误
不能定义新的防范,在非本地类型int上
func (i int)) Emit(){
fmt.Printf("%d",i)
}
扩展非本地类型错误
不能定义新的方法,在非本地类型net.AddrError上
func (a *net.AddrError) Emit(){
fmt.Pritnf("%v", a)
}
接口类型的方法
接口定义为一个方法的集合. 方法包含实际的代码
换句话说,一个接口就是定义,而方法就是实现.因此,接受者不能定义为接口类型,否则会引起invalid receiver type编译器错误
接收者类型必须是T或者*T,这的T是类型名.T叫做接收者基础类型或简称基础类型.基础类型一定不能使指针或接口类型,并且定义在于方法相同的包中
接口指针
在Go中创建指向指针的接口时无意义的,实际上创建接口值的指针也是非法的
2010年的发布日志说,语言改变是使用指针指向接口值不再自动反引用指针.指向接口值的指针通常是低级错误,而不是正确代码
如果不是有上面的限制,下面的代码就会赋值标准输入到buf的副本,而不是buf本身.
var buf bytes.Buffer
io.Copy(buf, ok.Stdin)
接口名字
根据规则,单方法接口命名为方法名加上er后缀:Reader,Writer,Formatter等
有一堆这样的命名,尬笑的翻译了他们的职责和包含的函数名,Read,Write,Close,Flush,String等等
有这规范的声明和含义,为了避免混淆,除非有类似的声明和含义,否则不要让方法和这些重名
相反的,如果类型实现了与众所周知的类型相同的方法,那么就用相同的名字和声明
将字符串转换方法命名为String而不是ToString
简短的例子
回顾冒泡排序
func bubblesort(n [int]){
for i :=0; i<len(n)-1; i++{
for j := i + 1; j < len(n); j++{
if n[j]<n[i]{
n[i],[j] = n[j],n[i]
}
}
}
排序字符串的版本是类似的,除了函数的声明
func bubblesortString(n []string{/*...*/}
基于此,可能会需要两个函数,每个类型一个. 而通过使用接口可以让这个变得更加通用.
来创建一个可以对字符串和整数进行排序的函数,这个例子的某些航是无法运行的
//函数将接收一个空接口的slice
func sort(i []inteface{}){
//使用type switch找到输入参数实际的类型
witch i.(type){
//然后做排序
case string:
//...
case int:
//...
}
//返回slice
return /*...*/
}
但是如果使用sort([]int{1,4,5})调用这个函数,会失败:cannot use i (type []int) as type []interface in function argument
这是因为Go不能简单的将其转换为接口的slice,转换到接口是容易的,但是转换到slice的开销就高了.
简单来说: go不能(隐式转换为slice.
那么如何创建Go形式的这些通用函数内?用go隐式的处理来代替type switch方式的类型腿短
下面步骤是必须的:
- 定义一个有着若干排序相关的方法的接口类型(这里叫做Sorter).至少需要获取slice长度的函数,比较两个值的函数和交换函数
type Sorter interface{
Len() int //len()作为方法
Less(i,j int) bool //p[j]<p[i]作为方法
Swap(i,j int) //p[i],p[j]= p[j], p[i]作为方法
}
- 定义用于排序slice的新类型, 注意定义的是slice类型:
type Xi []int
type Xs []string
- 实现Sorter接口的方法,整数的
func (p Xi) Len()int{return len(p)}
func (p Xi) Less(i int, jint)bool {return p[j] < p[i]}
func (p Xi) Swap(i int,j int) {p[i], p[j]=p[j], p[i]}
//和字符串的
func (p Xs) Len() int{ return len(p)}
func (p Xs) Less(i int, j int)bool {return p[j]<p[i]}
func (p Xs) Swap(i int, j int) {p[i], p[j]=p[j], p[i]}
- 编写用于Sorter接口的通用排序函数
//x现在是Sorter类型
func Sort(x Sorter){
//使用定义的函数,实现了冒泡排序
for i :=0; i < x.Len()-1; i++{
for j :=i+1;j<x.Len();j++{
if x.Less(i,j){
x.Swap(i,j)
}
}
}
}
现在可以像下面这样使用通用的Sort函数:
ints := Xi{44,67,3,13,88,8,1,0}
strings := Xs{"nut","ape","elephant","zoo","go"}
Sort(ints)
fmt.Printf("%v\n", ints)
Sort(strings)
fmt.Printf("%v", strings)
在接口中列出接口
看下面接口定义,这个包是来自container/heap的:
type Interface interface{
sort.Interface
Push(x interface{})
Pop() interface{}
}
这里有另外一个接口heap.Interface的定义总被列出,看起来很古怪,但是这是正确的.
要记得接口只是一些方法的列表.sort.Interface同样是这样一个列表,因为包含在内是毫无错误的.
自省和反射
- 什么是反射:
通过反射我们可以还原一个对像的属性,方法。 通俗的讲,在分布式环境下,我给你传了一个json化的数据结构,但是这数据结构被解析后对应了一个方法,这方法可以用来反射调用。
Go语言实现了反射,所谓反射就是能检查程序在运行时的状态。我们一般用到的包是reflect包。
go语言的grpc我记得用的就是反射.
在下面的例子中,了解一下定义在Person的定义中的标签(这里命名为namester).
为了做到这个,需要refect包(在go中没有其他方法)
要记得,查看标签意味着返回类型的定义,因此使用reflect包来指出变量的类型.然后访问标签
使用reflect一般分成三步,下面简要的讲解一下:
要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。
这两种获取方式如下:
t := reflect.TypeOf(i) //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值
转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
最后,反射的话,那么反射的字段必须是可修改的,我们前面学习过传值和传引用,这个里面也是一样的道理。
反射的字段必须是可读写的意思是,如果下面这样写,那么会发生错误
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相应的值,必须这样写
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
使用反射自省,代码都是残缺的跑步起来,主要是用来理解
type Person struct{
name string "namestr" //namestr是标签
age int
}
p1 := new (Person) //new 开辟空间,会返回Person的指针
ShowTag(p1) //调用ShowTag()并传递指针
//代码都是残缺的跑步起来,主要是用来理解
func ShowTag(i interface{}){//*Person 作为参数调用
// Elem返回类型的元素类型。
//如果类型的类型不是Array、Chan、Map、Ptr或Slice,则会引发恐慌。
switch t:= reflect.TypeOf(i);t.Kind(){
case reflect.Ptr:
//同样的在 t 使用 Elem() 得到了指针指向的值。
//t.Elem得到指针指向的值,Field(0)访问零值字段
//结构structField有成员Tag,返回字符串类型的标签名,因此在第0个字段是可以用.Tag访问这个名字,Field(0).Tag,这样就得到了namestr
tag := t.Elem().Field(0).Tag
}
}
//为了让类型和值之间的区别更加清晰,看下面的代码:
//代码都是残缺的跑步起来,主要是用来理解
func show(i interface{}){
switch t := i.(type){
case *Person:
t := reflect.TypeOf(i) //得到类型的元数据
v := reflect.ValueOf(i) //得到实际的值
//这里希望获得标签,因此需要Elemn(重定向到其上,访问第一个字段来获取标签,注意t作为一个reflect.Type来操作
tag := t.Elemn().Field(0).Tag
//现在访问其中一个成员的值,并让v上的Elem()进行重定向这样就访问到了结构,然后访问第一个字段安Field(0)并且调用其上的String()方法.
name := v.Elem().Field(0).String()
}
}
设置值与获得值类似,但是仅仅工作在可导出的成员上。这些代码:
- 私有成员的反射
type Person struct{
name string
age int
}
func Set(i interface{}){
switch i.(type){
case *Person:
r := reflect.ValueOf(i)
r.Elem(0).Field(0).Setstring("Albert hahah")
}
}
上面的代码可以编译并运行,但是当运行的时候,将得到打印了栈的运行时错误:
panic: reflect.Value.SetString using value obtained using unexported
field
下面的代码没有问题,并且设置了成员变量 Name 为 “Albert Einstein”。当然,这仅仅工
作于调用 Set() 时传递一个指针参数。
- 共有成员的反射
type Person struct{
Name string
age int
}
func Set(i interface{}){
switch i.(type){
case *Person:
r := reflect.ValueOf(i)
r.Elem().Field(0).SetString("Albert 沙卡拉卡")
}
}
并发(channel和goroutine)
并发讲解
这一章主要介绍Go的Channel和goroutine开发并行程序的能力,gorutine是Go并发能力的核心
- 之所以叫做goroutine是因为已有的短语---线程,协程,进程等等,无法准确表达含义.
- goroutine有简单的模型: 他是与其他goroutine并行执行的,有着想听的地址空间的函数.
- 他是轻量级的,仅比分配栈空间多一点点消耗.
- 而初始时栈是很小的,所以他们也是廉价的,并且随着需要再堆空间上分配和释放
groutine是一个普通的函数,只是需要使用关键字go 开头定义
channel的数据必须在goroutine中接收,否则,在channel第一次写入数据时会报错,死锁
直接声明的channel里面不管是发数据还是收数据,都是阻塞模式,
如果想非阻塞就用select 调度,default获取,或者使用make()来创建buffer channel,buffer要大于0
//channel会阻塞
ch := make(chan int)
ch <- 1
fmt.Println(<-ch) // 1
//buffer channel不会阻塞
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch) // 1
//定义一个函数
func ready(){
println("ready func")
}
ready() //普通函数调用
go ready() //ready作为goroutine运行
让一个函数作为两个goroutine运行,goroutine等待一段时间,然后打印一些内容到屏幕.
下面两个go关键字启动了两个goroutine,main函数等待足够长的时候,这样每个goroutine会打印各自的文本到屏幕
现在是下面time.Sleep(5 * time.Second)
等待五秒,但实际上没有任何办法知道,当所有goroutine都已退出才知道
func ready(w string, sec int){
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is ready")
}
func main(){
//下面启动两个gorutine
go ready("Tea", 2)
go ready("Coffee", 1)
fmt.Println("I'm watting")
time.Sleep(5 * time.Second)
}
//I'm watting 立即打印
//Coffee is ready //1秒后
//Tea is ready //2秒后
就像上面,如果不等待goroutine的执行,程序立刻停止,而任何正在执行的goroutine都会停止运行.
如果要防止这个问题,需要一些能够同goroutine通信的机制.这时候就需要channel了
channel可以与Unix shell中的双向管道做类比: 可以用它发送或接受值
这些值只能是特定的类型: channel类型.
定义一个channel时,也需要定义发送到channel的值的类型,
注意: 必须使用make创建channel
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
创建channel ci用于发送和接收整数
创建channel cs用于发送和接收字符串
创建channel cf用于满足各种类型
想channel发送或接受数据,是通过类似的操作符来完成的: "<-" 适度是写具体看操作符所处的位置
ci <- 1 //发送整数1到channel ci
<- ci //从channel ci接收整数
i := <-ci //从channel ci接收整数,并保存到变量i中
channel和goroutine实例
//定义C作为int型的channel,就是说,这个channel只能用来传输整数
//注意这个变量是全局的,这样goroutine可以访问它
var c chan int
func ready(w string sec int){
time.Sleep(time.Duration(sec) * time.Second)
fmt.Println(w, "is Ready")
//发送整数1到channel c
c <-1
}
func main(){
//初始化channel c
c = make(chan int)
//用关键字go 开启2个goroutine
go ready("tea", 2)
go ready("Coffee",1)
//打印字符串
fmt.Println("I'm waiting,bug not too long")
//等待,直到从channel上接收一个值,因为没有接收,所以接收到的值会丢了
<-c
//两个goroutine,接收两个值
<-c
}
//I'm waitting 立即打印
//Coffee is ready 再次打印
//tea is ready 最后打印
上面仍然有一些不提好的东西,比如不得不从channel中读取两次
在这个例子中没有问题,但是如果不知道有启动了多少个gorou就不知道该怎么办了
这里有另外一个go内建的关键字:select.通过select(和其他东西)可以监听channel 上输入的数据
L: for{
select{
case <-c:
i++
if i>1{
break L
}
}
}
现在将会一直等待下去,只有从channel c上接收到多个响应时才会退出循环L.
使其并行运行
虽然goroutine是并发执行的,但是他们并不是并行运行的.
如果不告诉go额外的东西,同一时刻只会有一个goroutine执行.
利用runtime.GOMAXPROCS(n)可以设置goroutine并行执行的数量
来自文档: GOMAXPROCS 设置了同时运行的CPU的最大数量,并返回之前的设置,如果n<1不会改变当前设置,当调度得到改进后,这将被移除
如果不希望修改任何代码源,统一可以通过设置环境变量GOMAXPROCS为目标值.
更多关于channel
当go中使用ch :=make(chan bool)
创建channel时,bool型的无缓冲channel会被创建.
这对于程序来说意味着,首先,如果读取(value := <-ch
)他将会被阻塞,直到有数据接收
其次,任何发送(ch<-5)将会被阻塞,直到数据被独处.无传统channel是在多个goroutine直接同步很棒的工具
不过go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素.
ch :=make(chan bool, 4)
, 创建了可以存储4个元素的bool型channel
在这个channel中,前4个元素可以无阻塞的写入,当写入到第5元素时,代码江湖阻塞,直到其他goroutine从channel中读取一些数据来腾出空间
一句话,在go中下面的为true:
//value ==0 无缓冲
//value>0 缓冲value的元素
ch := make(chan type, value)
关闭channel
当channel被关闭后,读取端需要知道这个事情,下面的代码演示了如何检查channel是否被关闭
x , ok = <-ch
当OK 被赋值为true,意味着channel尚未被关闭,同时可以读取数据,否则OK被赋值为false,在这个情况下channel被关闭
goroutine 详细代码案例
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
//这里循环10个和循环100个1000个我们都不用去管
//如果把这里改成1000, 就有1000个然在不断的打印,但是不是每个人都有机会在这一毫秒中打印出来,但是很多人都在打印
//那么这个10和1000有什么关系呢,我们熟悉操作的话,应该知道,我开10个线程没问题,我开100个也还好,但是也差不多了,如果1000个
//要1000个人并发的去执行一些事情,这就不能通过线程来做到,我们在其他的语音中,要通过异步IO的方式来做到1000个人同时并发的执行
//但是在go语言当中呢, 我们不用去管,10个也行,100个也行,1000个也行,都可以并发的执行
//goroutine是一种协程,或者说是和协程是比较像的,协程呢我们叫做是coroutine,这个是其他所有编程语言中都有这个叫法,但是不是所有语言都支持
//协程是什么
//协程是一个轻量级的线程,他的作用表面上看和线程差不多,都是并发的执行一些任务的,但是是轻量级的
//协程为什么是轻量级的
//非抢占式多任务处理,由协程主动交出控制权,线程就不同了,线城是抢占式的多任务处理,没有主动该控制权, 在任何时候都会被操作系统切换.
// 哪怕一个语句执行到一半都会被系统从中间掐掉,然后转到其他任务里面去做
//但是协程就不同了,协程是非抢占式的,什么时候交出控制权呢, 是由我协程内部主动决定的,正式因为这个非抢占式,协程才能做到轻量级
//抢占式,就需要做到最坏情况,比如我去抢的时候,别人正做到一半,那上下文我们存起来更多的东西.
//非抢占式,我们 只要集中处理切换的这几个点就可以了,这个对资源的消耗就会小很多
//协程是 编译器/解释器/虚拟机层面的多任务,他不是错做系统层面的多任务,操作系统还是其中只有线程,没有协程
//具体在执行上,操作系统有自己的调度器, 我们GO语言有一个调度器会来调度我们的协程,因此多个协程可能在一个或多个线程上运行,这个是有调度器来决定的
//这个程序表面上看,和抢占式没有什么区别,每个人都会打印,打到一半都会跳出来换成别人,这是 因为这个Printf是一个IO的操作,他在IO的操作里面会有一个协程的切换切换
//IO操作总会有一个等待的过程
//我们想办法不让他切换,我们不打印了,我们去开一个数组,在匿名函数中,不断的加自己对应下标的值做++,然后打印数组,这是再运行就会成为一个死循环
//因为我们之前我们在匿名函数中fmt.Printf("Hello from "+"goroutine %d\n", i)这个是IO操作,IO操作里面是会有协程之间的切换
//但是这里的a[i]++,我们在改成这个之后,这个把对应的a[i]锁对应的这个内存块的数字去做一个+1,这个a[i]++呢,他的中间没有机会去做协程之间的一个切换
//那这样的话我们就会被一个协程所抢掉,那这个一个协程如果不主动交出控制权的话,那他就会始终在这个协程里面
//同样我们的main()也是一个goroutine,其他的goroutine是他来开出来的,他里面的那个time.Sleep(time.Millisecond),因为没有人交出控制权,所以用于sleep不出来
//这时候我们可以使用runtime.Gosched()函数来手动交出控制权,让别人也有机会去运行,那我们的调度器总有机会调度给我自己,大家有让别人运行之后,才可以一起并发的执行
//但是一般我们很少会用到runtime.Gosched()函数,这里只是做一个演示,一般我们都要其他机会去进行切换
var a [10]int //这里改成数组,尝试不等到IO,直接输出所有的值
for i := 0; i < 10; i++ {
//匿名函数,不是什么新概念,后面的(i)代表把外面i穿进去,前面的func(i int)代表这个函数的参数要求和类型
//如果不加go,就是从0-10 反复的去调用这个匿名函数,然后这个函数里面是没有退出条件的,所以就是一个不断打印的死循环
//加了go之后,就不是不断的调这个函数,而是并发的去执行这个函数,因此我主程序还在跑,然后我并发的去开了一个函数,然后开出来的函数不断的打印hello
//这就相当于我们开了一个线程,实际上我们开的不是线程,是一个协程,虽然现在看起来差不多,所以主程序会继续往下跑,因此我们就开了10个goroutine,不断地去打印
//但是这时候如果我们不在主进程中加time.Sleep,运行这个程序的时候,什么都不会打印,而是直接就退出了
//退出的原因是,因为我们的main()和匿名函数是并发执行的,匿名函数他还来不及打印,我们的main()就从0-10就已经for循环完了,然后我们的main就退出了
//go语言的程序,一旦main退出了,所有的goroutine就被杀掉了,所以还没来得及打印东西就被杀掉了
//所以我们需要再main()进程中加一个time.Sleep(time.Millisecond) 让主进程不着急退出
//这里这个i为什么要单独定义传进去呢,
//不定义的话就是引用了外面的这个i,不定义的话运行该代码就会提示index out of range,
//我们在外面命令做了i<10的时候才会穿进去,但是为什么会out of range 呢,如果看不出来也没关系,可以用命令行来做测试
//go run goroutine.go 运行该文件的时候会提示out of range 的错误
//通过go run -race goroutine.go检测数据访问的冲突
//然后会输出 WARING: DATA RACE 在下面有Read ad 0x00000 代表写,Previous write in 0x0000 代表写入 而且还会标识出代码的行号
//别的地方也没有写入数据 应该错误的就是i造成的,如果想要证明这一点, 我们可以打印i的内存地址来证明
//不把i穿进去呢,就是函数是编程的概念, 这里这个函数就是一个闭包,他呢就引用了外面的这个i,那for循环外面的i和里面的i是同一个i
//因此外面的这个i不断的去做加法,最后当外面的这个i跳出以后,调到time.sleep()的时候i已经变成了10,我们的条件是小于10跳进去,最后一次等于10跳出来
//因此呢,这个最终会是10,当i加到了10以后呢a[i]++就会去引用10,所以就会出错,我们在命令行运行时用-race就是检测出这个错误,就知道我们为什么会出这个错
//因为我们要把这个i拷一份给这个goroutine,每一个goroutine他都要自己固定下来这个i, 通过传值的形式让他固定下面.
//函数里面func(i int)我叫i是便于读,值一旦穿进去叫什么都可以后面的()代表的传如的值,把外面的指定变量传入进去
//最后我们运行,发现没有错误.但是通过go run -race goroutine.go 发现还是有data race 的waring
//我们通过行号和内存你地址发现,是一边a[i]++在并发不断写入,而一边fmt.Println(a)在输出,这个给检测了出来,这个问题需要我们使用channel来解决
go func(i int) {
for {
//这里这个i,如果直接用for循环里的i会不安全,所以我们把i通过传值的形式,传进来
//fmt.Printf("Hello from "+"goroutine %d\n", i)
//这里改成数组,尝试不等到IO,直接输出所有的值
a[i]++
runtime.Gosched() //手动交出控制权,具体为什么可以看var a [10]int上面的注释
}
}(i) //这里是传值
}
time.Sleep(time.Millisecond)
//打印++后的数组
fmt.Println(a)
}
//协程Coroutine
//子程序是协程的一个特例,我们所有的函数调用,都可以看做是一个子程序,所有的这些函数调用都是协程的一个特例
//协程是比子程序更加宽泛的一个概念 - Donnald Knuth说的
//普通函数
//一个main调用函数,dowork,这个dowork做完之后,才会将控制权交还给main函数,然后main函数去执行下一个语句
//协程
//也是main和dowork他不是一个单项的箭头,而是双向的通道,main和dowork之间可以双向的流通,他不知数据,他的控制权也可以双向的流通
//就像我们并发执行两个线程,各做各的,并且两个人可以相互的通信而且呢控制权可以互相的交换给彼此
//那这个main和dowork运行在哪里呢,有可能在同一个线程也可能是多个线程,这个不用我们操心,反正我们就开两个协程让他们通信,我们的调度器很可能把他们放在同一个线程之内
//这样才有可能做到,我们开了1000个协程在我们的机器上运行
//go语言的协程
//go语言的进程开起来之后,会有一个调度器,调度器就负责调度协程,有些协程可能会放在一个线程里面,有些可能是两个放在一个线程里面或者很多个协程放在一个线程里面,这个我们不用管,有调度器来控制
//定义goroutine 只要我们在函数调用前面加上go关键字就能将该函数交给调度器运行,就变成了一个协程
//不需要在定义时区分是否是异步函数
//调度器会在合适的点进行切换,他虽然是一个非抢占式的,但是我们后面还是有一个调度器来进行切换,这些切换的点我们并不能完全的进行控制,这也是传统协程的一点区别
//这一些和传统协程的一点区别,传统的我们都需要在所有的切换的点,显示的写出来,但是goroutine不一样,我们就需要像写普通的函数一样,goroutine调度器会进行切换,但是又和线程的切换不一样
//使用go run -race来检测数据访问冲突的点
//goroutine可能会切换的点
//I/O,select
//channel
//等待锁,goroutine也是有锁的
//函数调用时(有时),函数调用时会有机会切换,但是是否切换呢,由调度器来做决定
//runtime.Gosched() 这个函数是手动切换的一个点,在这一点愿意交出我们的控制权
//上面的几个只是作为参考,不能保证切换,不能保证在其他地方不切换
//虽然这么说,我们的这个goroutine还是非抢占式的,从代码来看他和抢占式的有点像,但是它运行的这个机制还是非抢占式的,比如我们之前不断的做a[i]++的话他就没有机会切换了,进程就死掉了
//我们看看开1000个协程的话,我们的系统到底开了几个线程
//将i<10改成i<1000把a[i]++改成原来的fmt.Printf("Hello from "+"goroutine %d\n", i) 将main函数中的输出删掉,sleep改成1分钟
//通过top命令查看__goroutine进程在th字段也就是线程来看/前面是总线程数/后面是活跃的线程,一般/后面CPU有几个核心就会活跃几个,他觉得没有必要开的超过CPU的核心数,就做了自己的调度
//通过这个我们就可以看到,虽然我们开了1000个gorouteine但是他会映射到我们的几个物理线程上去执行,那么我们后面的调度器会进行调度
channel详细代码案例
纯粹的channel代码
package main
import (
"fmt"
"time"
)
//channel
//我们可以开很多goroutine,那么goroutine和goroutine之间的双向的通道就是channel
func chanDemo() {
//定义channel
//chan 代表channel 然后是int类型
//只是定义了c是一个channel 变量类型是一个int,但是他里面的channel并没有帮我们做出来,此时的c 的值是nil,nil的channel我们没办法用
//nil channel 以后我们学select的时候会用到
//var c chan int
//做一个channel出来, 这时候这个channel是可以用的
//我们创建完channel之后就可以往这里面发数据
//函数是一等公民,可以作为参数也可以作为返回值,我们的channel也是一等公民
c := make(chan int)
//channel的数据必须在goroutine中接收,否则,在channel第一次写入数据时会报错,死锁
go func() {
for {
n := <-c
//这里打印的时候只能打印出1,打印不出2
//因为,如果main中没有加sleep的时候,该打印2的时候main就退出了
//所以,我们在main中加了一个sleep,以后的话会有怎么进行协作,到时候就不用sleep了
fmt.Println(n)
}
}()
//todo 这时候运行的时候在这里是会报错的,提示发送1时死锁 fatal error: all goroutines are asleep - deadlock!
//死锁报错, 是因为channel是goroutine和goroutine之间的一个交互,我们必须采用另外一个goroutine去接收
//也就是说发了一个数据没有任何接收的话,是会出现deadlock的
//因此这上面我们写一个goroutine的协程来做
c <- 1 //把1发进去
c <- 2 //把2发进去
//从channel中收数据
//直接接收数据打印是会报错的,理由就是上面我们写的TODO中
//n := <-c //把channel中的数据读出来赋值到n当中
//打印我们读出来的数据
//fmt.Println(n)
time.Sleep(time.Millisecond)
}
//channel作为参数,也可以添加别的参数
func worker(id int, c chan int) {
for {
n := <-c
//这里的n也可以直接写<-c,上面不用 n:=<-c,不过我依然保留了, 便于以后自己理解
//这里打印的时候可以发现数据的结果是乱序的
//这是因为,虽然worker接到的时按照顺序接受的,但是在下面打印的时候,是一个IO操作,goroutine会进行调度,所以会乱序,但是都会打出来
fmt.Printf("worker %d received %c \n", id, n)
}
}
func chanDemo2() {
//这时候channel再次变成了一个一等公民
//为每个人建立一个channel,因为这类定义一个channel的数组
//数组里每一个人都是一个channel,然后我们在for循环中把这10个channel分发给这10个worker
//让后我们再次使用for循环给这10个人发送一些数据
var channels [10]chan int
//开多个worker
for i := 0; i < 10; i++ {
channels[i] = make(chan int)
//把数组中定义的10个channel分发给10个worker
go worker(i, channels[i])
}
//然后我们给这10个人分发一些数据
for i := 0; i < 10; i++ {
channels[i] <- 'a' + i
}
//如果觉得不够可以继续打
for i := 0; i < 10; i++ {
channels[i] <- 'A' + i
}
time.Sleep(time.Millisecond)
}
//这里这个chan如果在后面有<-代表这个channel只能用来send数据,如果<-在chan前面,表示只能从channel中取数据,不能往里面存数据
//收到这个channel的人,只能给他们发数据,既然外面的人只能发数据,那我们里面的那个人只能用来<-c收数据
func createWorker(id int) chan<- int {
//把channel作为返回值,该函数建了一个channel,开了一个goroutine,立刻就返回了,真正做事情是在这个goroutine里面
//这里自己建立一个channel
c := make(chan int)
go func() {
//这里这一块要分发给一个goroutine去做,不然这里就会死循环去收
//收的时候,这里还没有人拿到这个c,没人给我发数据,这里就会死掉了
for {
fmt.Printf("Worker %d received %c\n", id, <-c)
}
}()
//自己建立玩channel,要把他return出去
return c
}
func chanDemo3() {
//如果下面的createWorker的返回channel后面有<-,这里也必须有一个<-,如果前面有一个,这里的前面也要有意给
//但是如果加了<-之后,那这里的channel里拿出数据来就不可以了,只能往里面写
var channels [10]chan<- int
for i := 0; i < 10; i++ {
//这里调用createWorker建立10个worker,每个worker建立完之后就会返回一个channel
//把返回的channel存起来在,事先声明的数组中
//存起来之后就可以给他们分发数据
channels[i] = createWorker(i)
}
for i := 0; i < 10; i++ {
channels[i] <- 'a' + i
}
for i := 0; i < 10; i++ {
channels[i] <- 'A' + i
}
}
//channel作为参数,也可以添加别的参数
func worker2(id int, c chan int) {
//如果一旦外面的channel调用了close()关闭了channel之后,这里接收的那个人,还是会在1毫秒之内收到数据的不断的打印,但是,他收到的就是他的具体类型的默认值 int 0 string ''
//如果这里使用range(遍历完就会退出)就不需要下面的if判断了
//如果这里不加判断,close之后还在传具体类型的默认值,所以这里永远也不会退出,但是因为外面的main只能运行1毫秒,一毫秒后main函数退出,这里自然也就不存在了
//只有发送方才可以close
for n := range c {
//n, ok := <-c
//if !ok {
// break
//}
//这里的n也可以直接写<-c,上面不用 n:=<-c,不过我依然保留了, 便于以后自己理解
//这里打印的时候可以发现数据的结果是乱序的
//这是因为,虽然worker接到的时按照顺序接受的,但是在下面打印的时候,是一个IO操作,goroutine会进行调度,所以会乱序,但是都会打出来
fmt.Printf("worker %d received %d \n", id, n)
}
}
//channel缓冲区
func bufferedChannel() {
//之前我们说过,这里make完之后,下面我们往这个channel中发送数据,发完之后这个程序就会死掉,因为他没有人来收
//我们一旦发送数据,就必须要有人来收数据
//但是我们一旦发送了数据就要用协程来接收的话,也是比较耗费资源的,虽然协程是轻量级的
//这时候我们可以 加入一个缓冲区,比如我们缓冲区的大小是3,一旦设置缓冲区,数据大小不可大于缓冲区,否则会报错,但是如果有人接收的话,超过也没问题
//跟缓冲区的话,对性能的提升是有一定的优势的
//c := make(chan int)
c := make(chan int, 3)
go worker2(0, c)
c <- 1
c <- 2
c <- 3
//这里写入超过缓冲区,会出现deadlock的死锁
c <- 4
time.Sleep(time.Millisecond)
}
//关闭channel
func channelClose() {
//只有发送方才可以关闭channel
c := make(chan int)
//调用函数处理channel
go worker2(0, c)
c <- 1
c <- 2
c <- 3
c <- 4
//这时候我们close了这个channel,channel的关闭只能由发送方来做
//告诉接收方,我们发完了
//这时候在worker2中打印的内容就不像bufferedChannel的4个也不想其他的有多个,而是在打完1,2,3,4之后还在不断的打印,如果是int 则打印0,string则打印空
//也就是说一旦外面的channel关闭了之后,里面的接收的那个人,还是会收到数据的,但是一旦外面的关闭了之后,他收到的就是他的具体类型的默认值 int 0 string ''
//会在这1毫秒之内依然不断的,继续打印,如果想要屏蔽这一块,可以在worker2中打印的时候做判断,如果没有取到值就break即可
//如果里面不加判断,close之后还在传具体类型的默认值,所以这里永远也不会退出,但是因为外面的main只能运行1毫秒,一毫秒后main函数退出,这里自然也就不存在了
close(c)
time.Sleep(time.Millisecond)
}
func main() {
fmt.Println("channel as first-class citizen 作为一等公民,可以作为参数传来传去")
//chanDemo()
//chanDemo2()
//chanDemo3()
fmt.Println("Buffered channel")
//bufferedChannel()
fmt.Println("Channel close and range")
channelClose()
/**
channel 为什么要做成这个样子
理论基础: Communication Sequence Process (CSP)的理论
go语言的并发就是基于这个CSP模型做出来的
学完了go语言的channel接下来就进行应用
实践应用有一句话,是go语言的创作者他所说的一句话
Don't communicate by sharing memory,share memory by communication;
意思就是 不要通过共享内存来通信,要通过通信来共享内存
*/
}
用sync.WaitGroup来通信
package main
import (
"fmt"
"sync"
)
//这里我们直接用sync.WaitGroup来通信,告诉外面我们这里做完了
func doWorker(id int, w worker) {
//for n := range w.in {
// fmt.Printf("worker %d received %c \n", id, n)
// w.done()
//}
for {
n := <-w.in
fmt.Printf("worker %d received %c \n", id, n)
w.done()
}
}
//声明一个结构体,结构体里两个channel类型的成员属性, 将in作为传值,done作为内外通信
type worker struct {
in chan int
//这里要用指针,因为他是一个引用,而因为我们要用外面的wg,不能说我们来拷一份
//其他地方也只能传一个指针进去,wg只能用同一个
done func()
}
func createWorker(id int, wg *sync.WaitGroup) worker {
w := worker{
in: make(chan int),
done: func() {
wg.Done()
},
}
go doWorker(id, w)
return w
}
func chanDemo() {
//sync.WaitGroup的库
//go语言的并发执行,等待多人来完成任务这个go语言的库提供了一个方法,叫做sync.WaitGroup的库
//sync.WaitGroup.Add(n)添加计数,有几个协程n就写几
//sync.WaitGroup.Wait()阻塞直到计数为零,让进程等待协程执行完毕
//sync.WaitGroup.Done()减掉计数,等价于Add(-1),这样sync.WaitGroup只有两个API了, 在协程内部使用,运行完毕时用欧冠这个方法,告诉外面该方法运行完毕了
var wg sync.WaitGroup
var workers [10]worker
for i := 0; i < 10; i++ {
workers[i] = createWorker(i, &wg)
}
//这里这个也可以放到下面的for循环中每次wg.Add(1)
//add是说我们有多少个任务要并行
wg.Add(20)
for i := 0; i < 10; i++ {
workers[i].in <- 'a' + i
//也可以这样,但是既然我们知道一共是20个,所以不如直接在上面写20个算了
//wg.Add(1)
}
for i := 0; i < 10; i++ {
workers[i].in <- 'A' + i
}
//上面上面的任务全部做完
wg.Wait()
}
func main() {
//针对刚才的channelWaitgroup进行函数式编程的一点优化
//将sync.WaitGroup.done封装取来,doWorker在运行的结束的时候,直接调用worker.done()即可
//也只是让代码的抽象程度更高一些而已
chanDemo()
}
select 详细代码案例
package main
import (
"fmt"
"math/rand"
"time"
)
func generator() chan int {
out := make(chan int)
go func() {
i := 0
for {
//sleep 1500毫秒以内的时间,然后读取将0的自增写入到out中
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
out <- i
i++
}
}()
return out
}
func test1() {
var c1, c2 chan int //c1 and c2 =nil 初始化定义还没有赋值,所以会是默认值 nil channel是可以在select中运行,但是无法被select到,永远阻塞住的
//直接声明的channel里面不管是发数据还是收数据,都是阻塞模式,
//如果想非阻塞就用select 调度,default获取,或者使用make()声明时带上空间大小,来创建buffer channel
//select 作为channel的调度器,个人感觉和switch差不多
select {
case n := <-c1:
fmt.Println("Received from c1", n)
case n := <-c2:
fmt.Println("Received from c2", n)
default:
//如果c1和c2都没有数据,则默认走的区间,如果这里没有default,就会报错,我们想从c1和c2发数据但是里面没有,所以就会报错
fmt.Println("No value received")
}
}
func worker(id int, c chan int) {
for n := range c {
time.Sleep(time.Second * 2)
fmt.Printf("Worker %d received %d\n", id, n)
}
}
func createWorker(id int) chan<- int {
c := make(chan int)
go worker(id, c)
return c
}
func test2() {
var c1, c2 = generator(), generator() //声明时直接赋值
var worker = createWorker(0)
n := 0
hasValue := false
for {
//nil channel是可以在select中运行的,虽然无法被select到,形成阻塞
//所以这里我们利用这个特性,做一个判断
var activeWorker chan<- int
if hasValue {
activeWorker = worker
}
select {
case n = <-c1:
hasValue = true
case n = <-c2:
hasValue = true
case activeWorker <- n:
hasValue = false
//default:
//如果这里有default的话,会陷入到default的死循环当中
//原因就是,上面那两个case的运行还需要一点时间,才发数据,所以就会陷入这个死循环中
// fmt.Println("No value received")
}
}
}
func test3() {
var c1, c2 = generator(), generator() //声明时直接赋值
var worker = createWorker(0)
//因为供需关系的原因,c1,c2提供的数据比较快,而worker打印的比较慢,会导致中间漏掉很多多数据
//所以这里我们必须缓存起来
var values [] int
//计时器,在指定时间后发送一个数据,返回一个channel,下面我们用case来取,命中之后推出程序
tm := time.After(time.Second * 10)
//计时器,每秒发一次信息,返回一个channel,下面我们用case来取,命中之后提示当前队列有多长
tick := time.Tick(time.Second * 1)
for {
var activeWorker chan<- int
var activeValue int
if len(values) > 0 {
activeWorker = worker
activeValue = values[0]
}
select {
case n := <-c1:
values = append(values, n)
case n := <-c2:
values = append(values, n)
case activeWorker <- activeValue:
values = values[1:]
case <-time.After(800 * time.Millisecond):
//每次循环的时候判断, 八百毫秒取不到数据则提示
fmt.Println("time out")
case <-tick:
//每秒统计一下,当前队列有多长
fmt.Println("queue len=", len(values))
case <-tm:
//限制程序只运行十秒钟
fmt.Println("bye")
return
}
}
}
func main() {
//test1()
//test2()
test3()
}
通信
这一章,主要是了解如何用文件,目录,网络通信和运行其他程序和外部通信
IO操作
Go的IO核心是接口io.Reader和io.Writer
在go中从文件读写只要使用os包就可以了
package main
import "os"
func main(){
//创建一个1024字节的内存空间
buf :=make([]byte, 1024)
//打开文件
f, _ := os.Open("/etc/passwd")
//关闭资源
defer f.Close()
//死循环
for {
//一次读取1024字节
n ,_ := f.Read(buf)
//到大文件末尾
if n ==0 {break}
//将内容写入到os.Stdout即输出打印
os.Stdout.Write(buf[:n])
}
}
如果想要使用缓冲IO,则有bufio包
package main
import(
"os"
"bufio"
)
func main(){
buf := make([]byte, 1024)
//打开文件
f, _ :=os.Open("/etc/passwd")
defer f.Close()
//转换f为有缓冲的Reader,NewReader需要一个io.Reader因此或许你认为这会出错,其实不会.
//任何有Read()函数就实现了这个借口,同时*os.File已经这样做了
r := bufio.NewReader(f)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
for {
//从Reader读取,而向Writer写入,然后向屏幕输出文件
n, _ := r.Read(buf)
if n ==0 {break}
w.Write(buf[0:n])
}
}
io.Reader
再前面已经提到io.Reader借口对于Go语言来说非常重要
许多函数需要通过io.Reader来读取一些数据作为输入
为了满足这个接口,只需要实现一个方法: Read(p []byte)(n int,err error)
写入则是实现了Write方法的io.Writer
如果你让自己的程序或者包中的类型实现了io.Reader或者io.Writer接口,那么整个go标准库都可以使用这个类型
简单的例子
f, _ os.Open("/etc/passwd"); defer f.Close()
r := bufio.NewReader(f) //使其成为一个bufio,以便访问ReadString方法
s,ok:=r.ReadString('\n') {/***/} //从输入中读取一行,保存了字符串,通过string包就可以解析他
这两个例子的相似之处展示了Go拥有的脚本化特性.
命令行操作
来自命令行的参数在程序中通过字符串slice os.Args获取,导入包os即可.
flag包有着很好的接口统一提供了解析标识的方法.
这个例子就是一个DNS查询工具:
//定义bool标识,dnssec变量必须是指针,否则package无法设置其值
dnsser := flag.Bool("dnssec", false, "Request DNSSEC records")
//类似的,port选项
port := flag.String("port","53","Set the query port")
//简单的重定义Usage函数
flag.Usage = func(){
fmt.Fprintf(os.Stderr, "Usage : %s[OPTIONS][name ...]", os.Args[0])
//指定的每个标识,PrintDefaults将输出帮助信息
flag.PrintDefaults()
}
//解析标识,并填充变量
flag.Parse()
//当参数被解析后,可以使用它们
if *dnssec{
//do something
}
执行命令
os/exec包有函数可以执行外部命令,调用系统命令
通过定义一个有着数个方法的*exec.Cmd结构来使用
执行ls -l:
cmd :=exec.Command("/bin/ls", "-l")
err := cmd.Run()
上面的例子运行了ls -l 但是没有对返回的数据进行任何处理,通过下面的方法从命令行的标准输出中获得信息:
import "exec"
cmd := exec.Command("/bin/ls","-l")
buf, err := cmd.Output() //buf是一个[]byte
网络操作
所有网络相关的类型和函数都可以在net包中找到,这其中最重要的函数是Dial
当Dial到远程系统,这个函数返回Conn接口类型,可以用于发送或接受信息
函数Dial简洁的抽象了网络层和传输层,因此IPv4或者IPv6,TCP或者UDP可共用一个接口
通过TCP连接到远程系统(端口是80),然后是UDP,最后是TCP通过IPV6,大致是这样
conn,e := Dial("tcp","172.16.8.7:80")
conn,e := Dial("udp","172.16.8.7:80")
conn,e := Dial("tcp", "[这是一个IPV6地址]:80") //方括号代表强制
如果没发生错误,由e返回,就衣蛾使用conn从套接字中读取
在包net中的原始定义是:
//从连接读取数据。
Read(b []byte)(n int,err error)
这使得conn成为了io.Reader
//向连接写入数据。
Write(b []byte)(n int, err error)
这同样使得conn成为了io.Writer,事实上conn是io.ReadWriter
但是这些都是隐含的底层,通常总是应该使用更高层次的包,例如http包
一个简单的htt Get作为案例
package main
//导入依赖包
import(
"io/ioutil"
"net/http"
"fmt"
)
func main(){
//使用http的Get方法获取HTML页面
r,err := http.Get("http://www.baidu.com")
//简单的错误处理
if err != nil {
fmt.Printf("%s\n", err)//书上用的时err.String()
return
}
//将成哥内容读入到b中
b,err := ioutil.ReadAll(r.Body)
//也可以使用这个打印返回结果
//处理返回值,第二个值标识是否打印body
//byte, error := httputil.DumpResponse(response, true)
//关闭连接, //body资源要求必须关闭
r.Body.Close()
//输出内容
if err ==nil{
fmt.Printf("%s", string(b))
}
}
可以控制请求头的http案例
package main
import (
"fmt"
"net/http"
"net/http/httputil"
)
func simpleHttpDemo() {
response, error := http.Get("http://www.baidu.com")
//有错误处理错误
if error != nil {
panic(error)
}
//body资源要求必须关闭
defer response.Body.Close()
//处理返回值,第二个值标识是否打印body
byte, error := httputil.DumpResponse(response, true)
if error != nil {
panic(error)
}
fmt.Printf("%s\n", byte)
}
func httpControllerDemo() {
//对请求内容进行控制
request, err := http.NewRequest(http.MethodGet, "http://www.baidu.com", nil)
if err != nil {
panic(err)
}
//设置请求头
request.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3493.3 Safari/537.36")
//发送请求
//response, error := http.DefaultClient.Do(request) //默认发送请求
//自定义请求内容
//可以设置transport代理服务器 CheckRedirect重定向会走这里 jar设置cookie timeout
client := http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
fmt.Println("Redirect:", req)
return nil
},
}
response, error := client.Do(request)
//有错误处理错误
if error != nil {
panic(error)
}
//body资源要求必须关闭
defer response.Body.Close()
//处理返回值,第二个值标识是否打印body
byte, error := httputil.DumpResponse(response, true)
if error != nil {
panic(error)
}
fmt.Printf("%s\n", byte)
}
func main() {
//simpleHttpDemo()
httpControllerDemo()
//使用http客户端发送请求
//使用http.Client控制请求头部
//使用httputil简化工作
//http服务器的性能分析,要用的话首先需要一个http的服务器
//import _ "net/http/pprof" 这里的下划线标识,这个引用我可以使用
//访问/debug/pprof/
//使用go tool pprof分析,获取30秒内的CPU性能分析
// 比如在当前web服务下的命令行中 go tool pprof http://localhost:8888/debug/pprof/profile 把profile改成heap可以看内存使用情况,具体可以看goroot里面的pprof或者手册
//访问这个URL后, 会在当前命令行中打印很多东西, 输入web 通过浏览器打开
}
本文来自博客园,作者:我爱吃炒鸡,转载请注明原文链接:https://www.cnblogs.com/chinaliuhan/p/15079848.html