Linux服务¶
我自己对golang文档的学习笔记
http://c.biancheng.net/view/4356.html
简介¶
golang起源2007年, 在2009年对外发布,主要目标是兼具python等动态语言的开发速度和c语言等变异性语言的性能与安全性。
go是编译型语言¶
go使用编译器来编译代码,将源代码编译为二进制格式,程序员需要执行如下步骤:
- 使用文本编辑器创建go程序
- 编译程序
- 运行编译好的可执行程序
Note
go自带编译器,因此无需单独安装编译器。
为什么要学习go语言¶
如果创建系统程序,或者基于网络的程序,golang是个很不错的选择。golang在快速编译、高效执行、易于开发之间做到最佳平衡。golang支持交叉编译,完全支持utf-8编码,做到真正国际化。
特性¶
并发模型¶
Goroutine是go最显著特征,它用类协程的方式来处理并发单元,却又在运行时层面做了深度优化处理。
内存分配¶
go采用tcmalloc, 它本身就是为并发设计的高性能内存分配组件。
垃圾回收¶
每次升级,垃圾回收器必然是核心组件里修改最多的部分,逐步引入三色标记和写屏障等等,都是为了能让垃圾 回收在不影响用户逻辑的情况下更好地工作。尽管有了努力,当前版本的垃圾回收算法也只能说堪用,离好用尚有不少距离。
静态链接¶
Go 刚发布时,静态链接被当作优点宣传。只须编译后的一个可执行文件,无须附加任何东西就能部署。
标准库¶
功能完善、质量可靠的标准库为编程语言提供了充足动力。
工具链¶
完整的工具链对于日常开发极为重要。Go 在此做得相当不错,无论是编译、格式化、错误检查、帮助文档,还是第三方包下载、 更新都有对应的工具。其功能未必完善,但起码算得上简单易用。
并发而生¶
Go语言正是在多核和网络化的时代背景下诞生的原生支持并发的编程语言。Go语言从底层原生支持并发,无须第三方库,开发人员可以很轻松地在编写程序时决定怎么 使用 CPU 资源。
Go语言的并发是基于 goroutine 的,goroutine 类似于线程,但并非线程。可以将 goroutine 理解为一种虚拟线程。Go语言运行时会参与调度 goroutine, 并将 goroutine 合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。
多个 goroutine 中,Go语言使用通道(channel)进行通信,通道是一种内置的数据结构,可以让用户在不同的 goroutine 之间同步发送具有类型的消息。 这让编程模型更倾向于在 goroutine 之间发送消息,而不是让多个 goroutine 争夺同一个数据的使用权。
哪些项目使用go¶
Docker¶
docker是一种操作系统层的虚拟化技术,可以在应用程序和操作系统之间隔离,可称之为容器。
Kubernetes¶
google公司开发的构建与docker之上的容器调度服务,用户通过k8s集群进行云端容器集群管理。
哪些公司使用go¶
目前越来越多的大厂开始使用golang, google、fackbook、腾讯等。
go适合做什么¶
go擅长领域¶
- 服务器编程方面,go适合处理日志,数据打包,虚拟机处理,文件系统,分布式系统,数据库代理等。
- 网络编程方面: 适用于web应用,api应用,下载应用。
代表¶
- 云计算基础设施领域,代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。
- 基础软件,代表项目:tidb、influxdb、cockroachdb 等。
- 微服务,代表项目:go-kit、micro、monzo bank 的 typhon、bilibili 等。
- 互联网基础设施,代表项目:以太坊、hyperledger 等。
go优势和劣势¶
优势¶
- 对比c语言来讲,go拥有清晰的依赖管理和全自动的垃圾回收机制。
- 对比java, go拥有简明的类型系统,函数式编程方式和先进的并发编程模型。
- 对比php, go拥有更通用性和规范性。 性能方面占绝对优势。
- 对于python/ruby, go的优势在于其简单的语法、非侵入式和扁平化的类型系统的多范式编程模型。
劣势¶
- 从分布式计算的角度来看,Go语言的成熟度不及 Erlang
- 从程序运行速度的角度来看,Go语言虽然已与 Java 不相上下,但还不及 C
- 从第三方库的角度来看,Go语言的库数量还远远不及其他几门主流语言
依赖管理¶
godep¶
godep 是一个Go语言官方提供的通过 vender 模式来管理第三方依赖的工具,类似的还有由社区维护的准官方包管理工具 dep。
Go语言从 1.5 版本开始开始引入 vendor 模式,如果项目目录下有 vendor 目录,那么Go语言编译器会优先使用 vendor 内的包进行编译、测试等。
安装godep
go get github.com/tools/godep
安装go¶
大家可以在Go语言官网下载对应版本的的安装包(https://golang.google.cn/dl/)
工程结构详述¶
- src目录: 防止项目和库的源文件
- pkg目录: 放置编译后生成的包的归档文件
- bin: 放置编译后的二进制文件
第一个go程序¶
基本每个语言学习, 都会来个hello world , go也不例外。
package main
import "fmt"
func main(){
fmt.Println("hello world")
}
变量逃逸分析¶
栈只允许从线性表的同一端放入和取出数据,按照后进先出(LIFO,Last InFirst Out)的顺序
堆分配内存和栈分配内存相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。
堆和栈各有优缺点,该怎么在编程中处理这个问题呢?在 C/C++ 语言中,需要开发者自己学习如何进行内存分配,选用怎样的内存分配方式来适应不同的算法需求。 比如,函数局部变量尽量使用栈,全局变量、结构体成员使用堆分配等。程序员不得不花费很长的时间在不同的项目中学习、记忆这些概念并加以实践和使用。 Go语言将这个过程整合到了编译器中,命名为“变量逃逸分析”。通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
在使用Go语言进行编程时,Go语言的设计者不希望开发者将精力放在内存应该分配在栈还是堆的问题上,编译器会自动帮助开发者完成这个纠结的选择, 但变量逃逸分析也是需要了解的一个编译器技术,这个技术不仅用于Go语言,在 Java 等语言的编译器优化上也使用了类似的技术。
编译器觉得变量应该分配在堆和栈上的原则是: - 变量是否被取地址; - 变量是否发生逃逸。
指针¶
- 指针(pointer)在Go语言中可以被拆分为两个核心概念:类型指针,允许对这个指针类型的数据进行修改,传递数据可以直接使用指针,而无须拷贝数据,类型指针不能进行偏移和运算。
- 切片,由指向起始元素的原始指针、元素数量和容量组成。
指针基础样例¶
package main
import "fmt"
func main(){
// 定义2个变量
var a int = 1
var b int = 2
// 使用 &变量名 获取对应变量的地址
fmt.Printf("%p %p", &a, &b)
var ptr_a = &a
//var ptr_b = &b
// 打印ptr的类型
fmt.Printf("ptr type: %T\n", ptr_a)
// 打印ptr的指针地址
fmt.Printf("address: %p\n", ptr_a)
aa := *ptr_a
// 取值后的类型
fmt.Printf("value type: %T\n", aa)
// 指针取值后就是指向变量的值
fmt.Printf("value: %d\n", aa)
}
指针交互变量值¶
package main
import "fmt"
func swap(a *int, b *int) {
var t = *a
*a = *b
*b = t
}
func mainabc() {
var a = 20
var b = 10
fmt.Println(a, b)
swap(&a, &b)
fmt.Println(a, b)
}
new创建指针¶
package main
import "fmt"
func main() {
str := new(string)
*str = "Go语言教程"
fmt.Println(*str)
}
变量生命周期¶
变量的生命周期与变量的作用域有着不可分割的联系:
- 全局变量:它的生命周期和整个程序的运行周期是一致的;
- 局部变量:它的生命周期则是动态的,从创建这个变量的声明语句开始,到这个变量不再被引用为止;
- 形式参数和函数返回值:它们都属于局部变量,在函数被调用的时候创建,函数调用结束后被销毁。
关键字与标识符简述¶
词法元素包括 5 种,分别是标识符(identifier)、关键字(keyword)、操作符(operator)、分隔符(delimiter)、字面量(literal), 它们是组成Go语言代码和程序的最基本单位。
关键字¶
Go语言中的关键字一共有 25 个:
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
标示符¶
标识符是指Go语言对各种变量、方法、函数等命名时使用的字符序列, 标识符由若干个字母、下划线_、和数字组成,且第一个字符必须是字母。 通俗的讲就是凡可以自己定义的名称都可以叫做标识符。 下划线_是一个特殊的标识符,称为空白标识符,
匿名变量¶
匿名变量的特点是一个下画线“_”,“_”本身就是一个特殊的标识符,被称为空白标识符。
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}
func GetData() (int, int) {
return 100, 200
}
变量定义¶
单个变量定义¶
var 变量名 变量类型
多个变量定义¶
var (
a int
b string
c []float32
d func() bool
e struct {
x int
}
)
单个简单方式¶
名字 := 表达式
基本数据类型¶
整数类型¶
Go语言同时提供了有符号和无符号的整数类型,其中包括 int8、int16、int32 和 int64 四种大小截然不同的有符号整数类型, 分别对应 8、16、32、64 bit(二进制位)大小的有符号整数,与此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整数类型。
浮点类型¶
提供了两种精度的浮点数 float32 和 float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。
复数类型¶
复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。
bool类型¶
一个布尔类型的值只有两种:true 或 false。if 和 for 语句的条件部分都是布尔类型的值,并且==和<等比较操作也会产生布尔型的值。
字符串¶
一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列 (当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。
定义多行字符串
const str = `第一行
第二行
第三行`
字符类型¶
Go语言的字符有以下两种: - 一种是 uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符。 - 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型等价于 int32 类型。
广义的 Unicode 指的是一个标准,它定义了字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。
类型转换¶
valueOfTypeB = typeB(valueOfTypeA)
常量¶
常量使用关键字 const 定义,用于存储不会改变的数据,常量是在编译时被创建的,即使定义在函数内部也是如此, 并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型。由于编译时的限制,定义常量的表达式必须为能被编译器求值的常量表达式。
const (
e = 2.7182818
pi = 3.1415926
)
iota生成常量¶
type Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
模拟枚举¶
const (
FlagNone = 1 << iota
FlagRed
FlagGreen
FlagBlue
)
fmt.Printf("%d %d %d\n", FlagRed, FlagGreen, FlagBlue)
fmt.Printf("%b %b %b\n", FlagRed, FlagGreen, FlagBlue)
数组¶
数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,所以在Go语言中很少直接使用数组。
声明方式¶
var 数组变量名 [元素数量]Type
遍历数组¶
var team [3]string
team[0] = "hammer"
team[1] = "soldier"
team[2] = "mum"
for k, v := range team {
fmt.Println(k, v)
}
多维数组¶
允许使用多维数组,因为数组属于值类型,所以多维数组的所有维度都会在创建时自动初始化零值, 多维数组尤其适合管理具有父子关系或者与坐标系相关联的数据。
声明方式¶
var array_name [size1][size2]...[sizen] array_type
// demo
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}
切片¶
切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型, 或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。
使用方式¶
slice [开始位置 : 结束位置]
使用 make() 函数构造切片¶
a := make([]int, 2)
b := make([]int, 2, 10)
切片操作¶
append¶
内建函数 append() 可以为切片动态添加元素,代码如下
var a []int
a = append(a,1)
a = append(a,1,2,3)
a = append(a,[]int{ 1,2,3}
a = append(a[:i], append([]int{1,2,3})
Note
数组的长度和容量是2个概念, 切片在扩容的时候, 容量是原来容量的2倍进行扩充的。
copy¶
内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
delete¶
并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、 从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。
a = []int{1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素
a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
Note
连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、 频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。
range¶
range需要配合for来完成多个相同类型的元素的迭代问题。
func main() {
slice := []int {10,20,30,40}
for index, value := range slice {
fmt.Printf("Index: %d Value: %d\n", index, value)
}
for index :=2 ; index < len(slice) ; index ++ {
fmt.Printf("Value %d", slice[index])
}
}
map¶
map 是一种特殊的数据结构,一种元素对(pair)的无序集合,pair 对应一个 key(索引)和一个 value(值),所以这个结构也称为关联数组或字典。
func main() {
var demo01 map[string]int
var demo02 map[string]int
demo01 = map[string]int { "one":1,"two":2}
demo02 = make(map[string]int, 10)
demo02["one"] =1
demo02["two"] =2
for k,v :=range demo01 {
fmt.Println(k, v)
}
for k,v :=range demo02 {
fmt.Println(k, v)
}
fmt.Println("======")
delete(demo02, "one")
for k,v :=range demo02 {
fmt.Println(k, v)
}
}
Go语言中并没有为 map 提供任何清空所有元素的函数、方法,清空 map 的唯一办法就是重新 make 一个新的 map, 不用担心垃圾回收的效率,Go语言中的并行垃圾回收效率比写一个清空函数要高效的多。
sync.Map¶
map 在并发情况下,只读是线程安全的,同时读写是线程不安全的。
sync.Map 没有提供获取 map 数量的方法,替代方法是在获取 sync.Map 时遍历自行计算数量,sync.Map 为了保证并发安全有一些性能损失, 因此在非并发情况下,使用 map 相比使用 sync.Map 会有更好的性能。
package main
import (
"fmt"
"sync"
)
func main01() {
m := make(map[int]int)
go func(){
for {
m[1] = 1
}
}()
go func(){
for {
_ = m[1]
}
}()
for{
}
}
func main(){
var m sync.Map
m.Store("one",1)
m.Store("two",2)
fmt.Println(m.Load("one"))
m.Delete("one")
m.Range(func(k,v interface{}) bool{
fmt.Println(k,v)
return true
})
}
列表¶
列表使用 container/list 包来实现,内部的实现原理是双链表,列表能够高效地进行任意位置的元素插入和删除操作。
package main
import "fmt"
import "container/list"
func main() {
l := list.New()
l.PushBack("fist")
l.PushFront(67)
element := l.PushBack("fist")
l.InsertAfter("high", element)
l.InsertBefore("noon", element)
l.Remove(element)
for i := l.Front() ; i !=nil ; i=i.Next(){
fmt.Println(i.Value)
}
}
空值¶
布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串””,而指针、切片、映射、通道、函数和接口的零值则是 nil。
- nil 标识符是不能比较的
- nil 没有默认类型
- 不同类型 nil 的指针是一样的,地址都是0x0
- 不同类型的 nil 值占用的内存大小可能是不一样的
make和new¶
new 和 make 是两个内置函数,主要用来创建并分配类型的内存。在我们定义变量的时候,可能会觉得有点迷惑, 不知道应该使用哪个函数来声明变量,其实他们的规则很简单,new 只分配内存,而 make 只能用于 slice、map 和 channel 的初始化,
make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身, 而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
Go语言中的 new 和 make 主要区别如下:
- make 只能用来分配及初始化类型为 slice、map、chan 的数据。new 可以分配任意类型的数据;
- new 分配返回的是指针,即类型 *Type。make 返回引用,即 Type;
- new 分配的空间被清零。make 分配空间后,会进行初始化;
if¶
关键字 if 是用于测试某个条件(布尔型或逻辑型)的语句,如果该条件成立,则会执行 if 后由大括号{}括起来的代码块,否则就忽略该代码块继续执行后续的代码。
// 第一种情况
if condition {
// do something
}
// 第二种情况
if condition {
// do something
} else {
// do something
}
if condition1 {
// do something
} else if condition2 {
// do something else
}else {
// catch-all or default
}
// 特殊用法
if err := Connect(); err != nil {
fmt.Println(err)
return
}
Note
关键字 if 和 else 之后的左大括号{必须和关键字在同一行,
for¶
循环语句只支持 for 关键字,而不支持 while 和 do-while 结构,
// 第一种使用
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
// 第二种使用
sum := 0
for {
sum++
if sum > 100 {
break
}
}
// 第三种
step := 2
for ; step > 0; step-- {
fmt.Println(step)
}
// 第四种
for i <= 10 {
i++
}
for range¶
for range 可以用于遍历数组、切片、字符串、map及通道, 类似其他的语言中的foreach语句。
for key, value := range []int{1, 2, 3, 4} {
fmt.Printf("key:%d value:%d\n", key, value)
}
m := map[string]int{
"hello": 100,
"world": 200,
}
for key, value := range m {
fmt.Println(key, value)
}
c := make(chan int)
go func() {
c <- 1
c <- 2
c <- 3
close(c)
}()
for v := range c {
fmt.Println(v)
}
case¶
witch 要比C语言的更加通用,表达式不需要为常量,甚至不需要为整数,case 按照从上到下的顺序进行求值, 直到找到匹配的项,如果 switch 没有表达式,则对 true 进行匹配,
var a = "hello"
switch a {
case "hello":
fmt.Println(1)
case "world":
fmt.Println(2)
default:
fmt.Println(0)
}
// 一个分支多支的
var a = "mum"
switch a {
case "mum", "daddy":
fmt.Println("family")
}
// 分支表达式
var r int = 11
switch {
case r > 10 && r < 20:
fmt.Println(r)
}
//case 是一个独立的代码块,执行完毕后不会像C语言那样紧接着执行下一个 case,但是为了兼容一些移植代码,依然加入了 fallthrough 关键字来实现这一功能,
var s = "hello"
switch {
case s == "hello":
fmt.Println("hello")
fallthrough
case s != "world":
fmt.Println("world")
}
contine¶
continue 语句可以结束当前循环,开始下一次的循环迭代过程,仅限在 for 循环内使用,在 continue 语句后添加标签时,表示开始标签对应的循环。
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
continue OuterLoop
}
}
}
}
break¶
break 语句可以结束 for、switch 和 select 的代码块,另外 break 语句还可以在语句后面添加标签,表示退出某个标签对应的代码块, 标签要求必须定义在对应的 for、switch 和 select 的代码块上。
package main
import "fmt"
func main() {
OuterLoop:
for i := 0; i < 2; i++ {
for j := 0; j < 5; j++ {
switch j {
case 2:
fmt.Println(i, j)
break OuterLoop
case 3:
fmt.Println(i, j)
break OuterLoop
}
}
}
}
函数声明¶
Go语言里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 lambda 函数
- 方法
普通函数
func hypot(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}
fmt.Println(hypot(3,4)) // "5"
// 函数变量
var f func()
f = hypot
f()
package main
import (
"fmt"
"strings"
)
func StringProccess(list []string , chain []func(string) string ){
for index, str := range list{
result :=str
for _, proc := range chain {
result = proc(result)
}
list[index] = result
}
}
func removePrefix(str string ) string {
return strings.TrimPrefix(str,"go")
}
func main() {
list := []string{
"go scanner",
"go parser",
"go compiler",
"go printer",
"go formater",
}
chain := []func(string) string{
removePrefix,
strings.TrimSpace,
strings.ToUpper,
}
StringProccess(list, chain)
for _, str := range list {
fmt.Println(str)
}
}
可变参数¶
可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型。
package main
import "fmt"
func myfunc(args ...int ) {
for _,arg :=range(args){
fmt.Println(arg)
}
}
func main(){
myfunc(1,2,3)
}
任意类型的可变参数
func Printf(format string, args ...interface{}) {
// ...
}
延迟执行¶
defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行, 也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。
package main
import "fmt"
func main(){
fmt.Println("defer begin")
// 将defer放入延迟调用栈
defer fmt.Println(1)
defer fmt.Println(2)
// 最后一个放入, 位于栈顶, 最先调用
defer fmt.Println(3)
fmt.Println("defer end")
}
defer 语句正好是在函数退出时执行的语句,所以使用 defer 能非常方便地处理资源释放问题。
递归¶
func fibonacci(n int) (res int) {
if n <= 2 {
res = 1
} else {
res = fibonacci(n-1) + fibonacci(n-2)
}
return
}
使用递归的条件
- 一个问题可以拆分为多个子问题
- 拆分后和原问题一致,只是规模小了而已。
- 需要有终止条件。
运行时错误¶
希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。
接口错误的定义格式
type error interface {
Error() string
}
宕机¶
go语言会在编译时捕获很多错误信息,但有些错误只能在运行时检查,如数组越界,空指针引用等问题,都会引发宕机问题, 宕机不是,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样,但是,如果在损失发生时, 程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命,因此,宕机有时也是一种合理的止损方法。
package main
func main() {
panic("crash")
}
recover¶
panic 和 recover 的组合有如下特性:
- 有 panic 没 recover,程序宕机。
- 有 panic 也有 recover,程序不会宕机,执行完对应的 defer 后,从宕机点退出当前函数后继续执行。
测试¶
Go语言自带了 testing 测试包,可以进行自动化的单元测试,输出结果验证,并且可以测试性能。
测试文件必须”_test.go结尾”, 测试函数必须 “Test开头”
package main
import "fmt"
func GetArea(x int , y int) {
return x * y
}
package main
import "testing"
func TestGetArea(t *testing.T) {
area := GetArea(40,50)
if area !=2000{
t.Error("测试失败")
}
}
结构体¶
定义¶
可以通过自定义的方式形成新的类型,结构体就是这些类型中的一种复合类型,结构体是由零个或多个任意类型的值聚合成的实体, 每个值都可以称为结构体的成员。
type Point struct {
X int
Y int
}
初始化¶
type Point struct {
X int
Y int
}
var p Point
p.X = 10
p.Y = 20
键值对填充结构体¶
type People struct {
name string
child *People
}
relation := &People{
name: "爷爷",
child: &People{
name: "爸爸",
child: &People{
name: "我",
},
},
}
多值方式初始化¶
type Address struct {
Province string
City string
ZipCode int
PhoneNumber string
}
addr := Address{
"四川",
"成都",
610000,
"0",
}
fmt.Println(addr)
构造函数¶
结构体没有构造函数的功能,但是我们可以使用结构体初始化的过程来模拟实现构造函数。
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat {
Name: name,
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color,
}
}
接收器¶
Go 方法是作用在接收器(receiver)上的一个函数,接收器是某种类型的变量,因此方法是一种特殊类型的函数。
添加方法¶
type Bag struct {
items []int
}
// 将一个物品放入背包的过程
func Insert(b *Bag, itemid int) {
b.items = append(b.items, itemid)
}
func main() {
bag := new(Bag)
Insert(bag, 1001)
}
添加方式2¶
type Bag struct {
items []int
}
// 这个函数使用接收器, 接收insert方法。
func (b *Bag) Insert(itemid int) {
b.items = append(b.items, itemid)
}
func main() {
b := new(Bag)
b.Insert(1001)
}
垃圾回收¶
自带垃圾回收机制(GC)。GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源。
finalizer(终止器)是与对象关联的一个函数,通过 runtime.SetFinalizer 来设置,如果某个对象定义了 finalizer, 当它被 GC 时候,这个 finalizer 就会被调用,以完成一些特定的任务,例如发信号或者写日志等。
链表¶
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
package main
import "fmt"
type Node struct {
data int
next *Node
}
func ShowNode(p *Node) {
for p !=nil {
fmt.Println(*p)
p = p.next //移动指针
}
}
func main() {
var head = new(Node)
head.data = 1
var node1 = new(Node)
node1.data = 2
head.next = node1
var node2 = new(Node)
node2.data = 3
node1.next = node2
Shownode(head)
}
模拟继承¶
package main
import "fmt"
// 可飞行的
type Flying struct{}
func (f *Flying) Fly() {
fmt.Println("can fly")
}
// 可行走的
type Walkable struct{}
func (f *Walkable) Walk() {
fmt.Println("can calk")
}
// 人类
type Human struct {
Walkable // 人类能行走
}
// 鸟类
type Bird struct {
Walkable // 鸟类能行走
Flying // 鸟类能飞行
}
func main() {
// 实例化鸟类
b := new(Bird)
fmt.Println("Bird: ")
b.Fly()
b.Walk()
// 实例化人类
h := new(Human)
fmt.Println("Human: ")
h.Walk()
}
事件处理¶
可以将类型的方法与普通函数视为一个概念,从而简化方法和函数混合作为回调类型时的复杂性。这个特性和 C# 中的代理(delegate)类似, 调用者无须关心谁来支持调用,系统会自动处理是否调用普通函数或类型的方法。
package main
import "fmt"
// 实例化一个通过字符串映射函数切片的map
var eventByName = make(map[string][]func(interface{}))
// 注册事件,提供事件名和回调函数
func RegisterEvent(name string, callback func(interface{})) {
// 通过名字查找事件列表
list := eventByName[name]
// 在列表切片中添加函数
list = append(list, callback)
// 将修改的事件列表切片保存回去
eventByName[name] = list
}
// 调用事件
func CallEvent(name string, param interface{}) {
// 通过名字找到事件列表
list := eventByName[name]
// 遍历这个事件的所有回调
for _, callback := range list {
// 传入参数调用回调
callback(param)
}
}
// 声明角色的结构体
type Actor struct {
}
// 为角色添加一个事件处理函数
func (a *Actor) OnEvent(param interface{}) {
fmt.Println("actor event:", param)
}
// 全局事件
func GlobalEvent(param interface{}) {
fmt.Println("global event:", param)
}
func main() {
// 实例化一个角色
a := new(Actor)
// 注册名为OnSkill的回调
RegisterEvent("OnSkill", a.OnEvent)
// 再次在OnSkill上注册全局事件
RegisterEvent("OnSkill", GlobalEvent)
// 调用事件,所有注册的同名函数都会被调用
CallEvent("OnSkill", 100)
}
一般来说,事件系统不保证同一个事件实现方多个函数列表中的调用顺序,事件系统认为所有实现函数都是平等的。也就是说,无论例子中的 a.OnEvent 先注册,还是 GlobalEvent() 函数先注册,最终谁先被调用,都是无所谓的,开发者不应该去关注和要求保证调用的顺序。
io操作¶
几乎所有的数据结构都围绕接口展开,接口是Go语言中所有数据结构的核心。在实际开发过程中,无论是实现 web 应用程序,还是控制台输入输出, 又或者是网络操作,都不可避免的会遇到 I/O 操作。
package main
import (
"bufio"
"bytes"
"fmt"
)
func main() {
wr := bytes.NewBuffer(nil)
w := bufio.NewWriter(wr)
s := "C语言中文网"
n, err := w.WriteString(s)
w.Flush()
fmt.Println(string(wr.Bytes()), n, err)
}
断言¶
package main
import (
"fmt"
)
func main() {
var x interface{}
x = 10
value, ok := x.(int)
fmt.Print(value, ",", ok)
}
接口声明¶
Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。
样例
type Writer interface {
Write(p []byte) (n int, err error)
}
接口嵌套¶
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type WriteCloser interface {
Writer
Closer
}
简单web¶
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", index) // index 为向 url发送请求时,调用的函数
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello linuxpanda")
}
包简介¶
Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。
包导入¶
# 单行导入方式
import "包 1 的路径"
// 多行导入方式
import (
"包 1 的路径"
"包 2 的路径"
)
//全路径导入
import "lab/test"
//相对路径导入
import "../a"
包加载¶

gopath¶
GOPATH 是 Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录。
➜ ~ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/dxm/Library/Caches/go-build"
GOENV="/Users/dxm/Library/Application Support/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/dxm/go"
GOPRIVATE=""
GOPROXY="https://mirrors.aliyun.com/goproxy/"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
GCCGO="gccgo"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/3p/jc3w8_dn5dd0r5_5ccp3nrqm0000gn/T/go-build619420410=/tmp/go-build -gno-record-gcc-switches -fno-common"
使用gopath工程结构¶
export GOPATH=`pwd`
mkdir -p src/hello
填写代码
go install hello
常用包¶
标准的Go语言代码库中包含了大量的包,并且在安装 Go 的时候多数会自动安装到系统中。我们可以在 $GOROOT/src/pkg 目录中查看这些包。
fmt: fmt 包实现了格式化的标准输入输出 io: 这个包提供了原始的 I/O 操作界面。它主要的任务是对 os 包这样的原始的 I/O 进行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。 sort: 提供用于对切片和用户定义的计划进行排序的功能 strconv: 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能。 os: os 包提供了不依赖平台的操作系统函数接口,设计像 Unix 风格,但错误处理是 go 风格,当 os 包使用时,如果失败后返回错误类型而不是错误数量。 sync: sync 包实现多线程中锁机制以及其他同步互斥机制。 flag: flag 包提供命令行参数的规则定义和传入参数解析的功能。绝大部分的命令行程序都需要用到这个包。 encoding/json: encoding/json 包提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等。
导出包符号¶
如果想在一个包里引用另外一个包里的标识符(如类型、变量、常量等)时,必须首先将被引用的标识符导出,将要导出的标识符的首字母大写就可以让引用者可以访问这些标识符了。
概述¶
golang提供一种机制在运行时更新和检查变量的值,调用变量的方法和变量支持的操作。但是在编译时候不知道这些变量的具体类型, 这种机制成为反射。
reflect 包¶
Go语言中的反射是由 reflect 包提供支持的,它定义了两个重要的类型 Type 和 Value 任意接口值在反射中都可以理解为由 reflect.Type 和 reflect.Value 两部分组成, 并且 reflect 包提供了 reflect.TypeOf 和 reflect.ValueOf 两个函数来获取任意对象的 Value 和 Type。
指针和指针指向的元素¶
package main
import (
"fmt"
"reflect"
)
func main100() {
// 声明一个空结构体
type cat struct {
}
// 创建cat的实例
ins := &cat{}
// 获取结构体实例的反射类型对象
typeOfCat := reflect.TypeOf(ins)
// 显示反射类型对象的名称和种类
fmt.Printf("name:'%v' kind:'%v'\n", typeOfCat.Name(), typeOfCat.Kind())
// 取类型的元素
typeOfCat = typeOfCat.Elem()
// 显示反射类型对象的名称和种类
fmt.Printf("element name: '%v', element kind: '%v'\n", typeOfCat.Name(), typeOfCat.Kind())
}
测试结果如下
name:'' kind:'ptr'
element name: 'cat', element kind: 'struct'
指针类型的typename都是空的, typekind是ptr的,指针类型item是具体元素类型。
使用反射获取结构体的成员类型¶
- field: 根据索引获取到具体结构体字段信息。
- NumFiedLd: 获取字段个数。
- FieldByName: 跟进名字获取对应结构体字段信息。
- FieldByIndex: 根据索引获取对应结构体的字段信息。
- FieldByNameFunc: 根据匹配函数返回对应的字段。
反射定律¶
- 反射可以将接口类型变量转化为反射类型对象
- 反射可以将反射类型对象转换为接口类型变量
- 反射如果要修改反射类型对象,其值必须是可写的。
反射案例¶
反射创建实例¶
样例如下
package main
import (
"fmt"
"reflect"
)
func main() {
var a int
// 取变量a的反射类型对象
typeOfA := reflect.TypeOf(a)
// 根据反射类型对象创建类型实例
aIns := reflect.New(typeOfA)
// 输出Value的类型和种类
fmt.Println(aIns.Type(), aIns.Kind())
}
反射调用函数¶
package main
import (
"fmt"
"reflect"
)
func add(a, b int) int {
return a + b
}
func main() {
fv := reflect.ValueOf(add)
paramList := []reflect.Value{
reflect.ValueOf(10),
reflect.ValueOf(20),
}
retList := fv.Call(paramList)
fmt.Println(retList[0].Int())
}
性能和灵活性¶
- 能使用原生代码时,尽量避免反射操作。
- 提前缓冲反射值对象,对性能有很大的帮助。
- 避免反射函数调用,实在需要调用时,先提前缓冲函数参数列表,并且尽量少地使用返回值。