这里是 go 基础大部分内容来源于一门视频课程
学完后再来看手摸手开发一个全栈项目就基本可以直接上手了,项目本身没有什么难度!
包
Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io等。
任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是 package pacakgeName 语句,通过该语句声明自己所在的包。
Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。
同一个路径下只能存在一个package,一个package可以由多个源代码文件组成, package main 包有且只能由一个 main 函数
包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。比如在GOPATH/src/a/b/下定义一个包 c。在包 c 的源码中只需声明为package c,而不是声明为package a/b/c,但是在导入 c 包时,需要带上路径,例如import "a/b/c。
go mod 存在时引入自定义包是以 go mod 文件 module 名称开头如 module coolcar 引入自定义包 "coolcar/proto/gen/go"
Go中如果函数名的首字母大写,表示该函数是公有的,可以被其他程序调用,如果首字母小写,该函数就是是私有的。
包名
包名一般是小写的,使用一个简短且有意义的名称。
包名一般要和所在的目录同名,也可以不同,包名中不能包含-等特殊符号。
包一般使用域名作为目录名称,这样能保证包名的唯一性,比如 GitHub 项目的包一般会放到GOPATH/src/github.com/userName/projectName 目录下。
包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
导入包
import "包的路径"
import 导入语句通常放在源码文件开头包声明语句的下面;
导入的包名需要使用双引号包裹起来;
包名是从GOPATH/src/后开始计算的,使用/ 进行路径分隔。
go
// 单行导入
import "包 1 的路径"
import "包 2 的路径"
// 多行导入 多个包 推荐使用多行
import (
"包 1 的路径"
"包 2 的路径"
)
包的导入路径
包的引用路径有两种写法,分别是全路径导入和相对路径导入。
相对路径只能用于导入GOPATH下的包,标准包的导入只能使用全路径导入。
绝对路径就是GOROOT/src/或GOPATH/src/后面包的存放路径。
包的引用格式
使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过import _ "包的路径" 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。
go
package main
// 定义别名
import F "fmt"
func main() {
// 使用别名调用方法
F.Println("import别名")
}
// 省略引用格式
package main
import . "fmt"
func main() {
//不需要加前缀 fmt.
Println("省略引用格式 此种方式不推荐 因为无法识别函数属于那个包")
}
// 匿名引用格式
package main
import (
_ "database/sql"
"fmt"
)
func main() {
fmt.Println("匿名引用格式")
}
常用命令
build
用来打包go的源文件
build 单个文件 go build file1.go
build 多个文件 go build file1.go file2.go
build 编译时的附加参数:
-v编译时显示包名-p n开启并发编译,默认情况下该值为 CPU 逻辑核数-a强制重新构建-n打印编译时会用到的所有命令,但不真正执行-x打印编译时会用到的所有命令-race开启竞态检测-o指定文件名go build -o myexec main.go
clean
go clean 命令可以用来删除 Go 编译过程中产生的临时文件和目录,包括以下几种:
- 执行
go build命令时在当前目录下生成的与包名或者 Go 源码文件同名的可执行文件。这些文件是在编译 Go 代码时生成的,用于运行和测试代码。 - 执行
go test命令并加入-c标记时在当前目录下生成的以包名加.test后缀为名的文件。这些文件是在编译测试代码时生成的,用于运行测试用例。 - 执行
go install命令安装当前代码包时产生的结果文件。这些文件是在安装 Go 代码时生成的,用于部署和分发代码。 - 在编译 Go 或 C 源码文件时遗留在相应目录中的文件或目录。这些文件是在编译过程中临时生成的,通常是中间文件或目标文件,一般不需要保留。
附加参数
-i清除关联的安装的包和可运行文件,也就是通过go install安装的文件;-n把需要执行的清除命令打印出来,但是不执行;-r循环的清除在 import 中引入的包;-x打印出来执行的详细命令,其实就是 -n 打印的执行版本;-cache删除所有go build命令的缓存-testcache删除当前包所有的测试结果
run
go run命令会编译源码,并且直接执行源码的 main() 函数,不会在当前目录留下可执行文件。
go run不会在运行目录下生成任何文件,可执行文件被放在临时文件中被执行,工作目录被设置为当前目录。在go run的后部可以添加参数,这部分参数会作为代码可以接受的命令行输入提供给程序。
gofmt
Go语言的开发团队制定了统一的官方代码风格,并且推出了 ~gofmt· 工具(gofmt 或 go fmt)来帮助开发者格式化他们的代码到统一的风格。
gofmt 是一个 cli 程序,会优先读取标准输入,如果传入了文件路径的话,会格式化这个文件,如果传入一个目录,会格式化目录中所有 .go 文件,如果不传参数,会格式化当前目录下的所有 .go 文件。
附加参数
-l仅把那些不符合格式化规范的、需要被命令程序改写的源码文件的绝对路径打印到标准输出。而不是把改写后的全部内容都打印到标准输出。-w把改写后的内容直接写入到文件中,而不是作为结果打印到标准输出。-r添加形如"a[b:len(a)] -> a[b:]"的重写规则。如果我们需要自定义某些额外的格式化规则,就需要用到它。-s简化文件中的代码。-d只把改写前后内容的对比信息作为结果打印到标准输出。而不是把改写后的全部内容都打印到标准输出。命令程序将使用 diff 命令对内容进行比对。在 Windows 操作系统下可能没有 diff 命令,需要另行安装。-e打印所有的语法错误到标准输出。如果不使用此标记,则只会打印每行的第 1 个错误且只打印前 10 个错误。-comments是否保留源码文件中的注释。在默认情况下,此标记会被隐式的使用,并且值为 true。-tabwidth此标记用于设置代码中缩进所使用的空格数量,默认值为 8。要使此标记生效,需要使用"-tabs"标记并把值设置为 false。-tabs是否使用tab('\t')来代替空格表示缩进。在默认情况下,此标记会被隐式的使用,并且值为 true。-cpuprofile是否开启 CPU 使用情况记录,并将记录内容保存在此标记值所指的文件中。
install
与 go build 命令类似,附加参数绝大多数都可以与 go build 通用。go install 只是将编译的中间文件放在 GOPATH 的 pkg 目录下,以及固定地将编译结果放在 GOPATH 的 bin 目录下。
get
go get 命令可以借助代码管理工具通过远程拉取或更新代码包及其依赖包,并自动完成编译和安装。整个过程就像安装一个 App 一样简单。 go get github.com/davyxu/cellnet
这个命令可以动态获取远程代码包,目前支持的有 BitBucket、GitHub、Google Code 和 Launchpad。在使用 go get 命令前,需要安装与远程包匹配的代码管理工具,如 Git、SVN、HG 等,参数中需要提供一个包名。
附加参数
-d只下载不安装-u强制使用网络去更新包和它的依赖包-f只有在你包含了-u参数的时候才有效,不让-u去验证import中的每一个都已经获取了,这对于本地 fork 的包特别有用-fix在获取源码之后先运行 fix,然后再去做其他的事情-t同时也下载需要为运行测试所需要的包-v显示执行的命令
gopm
go get 命令因为网络原因可能导致某些包无法下载
使用gopm来获取无法下载的包(安装需使用命令行工具)github地址:
安装 go get -u github.com/gpmgo/gopm
常用命令
shell
# 查看当前工程依赖
gopm list
# 显示依赖详细信息
gopm list -v
# 列出文件依赖
gopm list -t [file]
# 拉取依赖到缓存目录
gopm get -r xxx
# 仅下载当前指定的包
gopm get -d xxx
# 拉取依赖到$GOPATH
gopm get -g xxx
# 检查更新所有包
gopm get -u xxx
# 拉取到当前所在目录
gopm get -l xxx
# 运行当前目录程序
gopm run
# 生成当前工程的 gopmfile 文件用于包管理
gopm gen -v
# 根据当前项目 gopmfile 链接依赖并执行 go install
gopm install -v
# 更新当前依赖
gopm update -v
# 清理临时文件
gopm clean
# 编译到当前目录
gopm bin
generate
go generate 命令是在Go语言 1.4 版本里面新添加的一个命令,当运行该命令时,它将扫描与当前包相关的源代码文件,找出所有包含 //go:generate的特殊注释,提取并执行该特殊注释后面的命令。
格式
go generate [-run regexp] [-n] [-v] [-x] [command] [build flags] [file.go... | packages]
-run正则表达式匹配命令行,仅执行匹配的命令;-v输出被处理的包名和源文件名;-n显示不执行命令;-x显示并执行命令;command可以是在环境变量PATH中的任何命令。
注意
- 该特殊注释必须在
.go源码文件中; - 每个源码文件可以包含多个
generate特殊注释; - 运行
go generate命令时,才会执行特殊注释后面的命令; - 当
go generate命令执行出错时,将终止程序的运行; - 特殊注释必须以
//go:generate开头,双斜线后面没有空格。
test
go test 命令,会自动读取源码目录下面名为 *_test.go 的文件,生成并运行测试用的可执行文件。
pprof
Go语言工具链中的 go pprof 可以帮助开发者快速分析及定位各种性能问题,如 CPU 消耗、内存分配及阻塞分析。
go pprof 工具链配合 Graphviz 图形化工具可以将runtime.pprof包生成的数据转换为 PDF 格式,以图片的方式展示程序的性能分析结果。
接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
Go语言中使用接口来体现多态,是duck-type(鸭子类型)的一种体现。当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。
接口的定义与实现
go
package main
import (
"fmt"
)
// 定义一个接口
type Phone interface {
call()
}
// 定义一个结构体
type NokiaPhone struct {
}
// 实现
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
type IPhone struct {
}
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
//定义接口类型的变量
var phone Phone
//只要实现了此接口方法的类型,那么这个类型的变量(接收者类型)就可以给i赋值
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
javascript
package main
import "fmt"
// 声明一个接口
type reg interface {
get(str string) string
}
// 声明结构体
type str1 struct {
str string
}
// 定义结构体方法
func (s str1) get(str string) string {
return str + "str1"
}
type str2 struct {
str string
}
func (s str2) get(str string) string {
return str + "str2"
}
func getStr(r reg) {
fmt.Println(r.get("你是猪!"))
}
func main() {
// 实现了方法 就是实现了接口
testStr := str2{"11111111111"}
getStr(testStr)
}
接口被实现的条件
接口的方法与实现接口的类型方法格式一致,在类型中添加与接口签名一致的方法就可以实现该方法。
签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现。
接口中所有方法均被实现,当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
空接口与类型断言
具有0个方法的接口称为空接口。它表示为interface {}。由于空接口有0个方法,所有类型都实现了空接口。
类型断言用于提取接口的基础值,语法:i.(T)
go
package main
import(
"fmt"
)
func assert(i interface{}){
s:= i.(int)
fmt.Println(s)
}
// 程序打印的是int值, 但是如果我们给s 变量赋值的是string类型,程序就会panic。
func main(){
var s interface{} = 55
assert(s)
}
// 可以这样写 =======================================================
package main
import (
"fmt"
)
func assert(i interface{}) {
v, ok := i.(int)
fmt.Println(v, ok)
}
func main() {
// 如果 i 的值是int类型, 那么v就是i 对应的值,
// ok就是true。否则ok为false,程序并不会panic。
var s interface{} = 56
assert(s)
var i interface{} = "Steven Paul"
assert(i)
}
类型判断
类型判断的语法类似于类型断言。在类型断言的语法i.(type)中,类型type应该由类型转换的关键字type替换。
go
// 基本类型判断
package main
import (
"fmt"
)
func findType(i interface{}) {
switch i.(type) {
case string:
fmt.Printf("String: %s\n", i.(string))
case int:
fmt.Printf("Int: %d\n", i.(int))
default:
fmt.Printf("Unknown type\n")
}
}
func main() {
findType("Naveen")
findType(77)
findType(89.98)
}
/*
=================================================================
还可以将类型与接口进行比较。如果我们有一个类型并且
该类型实现了一个接口,那么可以将它与它实现的接口进行比较。
*/
package main
import "fmt"
type Describer interface {
Describe()
}
type Person struct {
name string
age int
}
func (p Person) Describe() {
fmt.Printf("%s is %d years old", p.name, p.age)
}
func findType(i interface{}) {
switch v := i.(type) {
case Describer:
v.Describe()
default:
fmt.Printf("unknown type\n")
}
}
func main() {
findType("Naveen")
p := Person{
name: "Naveen R",
age: 25,
}
findType(p)
}
// 输出
unknown type
Naveen R is 25 years old // 验证成功 调用 Describe()
接口的继承
接口既然可以继承自然可以组合
go
package main
import "fmt"
// 定义一个接口 声明SayHi方法
type Humaner interface {
SayHi()
}
// 定义一个接口 继承Humaner接口 存在 SayHi、Sing方法
type Personer interface {
Humaner
Sing(lrc string)
}
// 定义一个结构体
type Student struct {
name string
id int
}
// 实现接口的sayhi
func (s *Student)SayHi() {
fmt.Printf("%s sayhi\n", s.name)
}
// 实现接口 Sing
func (p *Student)Sing(lrc string) {
fmt.Printf("student %s sing %s\n", p.name, lrc)
}
func main() {
//定义一个接口的类型的变量
var i Personer
s := &Student{"mike", 1}
i = s
i.SayHi()
i.Sing("loving you ")
}
// 执行结果
// mike sayhi
// student mike sing loving you
异常
异常分为三种编辑时异常、编译时异常、运行时异常,通常 error 返回一般异常 panic 返回致命异常。
error 异常
error 异常不会中断程序执行
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
go
type error interface {
Error() string
}
创建一个 error
创建一个 error 最简单的方法就是调用 errors.New函数,它会根据传入的错误信息返回一个新的 error,示例代码如下:
go
package main
import (
"errors"
"fmt"
)
// 使用 errors 来抛出一个error异常
func MyDiv(a, b int) (result int ,err error) {
err = nil
if b == 0 {
err = errors.New("1/0 is error")
} else {
result = a / b
}
return result, err
}
func main() {
result, err := MyDiv(1, 0)
if err != nil {
fmt.Println("result err = ", err)
} else {
fmt.Println("result = ", result)
}
}
自定义错误类型
上边说 error 是一个接口,那么我们就可以使用 error 接口自定义一个 Error() 方法,来返回自定义的错误信息。
go
package main
import (
"fmt"
"math"
)
// 定义结构体
type dualError struct {
Num float64
problem string
}
// 结构体方法 自定已error 错误
func (e dualError) Error() string {
// 返回自定义错误信息
return fmt.Sprintf("Wrong!!!,because \"%f\" is a negative number", e.Num)
}
// 定义一个函数 接收一个参数, 返回两个参数 其中最后一个时错误 error
func Sqrt(f float64) (float64, error) {
// 判断获取参数小于 0 抛出error 异常
if f < 0 {
// dualError{Num: f} 默认会默认调用其是实现的 error 方法
return -1, dualError{Num: f}
}
return math.Sqrt(f), nil
}
func main() {
result, err := Sqrt(-13)
if err != nil {
fmt.Println(err)
} else {
fmt.Println(result)
}
}
// 运行结果如下: Wrong!!!,because "-13.000000" is a negative number
panic 异常
panic 异常与 error 的区别在于, panic 异常会中断程序的继续执行。
不是所有的 panic 异常都来自运行时,直接调用内置的 panic 函数也会引发 panic 异常;panic 函数接受任何值作为参数。当某些不应该发生的场景发生时我们就应该调用 panic。
go
func testb() {
//显式调用panic函数,导致程序中断
panic("this is a panic test")
}
func testc() {
fmt.Println("cccccccccccc")
}
func main() {
// testb 中使用了 panic 异常 则 testc不会继续执行
testb()
testc()
}
使用 recover函数,捕获panic异常
recover 函数是一个内置函数
- recover 函数只有在 defer 代码块中才会有效果
- 专门用来接收panic函数返回值。
- 如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。
- 在未发生panic时调用recover,recover会返回nil。
panic 是一个可以停止程序执行流程的内置函数假设当前 F 函数当中某处代码触发 panic 函数则 F 函数停止后面代码的执行,转而执行 F 函数内部的 defer 函数(如果已经声明了defer函数的话...),然后结束F函数,将当前处理权转给F的调用函数
go
func recoverFunc(i int) {
// recover 必须在 defer 函数内
// 在 recover() 声明前的错误无法被捕获
// recover() 返回值为接口类型
defer func() {
if recover() != nil {
fmt.Println("你的程序遇到致命的错误!", recover())
}
}()
// 数组越界
var testArr [10]int
testArr[i] = 100
}
// 产生数组下标越界异常
recoverFunc(100)
单元测试
Go自带了测试框架和工具,在testing包中,以便完成单元测试(T类型)和性能测试(B类型)。
一般测试代码放在*_test.go文件中,与被测代码放于同一个包中。
语法格式 func TestXxx(*testing.T)
Xxx 可以是任何字母数字字符串,但是第一个字母不能是小写字母。
测试方法
Fail: 标记失败,但继续执行当前测试函数FailNow: 失败,立即终止当前测试函数执行Log: 输出错误信息Error: Fail + LogFatal: FailNow + LogSkip: 跳过当前函数,通常用于未完成的测试用例
第一个单元测试
运行以下命令,自动搜集所有的测试文件(*_test.go)提取全部测试函数。
go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-2)
}
go
func TestFib(t *testing.T) {
var (
in = 7
expected = 13
)
actual := Fib(in)
if actual != expected {
t.Errorf("Fib(%d) = %d; expected %d", in, actual, expected)
}
}
执行 go test .输出:
go
$ go test .
ok chapter09/testing 0.007s // 表示通过
修改函数为
go
func Fib(n int) int {
if n < 2 {
return n
}
return Fib(n-1) + Fib(n-1)
}
再次执行测试 go test . 得到输出 输出结果包括:出错的测试函数名称,执行时长和错误信息。
go
$ go test .
--- FAIL: TestSum (0.00s)
t_test.go:16: Fib(10) = 64; expected 13
FAIL
FAIL chapter09/testing 0.009s
testing 的测试用例形式
TestXxxx(t *testing.T) // 基本测试用例
BenchmarkXxxx(b *testing.B) // 压力测试的测试用例
Example_Xxx() // 测试控制台输出的例子
TestMain(m *testing.M) // 测试 Main 函数
如 Example 的例子:
go
func Example_GetScore() {
score := getScore(100, 100, 100, 2.1)
fmt.Println(score)
// Output:
// 31.1
}
常见错误
测试函数找不到?
测试函数找不到?或者 go: cannot find main module, but found .git?或者 cannot determine module path for source directory xxxxxxx
检查根目录是否存在 mod 文件,如不存在请新建,在根目录执行命令 go mod init [自定义一个名字]
goroutine 协程
goroutine是Go并行设计的核心。goroutine说到底其实就是协程它比线程更小,十几个goroutine可能体现在底层就是五六个线程。
Go语言内部帮你实现了这些goroutine之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。
也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易用、更高效、更轻便。
协程的特点
协程是一个轻量级的线程,此处可以与进程、线程、协程 进行对比
Go协程是非抢占式多任务处理,需要由协程主动交出控制权
协程是一个 虚拟机层面的多任务处理
多个协程可能运行在一个或者多个线程上
创建 goroutine
在函数前 加 go 关键字 即可创建 goruoutine
go
func newTask() {
for {
fmt.Println("this is new task")
time.Sleep(time.Second) //延时1s
}
}
func main() {
go newTask() // 建一个协程,新建一个任务
for {
fmt.Println("this is a main goroutine")
time.Sleep(time.Second) //延时1s
}
}
主协程先退出其它子协程也会跟着退出
go
// 主协程退出了,其它子协程也要跟着退出
// manin 函数本身是一个 goroutine
func main() {
go func() {
fmt.Println("this is a new task")
time.Sleep(time.Second)
}() //() 代表调用
i :=0
for {
i++
fmt.Println("main this is a main task i=", i)
time.Sleep(time.Second)
if i == 3 {
break
}
}
}
Go语言协程调度器
Go语言中所有的协程都通过调度器进行调度,大并发下成百上千甚至几千的协程调用 通过调度器安排到不同的线程中执行。
调度的切换调度器会在合适的时间点进行协程之间的切换,通过切换可以将计算资源分配给其他协程而不至于被一个协程锁死资源
goroutine 可能的切换点 仅是参考不能保证切换,不能保证在其他地方不进行切换
- IO/SELECT
- channel
- 函数调用 (有时)
- runtime.Gosched() 手动切换
- 等待锁
相关方法
runtime.Gosched() 让出时间片,先让别的协程执行,它执行完,在回来执行此协程
runtime.Goexit() 终止所在的协程
runtime.GOMAXPROCS() 指定以x核运算
Channel
Channel是Go中的一个核心类型,你可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯(communication)。
Channel可以作为一个先入先出(FIFO)的队列,接收的数据和发送的数据的顺序是一致的。
Channel的创建
channel必须先创建再使用: ch := make(chan int) 或者 make(chan int, 100), 否则会永久阻塞。二者的区别在于 一个创建时初始化了容量,一个无
Channel类型 ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .,可选的<-代表channel的方向。如果没有指定方向,那么Channel就是双向的,既可以接收数据,也可以发送数据。
go
ch := make(chan int)
ch <- v // 发送值v到Channel ch中
v := <-ch // 从Channel ch中接收数据,并将数据赋值给v
//========================================================================================
chan T // 可以接收和发送类型为 T 的数据
chan<- float64 // 只可以用来发送 float64 类型的数据
<-chan int // 只可以用来接收 int 类型的数据
Channel有无容量的区别(缓存)
容量(capacity)代表Channel容纳的最多的元素的数量,代表Channel的缓存的大小。
如果没有设置容量,或者容量设置为0, 说明Channel没有缓存,只有sender和receiver都准备好了后它们的通讯(communication)才会发生(Blocking)。
如果设置了缓存,就有可能不发生阻塞, 只有buffer满了后 send才会阻塞, 而只有缓存空了后receive才会阻塞。一个nil channel不会通信。
通过 len 函数可以获得 chan 中的元素个数,通过 cap 函数可以得到 channel 的缓存长度。
range 遍历
channel 也可以使用 range 取值,并且会一直从 channel 中读取数据,直到有 goroutine 对改 channel 执行 close 操作,循环才会结束。
go
// consumer worker
ch := make(chan int, 10)
for x := range ch{
fmt.Println(x)
}
关闭 Channel
golang 提供了内置的 close 函数对 channel 进行关闭操作。
关闭一个未初始化(nil) 的 channel 会产生 panic
重复关闭同一个 channel 会产生 panic
向一个已关闭的 channel 中发送消息会产生 panic
从已关闭的 channel 读取消息不会产生 panic,且能读出 channel 中还未被读取的消息,若消息均已读出,则会读到类型的零值。
从一个已关闭的 channel 中读取消息永远不会阻塞,并且会返回一个为 false 的 ok-idiom,可以用它来判断 channel 是否关闭
关闭 channel 会产生一个广播机制,所有向 channel 读取消息的 goroutine 都会收到消息
go
// 关闭
ch := make(chan int)
close(ch)
// 使用一个额外的返回参数来检查channel是否关闭。 如果OK 是false,表明接收的x是产生的零值,这个channel被关闭了或者为空。
x, ok := <- ch
fmt.Println(x, ok)
select
select 是用于处理通道操作的关键字,通常用于在多个通道上等待操作。它允许你在多个通道上进行非阻塞的操作,并根据操作的情况执行相应的代码块。select 语句使得在多个通道之间进行选择和控制变得更加灵活。
select语句选择一组可能的send操作和receive操作去处理。它类似switch,但是只是用来处理通讯(communication)操作。
- 它的
case可以是send语句,也可以是receive语句,亦或者default。 - 如果有同时多个
case去处理,那么Go会伪随机的选择一个case处理(pseudo-random)。 - 如果没有
case需要处理,则会选择default去处理,如果default case存在的情况下。 - 如果没有
default case,则select语句会阻塞,直到某个case需要处理。
receive 语句可以将值赋值给一个或者两个变量。它必须是一个receive操作。
最多允许有一个default case,它可以放在case列表的任何位置,尽管我们大部分会将它放在最后。
nil channel上的操作会一直被阻塞,如果没有default case,只有nil channel的select会一直被阻塞。
go
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)
}
select语句和switch语句一样它不是循环它只会选择一个case来处理,如果想一直处理channel你可以在外面加一个无限的for循环:
go
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
超时处理
select有很重要的一个应用就是超时处理。 因为上面我们提到,如果没有case需要处理,select语句就会一直阻塞着。这时候我们可能就需要一个超时操作,用来处理超时的情况。
time.After方法,它返回一个类型为<-chan Time的单向的channel,在指定的时间发送一个当前时间给返回的channel中。
下面这个例子我们会在2秒后往channel c1中发送一个数据,但是select设置为1秒超时,因此我们会打印出timeout 1,而不是result 1。
go
import "time"
import "fmt"
func main() {
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
}
Timer 和 Ticker
timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个Channel,在将来的那个时间那个Channel提供了一个时间值。
go
// 例子中第二行会阻塞2秒钟左右的时间,直到时间到了才会继续执行。
timer1 := time.NewTimer(time.Second * 2)
<-timer1.C
fmt.Println("Timer 1 expired")
当然如果只是想单纯的等待的话,可以使用time.Sleep来实现。还可以使用timer.Stop来停止计时器。
go
timer2 := time.NewTimer(time.Second)
go func() {
<-timer2.C
fmt.Println("Timer 2 expired")
}()
stop2 := timer2.Stop()
if stop2 {
fmt.Println("Timer 2 stopped")
}
ticker是一个定时触发的计时器,它会以一个间隔interval往Channel发送一个事件(当前时间),而Channel的接收者可以以固定的时间间隔从Channel中读取事件。
go
// 例子中ticker每500毫秒触发一次,你可以观察输出的时间。
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
timer, ticker也可以通过Stop方法来停止。一旦它停止,接收者不再会从channel中接收数据了。
fmt
fmt包实现了类似C语言printf和scanf的格式化I/O。格式化动作('verb')源自C语言但更简单。
通用
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 值的类型的Go语法表示
%% 百分号
布尔值
%t单词true或false
整数
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"
浮点数与复数
%b 无小数部分、二进制指数的科学计数法,如-123456p-78
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
字符串和 []byte
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f)
%X 每个字节用两字符十六进制数表示(使用A-F)
指针
%p 表示为十六进制,并加上前导的0x
格式化错误
如果给某个占位符提供了非法的参数,如给%d提供了一个字符串,生成的字符串会包含该问题的描述,如下所例:
go
// 类型错误或占位符未知:%!verb(type=value)
Printf("%d", hi)
// %!d(string=hi)
// 实参太多:%!(EXTRA type=value)
Printf("hi", "guys")
// hi%!(EXTRA string=guys)
// 实参太少:%!verb(MISSING)
Printf("hi%d")
// hi %!d(MISSING)
// 宽度或精度不是 int 类型:%!(BADWIDTH)或 %!(BADPREC)
Printf("%*s", 4.5, "hi")
// %!(BADWIDTH)hi
Printf("%.*s", 4.5, "hi")
// %!(BADPREC)hi