Golang学习笔记,从入门到精通,持续记录

14,128次阅读
没有评论

共计 10788 个字符,预计需要花费 27 分钟才能阅读完成。

Golang 官网:https://go.dev/、Golang 下载:https://go.dev/、Golang 学习文档:https://go.dev/doc/

Go 标准库文档:https://pkg.go.dev/std

Golang 标准库中文文档:https://studygolang.com/pkgdoc

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC(垃圾回收),结构形态及 CSP-style 并发计算。

安装 Golang

相关文档:https://go.dev/doc/install,下载对应操作系统的安装包后,按说明安装即可;

环境变量

环境变量主要是能操作系统能在任意目录访问 go 的可执行文件,Window 下将 go 的安装目录添加到 PATH 环境变量即可(C:Program FilesGobin);

GOOS            #编译系统
GOARCH          #编译 arch
GO111MODULE     #gomod 开关
GOPROXY         #go 代理 https://goproxy.io  https://goproxy.cn
GOSSAFUNC       #生成 SSA.html 文件,展示代码优化的每一步 GOSSAFUNC=func_name go build
GOPATH          #用来指定项目开发目录,所有项目文件都在这个路径下面
GOROOT          #GO 的安装路径

依赖管理

1. 软件包

软件包仓库:https://pkg.go.dev/,Go1.13 之后 GOPROXY 默认值为 https://proxy.golang.org,在国内是无法访问的;修改镜像地址:

go env -w GOPROXY=https://goproxy.cn,direct

Go 语言从 v1.5 开始开始引入 vendor 模式,查找项目的某个依赖包,首先会在项目根目录下的 vender 文件夹中查找,如果没有找到就会去 $GOAPTH/src 目录下查找。

从 Go1.11 开始, Go 官方加入 Go Module 支持, Go1.12 成为默认支持; 从此告别源码必须放在 Gopath 中 以及 Gopath 对初学者造成的困扰.

要启用 go module 支持首先要设置环境变量 GO111MODULE(如其名字所暗示,GO111MODULE 是 Go 1.11 引入的新版模块管理方式。),通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是 auto。

  • GO111MODULE=off 禁用模块支持,编译时会从 GOPATH 和 vendor 文件夹中查找包。
  • GO111MODULE=on 启用模块支持,编译时会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod 下载依赖。
  • GO111MODULE=auto,当项目在 $GOPATH/src 外且项目根目录有 go.mod 文件时,开启模块支持。

简单来说,设置 GO111MODULE=on 之后就可以使用 go module 了,以后就没有必要在 GOPATH 中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

相关

使用 go module 管理依赖后会在项目根目录下生成两个文件 go.mod 和 go.sum。

2. 初始化

go mod init [module 名称]  #初始化
go mod tidy               #检测和清理依赖

3. 安装依赖

go get path@version #安装指定包
go get -u  #更新依赖
go get -v  #输出下载的包列表
go get -u github.com/go-ego/gse  #更新指定包依赖
go get -u github/com/go-ego/gse@v0.60.0-rc4.2  #指定版本
go list -json github.com/go-ego/gse #列出指定代码包的信息

4. 替换安装源

在国内访问 http://golang.org/ x 的各个包都需要翻墙,你可以在 go.mod 中使用 replace 替换成 github 上对应的库。

# 使用命令行:
go mod edit -replace github.com/go-ego/gse=/path/to/local/gse
go mod edit -replace github.com/go-ego/gse=github.com/vcaesar/gse

#直接修改模块文件:
replace github.com/go-ego/gse => github.com/vcaesar/gse

5. 常用命令

go mod init  # 初始化 go.mod
go mod tidy  # 拉取缺少的模块,移除不用的模块
go mod download  # 下载 go.mod 内的依赖,与 get 不同的是 get 会下载关联的依赖

go mod vendor  # 将依赖转移至本地的 vendor 文件
go mod edit  # 手动修改依赖文件
go mod graph  # 打印依赖图
go mod verify  # 校验依赖

6.GOPATH

go 命令依赖一个重要的环境变量:$GOPATH,Go 从 1.1 版本到 1.7 必须设置这个变量,而且不能和 Go 的安装目录一样,这个目录用来存放 Go 源码,Go 的可运行文件,以及相应的编译之后的包文件。所以这个目录下面有三个子目录:src、bin、pkg

从 go 1.8 开始,GOPATH 环境变量现在有一个默认值,如果它没有被设置。它在 Unix 上默认为 $HOME/go, 在 Windows 上默认为 %USERPROFILE%/go。

  • src 存放源代码(比如:.go .c .h .s 等)
  • pkg 编译后生成的文件(比如:.a)
  • bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中,如果有多个 gopath,那么使用 ${GOPATH//://bin:}/bin 添加所有的 bin 目录)

7. go.mod

go.mod 文件记录了项目所有的依赖信息,其结构大致如下:

module github.com/Q1mi/studygo/blogger

go 1.12

require (
	github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
	github.com/gin-gonic/gin v1.4.0
	github.com/go-sql-driver/mysql v1.4.1
	github.com/jmoiron/sqlx v1.2.0
	github.com/satori/go.uuid v1.2.0
	google.golang.org/appengine v1.6.1 // indirect
)
  • module 用来定义包名
  • require 用来定义依赖包及版本
  • indirect 表示间接引用

相关文档:https://zhuanlan.zhihu.com/p/359843333

module 用于定义当前应用的包名,可通过包名使用绝对路径来引入同目录的其它包

8. 常见问题

参考:http://www.9ong.com/062021/gomod%E5%8C%85%E7%AE%A1%E7%90%86.html

  • 使用 Go 的包管理方式,依赖的第三方包被下载到了 $GOPATH/pkg/mod 路径下。
  • $GOPATH/pkg/mod 下的包中最后会有一个版本号 v1.0.5,也就是说,$GOPATH/pkg/mod 里可以保存相同包的不同版本。版本是在 go.mod 中指定的。如果,在 go.mod 中没有指定,go 命令会自动下载代码中的依赖的最新版本,本例就是自动下载最新的版本。如果,在 go.mod 用 require 语句指定包和版本,go 命令会根据指定的路径和版本下载包,指定版本时可以用 latest,这样它会自动下载指定包的最新版本;
  • go 会根据 GO111MODULE 的值而采取不同的处理方式,默认情况下,GO111MODULE=auto 自动模式,auto 自动模式下,项目在 $GOPATH/src 里会使用 $GOPATH/src 的依赖包,在 $GOPATH/src 外,就使用 go.mod 里 require 的包,on 开启模式,1.12 后,无论在 $GOPATH/src 里还是在外面,都会使用 go.mod 里 require 的包
  • init 生成的 go.mod 的模块名称,用于引入自定义包:模块名 + 路径

IDE 工具

Jetbrains 全家桶:https://www.jetbrains.com/zh-cn/go/

代码目录结构 

GOPATH 下的 src 目录就是接下来开发程序的主要目录,所有的源码都是放在这个目录下面,那么一般我们的做法就是一个目录一个项目,例如: $GOPATH/src/mymath 表示 mymath 这个应用包或者可执行应用,这个根据 package 是 main 还是其他来决定,main 的话就是可执行应用,其他的话就是应用包。

所以当新建应用或者一个代码包时都是在 src 目录下新建一个文件夹,文件夹名称一般是代码包名称,当然也允许多级目录,例如在 src 下面新建了目录 $GOPATH/src/github.com/astaxie/beedb 那么这个包路径就是 ”github.com/astaxie/beedb”,包名称是最后一个目录 beedb

每一个可独立运行的 Go 程序,必定包含一个 package main,在这个 main 包中必定包含一个入口函数 main,而这个函数既没有参数,也没有返回值。

注意

一般建议 package 的名称和目录名保持一致

1. 编译应用包

# 编译后生成编译的应用包文件到 pkg
cd 应用包源代码目录
go install 
# 或者
go install  应用包源代码目录

命令行

1. 命令大全

go build      // 编译包和依赖包
go clean      // 移除对象和缓存文件
go doc        // 显示包的文档
go env        // 打印 go 的环境变量信息
go bug        // 报告 bug
go fix        // 更新包使用新的 api
go fmt        // 格式规范化代码
go generate   // 通过处理资源生成 go 文件
go get        // 下载并安装包及其依赖
go install    // 编译和安装包及其依赖
go list       // 列出所有包
go run        // 编译和运行 go 程序
go test       // 测试
go tool       // 运行给定的 go 工具
go version    // 显示 go 当前版本
go vet        // 发现代码中可能的错误

2. go get

get 命令用来解决 go 模块及其依赖项目的下载、创建和安装问题。实际该命令线执行从在线仓库(BitBucket、GitHub、Google Code、国内的 gitee 等)下载模块(包),再执行 Go Install 命令。get 命令是依赖 git。

get 会先下载相关依赖项目模块,下载时每个包或包的部分模块,下载的版本默认遵从以下顺序:最新 release 版 > 最新 pre-release 版 > 其他可用的较高版本

  • -d 只下载,而不执行创建、安装
  • -t 同时下载命令行指定包的测试代码(测试包)
  • -u 在线下载更新指定的模块(包)及依赖包(默认不更新已安装模块),并创建、安装
  • -v 打印出所下载的包名
  • -insecure 允许命令在非安全的 scheme(如 HTTP)下执行 get 命令
  • -fix 在下载代码包后先执行修正动作,而后再进行编译和安装,根据当前 GO 版本对所下载的模块(包)代码做语法修正
  • -f 忽略掉对已下载代码包的导入路径的检查
  • -x 打印输出,get 执行过程中的具体命令

基础语法

1. 数据类型

  1. 在 Go 中,布尔值的类型为 bool,值是 true 或 false,默认为 false。
  2. 整数类型有无符号和带符号两种。Go 同时支持 int 和 uint,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go 里面也有直接定义好位数的类型:rune, int8, int16, int32, int64 和 byte, uint8, uint16, uint32, uint64。其中 rune 是 int32 的别称,byte 是 uint8 的别称。
  3. 浮点数的类型有 float32 和 float64 两种(没有 float 类型),默认是 float64。
  4. Go 中的字符串都是采用 UTF- 8 字符集编码。字符串是用一对双引号(””)或反引号(` `)括起来定义,它的类型是 string。

1.1 字符串操作

  1. Go 字符串和 Java 一样不能被修改,重新赋值只是创建了一个新的字符串,并分配给了变量这个字符串的地址
  2. Go 中使用 + 操作符来连接两个字符串:
  3. 字符串虽不能更改,但可进行切片操作,s[1:]
  4. ` 括起的字符串为 Raw 字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。

2. 定义变量

  • 局部变量:在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
  • 全局变量:在函数体外声明的变量称之为全局变量,全局变量可以在整个包甚至外部包(被导出后)使用。

备注:

  • 局部变量不会一直存在,在函数被调用时存在,函数调用结束后变量就会被销毁,即生命周期。
  • Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。

使用 var 关键字是 Go 最基本的定义变量方式,与 C 语言不同的是 Go 把变量类型放在变量名后面

// 定义一个名称为“variableName”,类型为 "type" 的变量
var variableName type

// 定义三个类型都是“type”的变量
var vname1, vname2, vname3 type

// 初始化“variableName”的变量为“value”值,类型是“type”var variableName type = value

/*
定义三个类型都是 "type" 的变量, 并且分别初始化为相应的值
vname1 为 v1,vname2 为 v2,vname3 为 v3
*/
var vname1, vname2, vname3 type= v1, v2, 

类型推论,Go 可以通过变量初始化的值类型来判断变量类型。

/*
定义三个变量,它们分别初始化为相应的值
vname1 为 v1,vname2 为 v2,vname3 为 v3
然后 Go 会根据其相应值的类型来帮你初始化它们
*/
var vname1, vname2, vname3 = v1, v2, v3

/*
定义三个变量,它们分别初始化为相应的值
vname1 为 v1,vname2 为 v2,vname3 为 v3
编译器会根据初始化的值自动推导出相应的类型
*/
vname1, vname2, vname3 := v1, v2, v3

提示

“:=”这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明。

3. 常量

所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在 Go 程序中,常量可定义为数值、布尔值或字符串等类型

const constantName = value
// 如果需要,也可以明确指定常量的类型:const Pi float32 = 3.1415926

4. 分组声明

在 Go 语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

import(
	"fmt"
	"os"
)

const(
	i = 100
	pi = 3.1415
	prefix = "Go_"
)

var(
	i int
	pi float32
	prefix string
)

5.iota 枚举

Go 里面有一个关键字 iota,这个关键字用来声明 enum 的时候采用,它默认开始值是 0,const 中每增加一行加 1:

package main

import ("fmt")

const (
	x = iota // x == 0
	y = iota // y == 1
	z = iota // z == 2
	w        // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说 w = iota,因此 w == 3。其实上面 y 和 z 可同样不用 "= iota"
)

const v = iota // 每遇到一个 const 关键字,iota 就会重置,此时 v == 0

const (h, i, j = iota, iota, iota //h=0,i=0,j=0 iota 在同一行值相同)

const (
	a       = iota //a=0
	b       = "B"
	c       = iota             //c=2
	d, e, f = iota, iota, iota //d=3,e=3,f=3
	g       = iota             //g = 4
)

6.Go 默认行为

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于 class 中的带 public 关键词的公有函数;小写字母开头的就是有 private 关键词的私有函数。

7. 数组(Array)

在 [n]type 中,n 表示数组的长度,type 表示存储元素的类型。对数组的操作和其它语言类似,都是通过[] 来进行读取或赋值:

var arr [10]int  // 声明了一个 int 类型的数组
arr[0] = 42      // 数组下标是从 0 开始的
arr[1] = 13      // 赋值操作

a := [3]int{1, 2, 3} // 声明了一个长度为 3 的 int 数组
b := [10]int{1, 2, 3} // 声明了一个长度为 10 的 int 数组,其中前三个元素初始化为 1、2、3,其它默认为 0
c := [...]int{4, 5, 6} // 可以省略长度而采用 `...` 的方式,Go 会自动根据元素个数来计算长度


// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有 4 个 int 类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

提示

由于长度也是数组类型的一部分,因此[3]int 与[4]int 是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。

8. 动态数组 slice

  1. slice 和数组在声明时的区别:声明数组时,方括号内写明了数组的长度或使用 … 自动计算长度,而声明 slice 时,方括号内没有任何字符。
  2. slice 并不是真正意义上的动态数组,而是一个引用类型。slice 总是指向一个底层 array,slice 的声明也可以像 array 一样,只是不需要长度
  3. slice 可以从一个数组或一个已经存在的 slice 中再次声明。
  4. slice 是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值
// 和声明 array 一样,只是少了长度
var fslice []int

// 声明并初始化
slice := []byte {'a', 'b', 'c', 'd'}

从数组中声明:

//slice 可以从一个数组或一个已经存在的 slice 中再次声明

// 声明一个含有 10 个元素元素类型为 byte 的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

// 声明两个含有 byte 的 slice
var a, b []byte

// a 指向数组的第 3 个元素开始,并到第五个元素结束,a = ar[2:5]
// 现在 a 含有的元素: ar[2]、ar[3]和 ar[4]

// b 是数组 ar 的另一个 slice
b = ar[3:5]
// b 的元素是:ar[3]和 ar[4]

从概念上面来说 slice 像一个结构体,这个结构体包含了三个元素:

  • 一个指针,指向数组中 slice 指定的开始位置
  • 长度,即 slice 的长度
  • 最大长度,也就是 slice 开始位置到数组的最后位置的长度
Golang 学习笔记,从入门到精通,持续记录

slice

slice 有一些简便的操作

  • slice 的默认开始位置是 0,ar[:n]等价于 ar[0:n]
  • slice 的第二个序列默认是数组的长度,ar[n:]等价于 ar[n:len(ar)]
  • 如果从一个数组里面直接获取 slice,可以这样 ar[:],因为默认第一个序列是 0,第二个是数组的长度,即等价于 ar[0:len(ar)]

对于 slice 有几个有用的内置函数:

  • len 获取 slice 的长度
  • cap 获取 slice 的最大容量
  • append 向 slice 里面追加一个或者多个元素,然后返回一个和 slice 一样类型的 slice
  • copy 函数 copy 从源 slice 的 src 中复制元素到目标 dst,并且返回复制的元素的个数
在 Go 中,append 函数会向 slice 中添加一个或多个元素,并返回一个新的 slice。如果原始 slice 的容量不足以容纳新的元素,append 函数会自动扩展 slice 的容量。

因此,append 函数实际上是在原始 slice 后面新增数据。在新增数据时,如果原始 slice 的容量足够,append 函数会直接在原始 slice 的末尾添加新的元素,并返回原始 slice。如果原始 slice 的容量不足,append 函数会创建一个新的 slice,将原始 slice 中的所有元素复制到新的 slice 中,并在新的 slice 的末尾添加新的元素。

9.Map

map 的读取和设置也类似 slice 一样,通过 key 来操作,只是 slice 的 index 只能是`int`类型,而 map 多了很多类型,可以是 int,可以是 string 及所有完全定义了 == 与!= 操作的类型。


// 声明一个 key 是字符串,值为 int 的字典, 这种方式的声明需要在使用之前使用 make 初始化
var numbers map[string]int
// 另一种 map 的声明方式
numbers = make(map[string]int)
numbers["one"] = 1  // 赋值
numbers["ten"] = 10 // 赋值
numbers["three"] = 3

fmt.Println("第三个数字是:", numbers["three"]) // 读取数据
// 打印出来如: 第三个数字是: 3
  • map 是无序的,每次打印出来的 map 都会不一样,它不能通过 index 获取,而必须通过 key 获取
  • map 的长度是不固定的,也就是和 slice 一样,也是一种引用类型
  • 内置的 len 函数同样适用于 map,返回 map 拥有的 key 的数量
  • map 的值可以很方便的修改,通过 numbers[“one”]=11 可以很容易的把 key 为 one 的字典值改为 11
  • map 和其他基本型别不同,它不是 thread-safe,在多个 go-routine 存取时,必须使用 mutex lock 机制

map 有两个返回值,第二个返回值,如果不存在 key,那么 ok 为 false,如果存在 ok 为 true,csharpRating, ok := rating["C#"]

map 也是一种引用类型,如果两个 map 同时指向一个底层,那么一个改变,另一个也相应的改变:

m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut"  // 现在 m["hello"]的值已经是 Salut 了

10.make、new 操作

make 用于内建类型(map、slice 和 channel)的内存分配。new 用于各种类型的内存分配。

内建函数 new 本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的 T 类型的内存空间,并且返回其地址,即一个 * T 类型的值。用 Go 的术语说,它返回了一个指针,指向新分配的类型 T 的零值。有一点非常重要:

new 动态创建数组:

// 创建一个长度为 10 的 int 类型数组
arr := new([10]int)

// 设置数组的第一个元素为 1
// arr 是一个指针
(*arr)[0] = 1

// 获取数组的第一个元素
first := (*arr)[0]

make 返回初始化后的(非零)值。

内建函数 make(T, args)与 new(T)有着不同的功能,make 只能创建 slice、map 和 channel,并且返回一个有初始值 (非零) 的 T 类型,而不是 *T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个 slice,是一个包含指向数据(内部 array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice 为 nil。对于 slice、map 和 channel 来说,make 初始化了内部的数据结构,填充适当的值。

11. 错误类型

Go 内置有一个 error 类型,专门用来处理错误信息,Go 的 package 里面还专门有一个包 errors 来处理错误:

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {fmt.Print(err)
}

12. 初始值

13. 派生类型

  • 指针类型(Pointer)
  • 数组类型
  • 结构化类型(struct)
  • Channel 类型
  • 函数类型
  • 切片类型
  • 接口类型(interface)
  • Map 类型

14. 空白标识符_

_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。

package main

import "fmt"

func main() {_,numb,strs := numbers() // 只获取函数返回值的后两个
  fmt.Println(numb,strs)
}

// 一个可以返回多个值的函数
func numbers()(int,int,string){
  a , b , c := 1 , 2 , "str"
  return a,b,c
}

类型转换 

1. 显式类型转换

类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:

type_name(expression)

同样适用于自己定义的结构体和接口类型,但要注意的是,仅能用于将结构体类型转换接口类型,而不能将接口类型转为结构体类型。

2. 隐式类型转换

隐式转换,是编译器所为,在日常开发中,开发者并不会感觉到发生了变化。

流程控制

1.IF 语句

  • Go 里面 if 条件判断语句中不需要括号
  • Go 的 if 条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用
// 计算获取值 x, 然后根据 x 返回的大小,判断是否大于 10。if x := computedValue(); x> 10 {fmt.Println("x is greater than 10")
} else {fmt.Println("x is less than 10")
}

// 这个地方如果这样调用就编译出错了,因为 x 是条件里面的变量
fmt.Println(x)

2.For 语句

Go 里面最强大的一个控制逻辑就是 for,它既可以用来循环读取数据,又可以当作 while 来控制逻辑,还能迭代操作

for expression1; expression2; expression3 {}
for 初始化语句; 条件语句; 修饰语句 {}

expression1、expression2 和 expression3 都是表达式,其中 expression1 和 expression3 是变量声明或者函数调用返回值之类的,expression2 是用来条件判断,expression1 在循环开始之前调用,expression3 在每轮循环结束之时调用。

// For 循环
for i := 1;i<5000;i++ {log.Print(i)
}

// while
for i < 5000 {
    i++
    log.Print(i)
}

for-range 是 Go 特有的一种的迭代结构,可以迭代任何一个集合(包括数组和 map)。语法上类似其它语言中的 foreach 语句,可以获得每次迭代所对应的索引。

//Foreach
for key, val := range arr {log.Print(key, val)
}

平行赋值

i, j = j, i,右边的值,对应赋值给左边的变量

3.switch 语句

Go 的 switch 非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;而如果 switch 没有表达式,它会匹配 true。

i := 10
switch i {
case 1:
	fmt.Println("i is equal to 1")
case 2, 3, 4:
	fmt.Println("i is equal to 2, 3 or 4")
case 10:
	fmt.Println("i is equal to 10")
default:
	fmt.Println("All I know is that i is an integer")
}

Go 里面 switch 默认相当于每个 case 最后带有 break,匹配成功后不会自动向下执行其他 case,而是跳出整个 switch, 但是可以使用 fallthrough 强制执行后面的 case 代码。

4. 标签与 goto 

标签的名称是大小写敏感的,为了提升可读性,一般建议使用全部大写字母

package main

func main() {
	i:=0
	HERE:
		print(i)
		i++
		if i==5 {return}
		goto HERE
}

 5.break 与 continue

用法同其它语言,多层嵌套循环时可搭配标签使用。

函数

1. 函数的语法

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
	// 这里是处理逻辑代码
	// 返回多个值
	return value1, value2
}
  • 关键字 func 用来声明一个函数 funcName
  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过, 分隔
  • 函数可以返回多个值
  • 上面返回值声明了两个变量 output1 和 output2,如果你不想声明也可以,直接就两个类型
  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
  • 如果没有返回值,那么就直接省略最后的返回信息
  • 如果有返回值,那么必须在函数的外层添加 return 语句
  • 同类型的变量或者返回值类型相同时,可省略为一个

2. 可变参数

func myfunc(arg ...int) {}

arg ...int 代表这个函数接受不定数量的参数,在函数体中,变量 arg 是一个 int 的 slice

3. 传值与传指针

  • 传指针使得多个函数能操作同一个对象。
  • 传指针比较轻量级 (8bytes), 只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次 copy 上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go 语言中 channel,slice,map 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变 slice 的长度,则仍需要取地址传递指针)
package main

import "fmt"

// 简单的一个函数,实现了参数 + 1 的操作
func add1(a *int) int { // 请注意,*a = *a+1 // 修改了 a 的值
	return *a // 返回新值
}

func main() {
	x := 3
	fmt.Println("x =", x)  // 应该输出 "x = 3"
	x1 := add1(&x)  // 调用 add1(&x) 传 x 的地址
	fmt.Println("x+1 =", x1) // 应该输出 "x+1 = 4"
	fmt.Println("x =", x)    // 应该输出 "x = 4"
}

4.defer

可以在函数中添加多个延迟(defer)语句,当函数执行到最后时,这些 defer 语句会按照逆序执行,defer 是采用后进先出模式

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源。

// 关闭文件流
defer file.Close()
// 解锁一个加锁的资源
mu.Lock()  
defer mu.Unlock() 
// 打印最终报告
printHeader()  
defer printFooter()
// 关闭数据库链接
defer disconnectFromDB()

在 Go 中函数也是一种变量,我们可以通过 type 来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型

5. 保留函数

Go 里面有两个保留的函数:init 函数(能够应用于所有的 package)和 main 函数(只能应用于 package main)。这两个函数在定义时不能有任何的参数和返回值。虽然一个 package 里面可以写任意多个 init 函数,但这无论是对于可读性还是以后的可维护性来说,建议在一个 package 中每个文件只写一个 init 函数。

Go 程序会自动调用 init()和 main(),所以不需要在任何地方调用这两个函数。每个 package 中的 init 函数都是可选的,但 package main 就必须包含一个 main 函数。

程序的初始化和执行都起始于 main 包。如果 main 包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到 fmt 包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行 init 函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init 函数(如果存在的话),最后执行 main 函数。

Golang 学习笔记,从入门到精通,持续记录

运行过程

5. 内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。

  • close() 用于管道通信
  • len()、cap() len() 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap() 是容量的意思,用于返回某个类型的最大容量(只能用于数组、切片和管道,不能用于 map)
  • new()、make() new() 和 make() 均是用于分配内存:new() 用于值类型和用户定义的类型,如自定义结构,make 用于内置引用类型(切片、map 和管道)。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针(详见第 10.1 节)。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new() 进行更多的工作。new() 是一个函数,不要忘记它的括号。
  • copy()、append() 用于复制和连接切片
  • panic()、recover() 两者均用于错误处理机制
  • print()、println() 底层打印函数,在部署环境中建议使用 fmt 包
  • complex()、real ()、imag() 用于创建和操作复数

6.Panic 和 Recover

Go 没有像 Java 那样的异常机制,它不能抛出异常,而是使用了 panic 和 recover 机制。一定要记住,应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有 panic 的东西

panic() 和 recover() 是用来处理真正的异常(无法预测的错误)而不是普通的错误。

1.1Panic

Panic 是一个内建函数,可以中断原有的控制流程,进入一个 panic 状态中。当函数 F 调用 panic,函数 F 的执行被中断,但是 F 中的延迟函数会正常执行,然后 F 返回到调用它的地方。在调用的地方,F 的行为就像调用了 panic。这一过程继续向上,直到发生 panic 的 goroutine 中所有调用的函数返回,此时程序退出。panic 可以直接调用 panic 产生。也可以由运行时错误产生,例如访问越界的数组。

1.2.Recover

Recover 是一个内建的函数,可以让进入 panic 状态的 goroutine 恢复过来。recover 仅在延迟函数中有效。在正常的执行过程中,调用 recover 会返回 nil,并且没有其它任何效果。如果当前的 goroutine 陷入 panic 状态,调用 recover 可以捕获到 panic 的输入值,并且恢复正常的执行。

func throwsPanic(f func()) (b bool) {defer func() {if x := recover(); x != nil {b = true}
	}()
	f() // 执行函数 f,如果 f 中出现了 panic,那么就可以恢复回来
	return
}

7. 将函数作为参数

函数可以作为其它函数的参数进行传递,然后在其它函数内调用执行,一般称之为回调。

package main

import ("fmt")

func main() {callback(1, Add)
}

func Add(a, b int) {fmt.Printf("The sum of %d and %d is: %dn", a, b, a+b)
}

func callback(y int, f func(int, int)) {f(y, 2) // this becomes Add(1, 2)
}

8. 闭包

当不希望给函数起名字的时候,可以使用匿名函数,例如:func(x, y int) int {return x + y}。

这样的一个函数不能够独立存在(编译器会返回错误:non-declaration statement outside function body),但可以被赋值于某个变量,即保存函数的地址到变量中:fplus := func(x, y int) int {return x + y},然后通过变量名对函数进行调用:fplus(3,4)。

当然,也可以直接对匿名函数进行调用:func(x, y int) int {return x + y} (3, 4)。

func f() {
	for i := 0; i < 4; i++ {g := func(i int) {fmt.Printf("%d", i) }
		g(i)
		fmt.Printf("- g is of type %T and has value %vn", g, g)
	}
}

import 的使用 

//fmt 是 Go 语言的标准库,会从 GOROOT 环境变量指定目录下去加载该模块

import("fmt")

相对路径

import“./model”// 当前文件同一目录的 model 目录,但是不建议这种方式来 import

绝对路径

import“shorturl/model”// 加载 gopath/src/shorturl/model 模块 

点操作

import(. "fmt")

// 点操作的含义就是这个包导入之后在你调用这个包的函数时,可以省略前缀的包名,也就是调用的 fmt.Println("hello world")可以省略的写成 Println("hello world")

别名操作

import(f "fmt")

// 别名操作的话调用包函数时前缀变成了指定的前缀,即 f.Println("hello world")

_操作

import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)

//_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的 init 函数。

错误和异常处理

1. 基础知识

Go 的设计者觉得 try/catch 机制的使用太泛滥了,而且从底层向更高的层级抛异常太耗费资源,Go 语言通过内置的错误接口提供了非常简单的错误处理机制。

error 类型是一个接口类型,这是它的定义:

type error interface {Error() string
}

可以在编码中通过实现 error 接口类型来生成错误信息。

函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:

func Sqrt(f float64) (float64, error) {
    if f < 0 {return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

result, err:= Sqrt(-1)

// 不为 nil,说明发生异常
if err != nil {fmt.Println(err)
}

时间与日期

time 包为我们提供了一个数据类型 time.Time(作为值使用)以及显示和测量时间和日期的功能函数。

相关文档:https://studygolang.com/static/pkgdoc/pkg/time.htm 

字符串操作

作为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数。Go 中使用 strings 包来完成对字符串的主要操作。

1. 常用方法

  • 前缀和后缀,strings.HasPrefix(s, prefix string) bool,strings.HasSuffix(s, suffix string) bool
  • 字符串包含关系,strings.Contains(s, substr string) bool
  • 判断子字符串或字符在父字符串中出现的位置(索引),strings.Index(s, str string) int,strings.LastIndex(s, str string) int
  • 字符串替换,strings.Replace(str, old, new string, n int) string
  • 统计字符串出现次数,strings.Count(s, str string) int
  • 重复字符串,strings.Repeat(s, count int) string
  • 修改字符串大小写,strings.ToLower(s) string,strings.ToUpper(s) string
  • 修剪字符串,strings.TrimSpace(s,[cut])、TrimLeft、TrimRight
  • 分割字符串,strings.Split(s, sep)
  • 拼接 slice 到字符串,strings.Join(sl []string, sep string) string

相关文档:https://studygolang.com/static/pkgdoc/pkg/strings.htm

Go 正则

标准库 regexp 包实现了正则表达式搜索,正则表达式采用 RE2 语法(除了 c、C),和 Perl、Python 等语言的正则基本一致。

regexp 包中含有三个函数用来判断是否匹配,如果匹配返回 true,否则返回 false

func Match(pattern string, b []byte) (matched bool, error error)
func MatchReader(pattern string, r io.RuneReader) (matched bool, error error)
func MatchString(pattern string, s string) (matched bool, error error)

上面的三个函数实现了同一个功能,就是判断 pattern 是否和输入源匹配,匹配的话就返回 true,如果解析正则出错则返回 error。三个函数的输入源分别是 byte slice、RuneReader 和 string。

如果要验证一个输入是不是 IP 地址,那么如何来判断呢?

func IsIP(ip string) (m bool) {m, _ = regexp.MatchString("^[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}$", ip)
	return
}

1. 解析正则

func Compile(expr string) (*Regexp, error)
func CompilePOSIX(expr string) (*Regexp, error)
func MustCompile(str string) *Regexp
func MustCompilePOSIX(str string) *Regexp

CompilePOSIX 和 Compile 的不同点在于 POSIX 必须使用 POSIX 语法,它使用最左最长方式搜索,而 Compile 是采用的则只采用最左方式搜索 (例如[a-z]{2,4} 这样一个正则表达式,应用于 "aa09aaa88aaaa" 这个文本串时,CompilePOSIX 返回了 aaaa,而 Compile 的返回的是 aa)。前缀有 Must 的函数表示,在解析正则语法的时候,如果匹配模式串不满足正确的语法则直接 panic,而不加 Must 的则只是返回错误。

2. 字符串匹配

func (re *Regexp) Match(b []byte) bool
func (re *Regexp) MatchReader(r io.RuneReader) bool
func (re *Regexp) MatchString(s string) bool

Regexp 也定义了三个函数,它们和同名的外部函数功能一模一样,其实外部函数就是调用了这 Regexp 的三个函数来实现的

3. 字符串搜索

用来搜索的函数:

func (re *Regexp) Find(b []byte) []byte
func (re *Regexp) FindAll(b []byte, n int) [][]byte
func (re *Regexp) FindAllIndex(b []byte, n int) [][]int
func (re *Regexp) FindAllSubmatch(b []byte, n int) [][][]byte
func (re *Regexp) FindAllSubmatchIndex(b []byte, n int) [][]int
func (re *Regexp) FindIndex(b []byte) (loc []int)
func (re *Regexp) FindSubmatch(b []byte) [][]byte
func (re *Regexp) FindSubmatchIndex(b []byte) []int

函数的使用:

package main

import (
	"fmt"
	"regexp"
)

func main() {
	a := "I am learning Go language"

	re, _ := regexp.Compile("[a-z]{2,4}")

	// 查找符合正则的第一个
	one := re.Find([]byte(a))
	fmt.Println("Find:", string(one))

	// 查找符合正则的所有 slice,n 小于 0 表示返回全部符合的字符串,不然就是返回指定的长度
	all := re.FindAll([]byte(a), -1)
	fmt.Println("FindAll", all)

	// 查找符合条件的 index 位置, 开始位置和结束位置
	index := re.FindIndex([]byte(a))
	fmt.Println("FindIndex", index)

	// 查找符合条件的所有的 index 位置,n 同上
	allindex := re.FindAllIndex([]byte(a), -1)
	fmt.Println("FindAllIndex", allindex)

	re2, _ := regexp.Compile("am(.*)lang(.*)")

	// 查找 Submatch, 返回数组,第一个元素是匹配的全部元素,第二个元素是第一个 () 里面的,第三个是第二个 () 里面的
	// 下面的输出第一个元素是 "am learning Go language"
	// 第二个元素是 "learning Go",注意包含空格的输出
	// 第三个元素是 "uage"
	submatch := re2.FindSubmatch([]byte(a))
	fmt.Println("FindSubmatch", submatch)
	for _, v := range submatch {fmt.Println(string(v))
	}

	// 定义和上面的 FindIndex 一样
	submatchindex := re2.FindSubmatchIndex([]byte(a))
	fmt.Println(submatchindex)

	//FindAllSubmatch, 查找所有符合条件的子匹配
	submatchall := re2.FindAllSubmatch([]byte(a), -1)
	fmt.Println(submatchall)

	//FindAllSubmatchIndex, 查找所有字匹配的 index
	submatchallindex := re2.FindAllSubmatchIndex([]byte(a), -1)
	fmt.Println(submatchallindex)
}

4. 替换

func (re *Regexp) ReplaceAll(src, repl []byte) []byte
func (re *Regexp) ReplaceAllFunc(src []byte, repl func([]byte) []byte) []byte
func (re *Regexp) ReplaceAllLiteral(src, repl []byte) []byte
func (re *Regexp) ReplaceAllLiteralString(src, repl string) string
func (re *Regexp) ReplaceAllString(src, repl string) string
func (re *Regexp) ReplaceAllStringFunc(src string, repl func(string) string) string

使用案例:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
	"strings"
)

func main() {resp, err := http.Get("http://www.baidu.com")
	if err != nil {fmt.Println("http get error.")
	}
	defer resp.Body.Close()
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {fmt.Println("http read error")
		return
	}

	src := string(body)

	// 将 HTML 标签全转换成小写
	re, _ := regexp.Compile(`<[Ss]+?>`)
	src = re.ReplaceAllStringFunc(src, strings.ToLower)

	// 去除 STYLE
	re, _ = regexp.Compile(``)
	src = re.ReplaceAllString(src, "")
	// 去除 HTMLUnscape 的 STYLE
	re, _ = regexp.Compile(``)
	src = re.ReplaceAllString(src, "")

	// 去除 SCRIPT
	re, _ = regexp.Compile(``)
	src = re.ReplaceAllString(src, "")
	// 去除 HTMLUnsapce 的 SCRIPT
	re, _ = regexp.Compile(``)
	src = re.ReplaceAllString(src, "")

	// 去除所有尖括号内的 HTML 代码,并换成换行符
	re, _ = regexp.Compile(`<[Ss]+?>`)
	src = re.ReplaceAllString(src, "n")

	// 去除连续的换行符
	re, _ = regexp.Compile(`s{2,}`)
	src = re.ReplaceAllString(src, "n")

	fmt.Println(strings.TrimSpace(src))
}

相关文档:https://studygolang.com/pkgdoc

Go Json

1. 基础用法

Go 语言的 json 包用于读取和写入 JSON 数据。

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    Name string
    Age  int
}

func main() {jsonData := `{"Name": "John", "Age": 30}`
    var person Person

    /* json 解码 */
    err := json.Unmarshal([]byte(jsonData), &person)

    /* json 编码,返回的是字节数组 */
    res, err := json.Marshal(record)
    if err != nil {fmt.Println("Error decoding JSON:", err)
        return
    }
    fmt.Println(person.Name, person.Age)
}

提示

Go 语言中,对 struct 进行 json 序列化时,内部属性名称必须定义为公有,非公有属性将不会被序列化

2.json 提取 struct 类型声明

在 Golang 的结构体定义中添加 omitempty 关键字,来表示这条信息如果没有提供,在序列化成 json 的时候就不要包含其默认值。

// 字段在 JSON 中显示为键“myName”。Field int `json:"myName"` 

// 字段在 JSON 中显示为键“myName”,并且
// 如果其值为空,则从对象中省略该字段,// 如上定义。Field int `json:"myName,omitempty"` 

// 字段在 JSON 中显示为键“Field”(默认值),但
// 如果该字段为空,则跳过该字段。// 注意前导逗号。Field int `json:",omitempty"` 

// 字段被这个包忽略。Field int `json:"-"` 

// 字段在 JSON 中显示为键“-”。Field  `json:"-,"`

相关软件包:https://github.com/mholt/json-to-go/blob/master/json-to-go.js

struct 类型

1. 介绍

Go 语言中,也和 C 或者其他语言一样,我们可以声明新的类型,作为其它类型的属性或字段的容器。

type person struct {
	name string
	age int
}

var P person  // P 现在就是 person 类型的变量了

P.name = "Astaxie"  // 赋值 "Astaxie" 给 P 的 name 属性.
P.age = 25  // 赋值 "25" 给变量 P 的 age 属性
fmt.Printf("The person's name is %s", P.name)  // 访问 P 的 name 属性.

// 按照顺序提供初始化值
P := person{"Tom", 25}

// 通过 field:value 的方式初始化,这样可以任意顺序 
P := person{age:24, name:"Tom"} 

// 当然也可以通过 new 函数分配一个指针,此处 P 的类型为 
*person P := new(person)

2.struct 的匿名字段

Go 支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。

  • 当匿名字段是一个 struct 的时候,那么这个 struct 所拥有的全部字段都被隐式地引入了当前定义的这个 struct,能够实现字段的继承。
  • 可以是任意类型,使用基础类型时,通过 struct_name. 类型名进行访问
  • struct.name 和 struct.student.name,引用的是同一块内存地址
  • 被重载的属性,跟 java 等语言的面向对象是一样的,需要访问被重载的属性时,指定对应的字段名进行访问即可
Golang 学习笔记,从入门到精通,持续记录

匿名类型

3. 自定义类型

// 实际上只是一个定义了一个别名, 有点类似于 c 中的 typedef
type ages int

type money float32

type months map[string]int

m := months {
	"January":31,
	"February":28,
	...
	"December":31,
}

3. 带 Tag 的结构体

构体中的字段除了有名字和类型外,还可以有一个可选的标签 (tag):它是一个附属于字段的字符串,可以是文档或其他的重要标记。标签的内容不可以在一般的编程中使用,只有包 reflect 能获取它。

type FruitBasket struct {
    Name    string    `json:"name"`
    Fruit   []string  `json:"fruit"`
    Id      int64     `json:"id"`
    Created time.Time `json:"created"`
}

提示

使用另一个包内的类型,可以直接通过“包名. 类型”来调用

面向对象

1. 方法

在 Go 中带有接收者的函数,称为 method。method 是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在 func 后面增加了一个 receiver(也就是 method 所依从的主体)。

//Receiver 是以值传递,而非引用传递,是的,Receiver 还可以是指针, 两者的差别在于
// 指针作为 Receiver 会对实例对象的内容发生操作, 而普通类型作为 Receiver 仅仅是以副本
// 作为操作对象, 并不对原实例对象发生操作。func (r ReceiverType) funcName(parameters) (results)
  • 虽然 method 的名字一模一样,但是如果接收者不一样,那么 method 就不一样
  • method 里面可以访问接收者的字段
  • 调用 method 通过. 访问,就像 struct 里面访问字段一样
type Box struct {
	width, height, depth float64
	color Color
}

type BoxList []Box //a slice of boxes

func (b Box) Volume() float64 {return b.width * b.height * b.depth}

func (b *Box) SetColor(c Color) {b.color = c}

boxes := BoxList {Box{4, 4, 4, RED},
		Box{10, 10, 1, YELLOW},
		Box{1, 1, 20, BLACK},
		Box{10, 10, 1, BLUE},
		Box{10, 30, 1, WHITE},
		Box{20, 20, 20, YELLOW},
	}

boxes.PaintItBlack()

2. 指针作为 receiver

如果不使用引用,传递的就只是一个值拷贝,进行修改时不能改变原值的,而指针可以:

  • 指针方法可以通过指针调用
  • 值方法可以通过值调用
  • 接收者是值的方法可以通过指针调用,因为指针会首先被解引用
  • 接收者是指针的方法不可以通过值调用,因为存储在接口中的值没有地址

Go 语言规范定义了接口方法集的调用规则:

  • 类型 *T 的可调用方法集包含接受者为 *T 或 T 的所有方法集
  • 类型 T 的可调用方法集包含接受者为 T 的所有方法
  • 类型 T 的可调用方法集不包含接受者为 *T 的方法

3.method 继承

如果匿名字段实现了一个 method,那么包含这个匿名字段的 struct 也能调用该 method。

4.method 重写

匿名字段冲突一样的道理,可以在 struct 上面定义一个 method,重写匿名字段的方法。

5.interface 接口

接口提供了一种方式来 说明对象的行为:如果谁能搞定这件事,它就可以用在这儿。接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

type Namer interface {Method1(param_list) return_type
    Method2(param_list) return_type
    ...
}

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

type ReadWrite interface {Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {Lock()
    Unlock()}

type File interface {
    ReadWrite
    Lock
    Close()}

接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

type Any interface {}

任何其他类型都实现了空接口(它不仅仅像 Java/C# 中 Object 引用类型),any 或 Any 是空接口一个很好的别名或缩写。

空接口类似 Java/C# 中所有类的基类:Object 类,二者的目标也很相近。

可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

提示

一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误

5.1 类型断言:如何检测和转换接口变量的类型

类型断言用来测试在某个时刻变量是否包含类型 T 的值:

v := varI.(T)       // unchecked type assertion

varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)。

类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如果转换在程序运行时失败会导致错误发生。更安全的方式是使用以下形式来进行类型断言:

if _, ok := varI.(T); ok {// ...}

5.2 类型判断:type-switch

可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough。

如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句

switch t := areaIntf.(type) {//switch areaIntf.(type) {
case *Square:
	fmt.Printf("Type Square %T with value %vn", t, t)
case *Circle:
	fmt.Printf("Type Circle %T with value %vn", t, t)
case nil:
	fmt.Printf("nil value: nothing to check?n")
default:
	fmt.Printf("Unexpected type %Tn", t)
}

6. 反射

反射是用程序检查其所拥有的结构,尤其是类型的一种能力;这是元编程的一种形式。反射可以在运行时检查类型和变量,例如:它的大小、它的方法以及它能“动态地”调用这些方法。这对于没有源代码的包尤其有用。这是一个强大的工具,除非真得有必要,否则应当避免使用或小心使用。

变量的最基本信息就是类型和值:反射包的 Type 用来表示一个 Go 类型,反射包的 Value 为 Go 值提供了反射接口。

两个简单的函数,reflect.TypeOf 和 reflect.ValueOf,返回被检查对象的类型和值。例如,x 被定义为:var x float64 = 3.4,那么 reflect.TypeOf(x) 返回 float64,reflect.ValueOf(x) 返回

实际上,反射是通过检查一个接口的值,变量首先被转换成空接口。这从下面两个函数签名能够很明显的看出来:

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

reflect.Type 和 reflect.Value 都有许多方法用于检查和操作它们。

// blog: Laws of Reflection
package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4
	fmt.Println("type:", reflect.TypeOf(x))
	v := reflect.ValueOf(x)
	fmt.Println("value:", v)
	fmt.Println("type:", v.Type())
	fmt.Println("kind:", v.Kind())
	fmt.Println("value:", v.Float())
	fmt.Println(v.Interface())
	fmt.Printf("value is %5.2en", v.Interface())
	y := v.Interface().(float64)
	fmt.Println(y)
}

7. 动态方法调用

像 Python,Ruby 这类语言,动态类型是延迟绑定的(在运行时进行):方法只是用参数和变量简单地调用,然后在运行时才解析。

Go 的实现与此相反,通常需要编译器静态检查的支持:当变量被赋值给一个接口类型的变量时,编译器会检查其是否实现了该接口的所有函数。如果方法调用作用于像 interface{} 这样的“泛型”上,可以通过类型断言来检查变量是否实现了相应接口。

函数重载

根据方法参数数量和类型的不同,调用多个同名不同实现的方法

8.Go 面向对象

OO 语言最重要的三个方面分别是:封装、继承和多态,在 Go 中它们是怎样表现的呢?

封装(数据隐藏):和别的 OO 语言有 4 个或更多的访问层次相比,Go 把它简化为了 2 层:

  • 包范围内的:通过标识符首字母小写,对象只在它所在的包内可见
  • 可导出的:通过标识符首字母大写,对象对所在包以外也可见

类型只拥有自己所在包中定义的方法。

  • 继承:用组合实现:内嵌一个(或多个)包含想要的行为(字段和方法)的类型;多重继承可以通过内嵌多个类型实现
  • 多态:用接口实现:某个类型的实例可以赋给它所实现的任意接口类型的变量。类型和接口是松耦合的,并且多重继承可以通过实现多个接口实现。Go 接口不是 Java 和 C# 接口的变体,而且接口间是不相关的,并且是大规模编程和可适应的演进型设计的关键。

GO IO

1. 读取用户的输入

从键盘和标准输入 os.Stdin 读取输入,最简单的办法是使用 fmt 包提供的 Scan... 和 Sscan... 开头的函数。

fmt.Println("Please enter your full name:")
fmt.Scanln(&firstName, &lastName)
// fmt.Scanf("%s %s", &firstName, &lastName)
fmt.Printf("Hi %s %s!n", firstName, lastName) // Hi Chris Naegels
fmt.Sscanf(input, format, &f, &i, &s)
fmt.Println("From the string we read:", f, i, s)

Scanln() 扫描来自标准输入的文本,将空格分隔的值依次存放到后续的参数内,直到碰到换行。Scanf() 与其类似,除了 Scanf() 的第一个参数用作格式字符串,用来决定如何读取

package main
import (
    "fmt"
    "bufio"
    "os"
)

var inputReader *bufio.Reader
var input string
var err error

func main() {inputReader = bufio.NewReader(os.Stdin)
    fmt.Println("Please enter some input:")
    input, err = inputReader.ReadString('n')
    if err == nil {fmt.Printf("The input was: %sn", input)
    }
}

inputReader 是一个指向 bufio.Reader 的指针。inputReader := bufio.NewReader(os.Stdin) 这行代码,将会创建一个读取器,并将其与标准输入绑定。

bufio.NewReader() 构造函数的签名为:func NewReader(rd io.Reader) *Reader

该函数的实参可以是满足 io.Reader 接口的任意对象(任意包含有适当的 Read() 方法的对象,请参考章节 11.8),函数返回一个新的带缓冲的 io.Reader 对象,它将从指定读取器(例如 os.Stdin)读取内容。

返回的读取器对象提供一个方法 ReadString(delim byte),该方法从输入中读取内容,直到碰到 delim 指定的字符,然后将读取到的内容连同 delim 字符一起放到缓冲区。

ReadString 返回读取到的字符串,如果碰到错误则返回 nil。如果它一直读到文件结束,则返回读取到的字符串和 io.EOF。如果读取过程中没有碰到 delim 字符,将返回错误 err != nil。

2. 文件读写

Go 语言中,文件使用指向 os.File 类型的指针来表示的,也叫做文件句柄。

func main() {inputFile, inputError := os.Open("input.dat")
    if inputError != nil {
        fmt.Printf("An error occurred on opening the inputfilen" +
            "Does the file exist?n" +
            "Have you got access to it?n")
        return // exit the function on error
    }
    defer inputFile.Close()

    inputReader := bufio.NewReader(inputFile)
    for {inputString, readerError := inputReader.ReadString('n')
        fmt.Printf("The input was: %s", inputString)
        if readerError == io.EOF {return}      
    }
}

相关文档:https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/12.2.md

3. 从命令行读取参数

os 包中有一个 string 类型的切片变量 os.Args,用来处理一些基本的命令行参数,它在程序启动后读取命令行输入的参数。

// os_args.go
package main

import (
	"fmt"
	"os"
	"strings"
)

func main() {
	who := "Alice"
	if len(os.Args) > 1 {who += strings.Join(os.Args[1:], " ")
	}
	fmt.Println("Good Morning", who)
}

flag 包有一个扩展功能用来解析命令行选项。但是通常被用来替换基本常量

package main

import (
	"flag" // command line option parser
	"os"
)

var NewLine = flag.Bool("n", false, "print newline") // echo -n flag, of type *bool

const (
	Space   = " "
	Newline = "n"
)

func main() {flag.PrintDefaults()
	flag.Parse() // Scans the arg list and sets up flags
	var s string = ""
	for i := 0; i  0 {
			s += " "
			if *NewLine { // -n is parsed, flag becomes true
				s += Newline
			}
		}
		s += flag.Arg(i)
	}
	os.Stdout.WriteString(s)
}

4. 目录操作

文件操作的大多数函数都是在 os 包里面,列举几个目录操作的:

  • func Mkdir(name string, perm FileMode) error,创建名称为 name 的目录,权限设置是 perm,例如 0777
  • func MkdirAll(path string, perm FileMode) error,根据 path 创建多级子目录,例如 astaxie/test1/test2。
  • func Remove(name string) error,删除名称为 name 的目录,当目录下有文件或者其他目录时会出错
  • func RemoveAll(path string) error,根据 path 删除多级子目录,如果 path 是单个名称,那么该目录下的子目录全部删除。

建立与打开文件:

  • func Create(name string) (file *File, err Error),根据提供的文件名创建新的文件,返回一个文件对象,默认权限是 0666 的文件,返回的文件对象是可读写的。
  • func NewFile(fd uintptr, name string) *File,根据文件描述符创建相应的文件,返回一个文件对象

通过如下两个方法来打开文件:

  • func Open(name string) (file *File, err Error),该方法打开一个名称为 name 的文件,但是是只读方式,内部实现其实调用了 OpenFile。
  • func OpenFile(name string, flag int, perm uint32) (file *File, err Error),打开名称为 name 的文件,flag 是打开的方式,只读、读写等,perm 是权限

写文件:

  • func (file *File) Write(b []byte) (n int, err Error),写入 byte 类型的信息到文件
  • func (file *File) WriteAt(b []byte, off int64) (n int, err Error),在指定位置开始写入 byte 类型的信息
  • func (file *File) WriteString(s string) (ret int, err Error),写入 string 信息到文件

读文件:

  • func (file *File) Read(b []byte) (n int, err Error),读取数据到 b 中
  • func (file *File) ReadAt(b []byte, off int64) (n int, err Error),从 off 开始读取数据到 b 中

删除文件:

  • func Remove(name string) Error,调用该函数就可以删除文件名为 name 的文件和文件夹

获取文件目录列表:

  • os.ReadDir
  • ioutil.ReadDir
  • filepath.Walk

5. 扩展

判断文件是否存在

func PathExists(path string) (bool, error) {_, err := os.Stat(path)
	if err == nil {return true, nil}
	if os.IsNotExist(err) {return false, nil}
	return false, err
}
  • os.Executable(),返回可执行文件所在的绝对路径
  • os.Stat(),Stat 返回一个描述 name 指定的文件对象的 FileInfo。如果指定的文件对象是一个符号链接,返回的 FileInfo 描述该符号链接指向的文件的信息,本函数会尝试跳转该链接。如果出错,返回的错误值为 *PathError 类型。
  • os.IsExist(),返回一个布尔值说明该错误是否表示一个文件或目录已经存在。ErrExist 和一些系统调用错误会使它返回真。

 GO 协程

1. 基础知识

协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。

有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。

所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS!

runtime.GOMAXPROCS(*numCores)

当 main() 函数返回的时候,程序退出:它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。

另外,协程是独立的处理单元,一旦陆续启动一些协程,无法确定他们是什么时候真正开始执行的。代码逻辑必须独立于协程调用的顺序。

2. 其他知识

在其他语言中,比如 C#,Lua 或者 Python 都有协程的概念。这个名字表明它和 Go 协程有些相似,不过有两点不同:

  • Go 协程意味着并行(或者可以以并行的方式部署),协程一般来说不是这样的
  • Go 协程通过通道来通信;协程通过让出和恢复操作来通信
  • Go 协程比协程更强大,也很容易从协程的逻辑复用到 Go 协程。

3. 协程间的信道

Go 有一种特殊的类型,通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。数据的所有权(可以读写数据的能力)也因此被传递。

// 未初始化的通道的值是 nil。var identifier chan datatype
// 声明
var ch1 chan string
// 或者
ch1 = make(chan string)
// 通道的通道
chanOfChans := make(chan chan int)。

所以通道只能传输一种类型的数据,比如 chan int 或者 chan string,所有的类型都可以用于通道,空接口 interface{} 也可以,甚至可以(有时非常有用)创建通道的通道。

通道实际上是类型化消息的队列:使数据得以传输。它是先进先出(FIFO) 的结构所以可以保证发送给他们的元素的顺序(通道可以比作 Unix shells 中的双向管道 (two-way pipe))。通道也是引用类型,所以可以使用 make() 函数来给它分配内存。

流向通道(发送)

ch <- int1 表示:用通道 ch 发送变量 int1(双目运算符,中缀 = 发送)

从通道流出(接收),三种方式:

int2 = <- ch 表示:变量 int2 从通道 ch(一元运算的前缀操作符,前缀 = 接收)接收数据(获取新值);假设 int2 已经声明过了,如果没有的话可以写成:int2 := <- ch。

<- ch 可以单独调用获取通道的(下一个)值,当前值会被丢弃,但是可以用来验证,所以以下代码是合法的:

if <- ch != 1000{...}

默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送 / 接收操作在对方准备好之前是阻塞的:

  • 对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果 ch 中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。
  • 对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

4. 带缓冲的通道

buf := 100
ch1 := make(chan string, buf)

buf 是通道可以同时容纳的元素(这里是 string)个数

在缓冲满载(缓冲被全部使用)之前,给一个带缓冲的通道发送数据是不会阻塞的,而从通道读取数据也不会阻塞,直到缓冲空了。

缓冲容量和类型无关,所以可以(尽管可能导致危险)给一些通道设置不同的容量,只要他们拥有同样的元素类型。内置的 cap() 函数可以返回缓冲区的容量。

如果容量大于 0,通道就是异步的了:缓冲满载(发送)或变空(接收)之前通信不会阻塞,元素会按照发送的顺序被接收。如果容量是 0 或者未设置,通信仅在收发双方准备好的情况下才可以成功。

同步:ch :=make(chan type, value)

  • value == 0 -> synchronous, unbuffered(阻塞)
  • value > 0 -> asynchronous, buffered(非阻塞)取决于 value 元素

若使用通道的缓冲,你的程序会在“请求”激增的时候表现更好:更具弹性,专业术语叫:更具有伸缩性(scalable)。在设计算法时首先考虑使用无缓冲通道,只在不确定的情况下使用缓冲。

可以通过 range,像操作 slice 或者 map 一样操作缓存类型的 channel

5.Go Select 语句

  • select 是 Go 中的一个控制结构,类似于 switch 语句。
  • select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
  • select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
  • 如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
  • default 语句是可选的;fallthrough 行为,和普通的 switch 相似,是不允许的。在任何一个 case 中执行 break 或者 return,select 就结束了。
  • 如果都阻塞了,会等待直到其中一个可以处理
  • 如果多个可以处理,随机选择一个
  • 如果没有通道操作可以处理并且写了 default 语句,它就会执行:default 永远是可运行的(这就是准备好了,可以执行)。在 select 中使用发送操作并且有 default 可以确保发送不被阻塞!如果没有 default,select 就会一直阻塞。
  • select 语句实现了一种监听模式,通常用在(无限)循环中;在某种情况下,通过 break 语句使循环退出。
// 使用 select 语句非阻塞地从两个通道中获取数据
  for {
    select {
    case msg1 := <-ch1:
      fmt.Println(msg1)
    case msg2 := <-ch2:
      fmt.Println(msg2)
    default:
      // 如果两个通道都没有可用的数据,则执行这里的语句
      fmt.Println("no message received")
    }
  }
}

6. 信号量同步

为了知道计算何时完成,可以通过信道回报。

ch := make(chan int)
go sum(bigArray, ch) // bigArray puts the calculated sum on ch
// .. do something else for a while
sum := <- ch // wait for, and retrieve the sum

使用通道来达到同步的目的,这个很有效的用法在传统计算机中称为信号量 (semaphore)。或者换个方式:通过通道发送信号告知处理已经完成(在协程中)。

在其他协程运行时让 main 程序无限阻塞的通常做法是在 main() 函数的最后放置一个 select {}。

也可以使用通道让 main 程序等待协程完成,就是所谓的信号量模式

7. 实现并行的 for 循环

for i, v := range data {go func (i int, v float64) {doSomething(i, v)
		...
	} (i, v)
}

8. 通道的方向

通道类型可以用注解来表示它只发送或者只接收:

var send_only chan<- int 		// channel can only receive data
var recv_only <-chan int		// channel can only send data

只接收的通道 (<-chan T) 无法关闭,因为关闭通道是发送者用来表示不再给通道发送值了,所以对只接收通道是没有意义的。通道创建的时候都是双向的,但也可以分配给有方向的通道变量

9. 关闭通道

通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。

函数 close(ch) 将通道标记为无法通过发送操作 <- 接受更多的值;给已经关闭的通道发送或者再次关闭都会导致运行时的 panic()。

ch := make(chan float64)
defer close(ch)

使用逗号 ok 模式用来检测通道是否被关闭。

v, ok := <-ch   // ok is true if v received value

if v, ok := <-ch; ok {process(v)
}

记住应该在生产者的地方关闭 channel,而不是消费的地方去关闭它,这样容易引起 panic

10. 通道、超时和计时器

time 包中有一些有趣的功能可以和通道组合使用。time.Ticker 结构体:

type Ticker struct {
    C <-chan Time // the channel on which the ticks are delivered.
    // contains filtered or unexported fields
    ...
}

按照指定的时间,周期性的向通道写入数据:

import "time"

rate_per_sec := 10
var dur Duration = 1e9 / rate_per_sec
chRate := time.Tick(dur) // a tick every 1/10th of a second
for req := range requests {
    <- chRate // rate limit our Service.Method RPC calls
    go client.Call("Service.Method", req, ...)
}

定时器 (Timer) 结构体看上去和计时器 (Ticker) 结构体的确很像(构造为 NewTimer(d Duration)),但是它只发送一次时间,在 Dration d 之后。

func After(d Duration) <-chan Time

给通道读取指定超时时间:

func main() {c := make(chan int)
	o := make(chan bool)
	go func() {
		for {
			select {
				case v := <- c:
					println(v)
				case <- time.After(5 * time.Second):
					println("timeout")
					o <- true
					break
			}
		}
	}()
	<- o}

提示

实现的功能就类似 JS 的 setTimeout 和 setInterval

11.runtime,协程相关方法

runtime 包中有几个处理 goroutine 的函数:

  • Goexit,退出当前执行的 goroutine,但是 defer 函数还会继续调用
  • Gosched,让出当前 goroutine 的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
  • NumCPU,返回 CPU 核数量
  • NumGoroutine,返回正在执行和排队的任务总数
  • GOMAXPROCS,用来设置可以并行计算的 CPU 核数的最大值,并返回之前的值。

12. 锁和 sync 包

1. 基础知识

通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)

经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。

在 Go 语言中这种锁的机制是通过 sync 包中 Mutex 来实现的。sync 来源于 "synchronized" 一词,这意味着线程将有序的对同一变量进行访问。

sync.Mutex 是一个互斥锁,它的作用是守护在临界区入口来确保同一时间只能有一个线程进入临界区。

import  "sync"

type Info struct {
	mu sync.Mutex
	// ... other fields, e.g.: Str string
}

如果一个函数想要改变这个变量可以这样写:

func Update(info *Info) {info.mu.Lock()
    // critical section:
    info.Str = // new value
    // end critical section
    info.mu.Unlock()}

在 sync 包中还有一个 RWMutex 锁:它能通过 RLock() 来允许同一时间多个线程对变量进行读操作,但是只能一个线程进行写操作。如果使用 Lock() 将和普通的 Mutex 作用相同。包中还有一个方便的 Once 类型变量的方法 once.Do(call),这个方法确保被调用函数只能被调用一次。

2.WaitGroup

WaitGroup 是 Go 内置的 sync 包解决任务编排的并发原语。WaitGroup 直译是“等待组”,翻译成大白话就是等待一组协程完成任务。如果没有完成,就阻塞。

举个例子,我们要计算 100 万个数的和,并对这个和求根号。常规的思路肯定是先一个 for 循环计算总和,再开根号,但是这样效率很低。我们可以起 1000 个协程,每个协程计算 1000 个数的和,然后再对这些和求和,最后再开个根号。

这里有一个问题,计算根号的时候,需要等所有并发的协程都计算完才行,WaitGroup 就是解决等所有并发协程完成计算的问题的。

WaitGroup 的用法很简单。标准库中的 WaitGroup 只有三个方法:

func (wg *WaitGroup) Add(delta int)
func (wg *WaitGroup) Done()
func (wg *WaitGroup) Wait()
  • Add:用来设置 WaitGroup 的计数值,delta 可正可负。
  • Done:用来将 WaitGroup 的计数值减一,其实就是调用了 Add(-1)。
  • Wait:阻塞等待,直到 WaitGroup 的计数值变成 0,进入下一步。

使用 WaitGroup 的常规套路如下:

  • 声明 WaitGroup 变量
  • 执行 Add 方法。协程组的个数有 n 个,执行 Add(n)
  • 协程组中,每个协程最后,执行方法 Done

相关文档:https://zhuanlan.zhihu.com/p/350580031 

13. 协程同步

使用通道进行同步:使用一个通道接受需要处理的任务,一个通道接受处理完成的任务(及其结果)。worker 在协程中启动,其数量 N 应该根据任务数量进行调整。

 func main() {pending, done := make(chan *Task), make(chan *Task)
        go sendWork(pending)       // put tasks with work on the channel
        for i := 0; i < N; i++ {   // start N goroutines to do work
            go Worker(pending, done)
        }
        consumeWork(done)          // continue with the processed tasks
}

使用锁的情景:

  • 访问共享数据结构中的缓存信息
  • 保存应用程序上下文和状态信息数据

使用通道的情景:

  • 与异步操作的结果进行交互
  • 分发任务
  • 传递数据所有权

Socket 网络编程

Socket 起源于 Unix,而 Unix 基本哲学之一就是“一切皆文件”,都可以用“打开 open –> 读写 write/read –> 关闭 close”模式来操作。Socket 就是该模式的一个实现,网络的 Socket 数据传输是一种特殊的 I /O,Socket 也是一种文件描述符。Socket 也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的 Socket 描述符,随后的连接建立、数据传输等操作都是通过该 Socket 实现的。

常用的 Socket 类型有两种:流式 Socket(SOCK_STREAM)和数据报式 Socket(SOCK_DGRAM)。流式是一种面向连接的 Socket,针对于面向连接的 TCP 服务应用;数据报式 Socket 是一种无连接的 Socket,对应于无连接的 UDP 服务应用。

Go 的 net 包中定义了很多类型、函数和方法用来网络编程:

  • Dial(network, address string) (Conn, error),用于与指定网络地址建立连接,支持多种网络协议,例如 TCP、UDP、Unix 域套接字等。它接受两个参数,第一个参数是网络类型(例如 tcp、udp、unix 等),第二个参数是要连接的地址(例如 IP 地址和端口号、Unix 域套接字文件路径等)。如果连接成功,该方法将返回一个连接对象(Conn)。
  • Listen(network, address string) (Listener, error),用于监听指定网络地址,等待客户端连接,支持多种网络协议。它接受两个参数,第一个参数是网络类型(例如 tcp、udp、unix 等),第二个参数是要监听的地址(例如 IP 地址和端口号、Unix 域套接字文件路径等)。如果监听成功,该方法将返回一个监听器对象(Listener)。
  • Accept() (Conn, error),用于接受客户端连接,返回一个连接对象(Conn)。该方法通常在一个无限循环中调用,以等待客户端连接。当客户端连接成功后,该方法将返回一个连接对象,可以通过该对象进行数据的读写操作。
  • Read(b []byte) (n int, err error),用于从连接对象中读取数据,可以设置读取超时时间。它接受一个字节数组作为参数,将读取到的数据存储在该数组中,并返回读取到的字节数和可能出现的错误。
  • Write(b []byte) (n int, err error),用于向连接对象中写入数据,可以设置写入超时时间。它接受一个字节数组作为参数,将该数组中的数据写入到连接对象中,并返回写入的字节数和可能出现的错误。
  • Close() error,用于关闭连接对象。它将会释放连接占用的资源,并关闭连接。该方法通常在数据传输完成后调用,以释放连接占用的资源。

1.TCP Socket

在 Go 语言的 net 包中有一个类型 TCPConn,这个类型可以用来作为客户端和服务器端交互的通道,他有两个主要的函数:

func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)

TCPConn 可以用在客户端和服务器端来读写数据。

还有我们需要知道一个 TCPAddr 类型,他表示一个 TCP 的地址信息,他的定义如下:

type TCPAddr struct {
	IP IP
	Port int
	Zone string // IPv6 scoped addressing zone
}

在 Go 语言中通过 ResolveTCPAddr 获取一个 TCPAddr

func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net 参数是 "tcp4"、"tcp6"、"tcp" 中的任意一个,分别表示 TCP(IPv4-only), TCP(IPv6-only)或者 TCP(IPv4, IPv6 的任意一个)。
  • addr 表示域名或者 IP 地址,例如 "www.google.com:80" 或者 "127.0.0.1:22"。

2.TCP Client

Go 语言中通过 net 包中的 DialTCP 函数来建立一个 TCP 连接,并返回一个 TCPConn 类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的 TCPConn 对象来进行数据交换。一般而言,客户端通过 TCPConn 对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下:

func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • network 参数是 "tcp4"、"tcp6"、"tcp" 中的任意一个,分别表示 TCP(IPv4-only)、TCP(IPv6-only)或者 TCP(IPv4,IPv6 的任意一个)
  • laddr 表示本机地址,一般设置为 nil
  • raddr 表示远程的服务地址
package main

import (
	"fmt"
	"io/ioutil"
	"net"
	"os"
)

func main() {if len(os.Args) != 2 {fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
		os.Exit(1)
	}
	service := os.Args[1]
	tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
	checkError(err)
	conn, err := net.DialTCP("tcp", nil, tcpAddr)
	checkError(err)
	_, err = conn.Write([]byte("HEAD / HTTP/1.0rnrn"))
	checkError(err)
	// result, err := ioutil.ReadAll(conn)
	result := make([]byte, 256)
	_, err = conn.Read(result)
	checkError(err)
	fmt.Println(string(result))
	os.Exit(0)
}
func checkError(err error) {
	if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
		os.Exit(1)
	}
}

3.TCP server

可以通过 net 包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。

func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
func (l *TCPListener) Accept() (Conn, error)

当有新的客户端请求到达并同意接受 Accept 该请求的时候他会反馈当前的时间信息。值得注意的是,在代码中 for 循环里,当有错误发生时,直接 continue 而不是退出,是因为在服务器端跑代码的时候,当有错误发生的情况下最好是由服务端记录错误,然后当前连接的客户端直接报错而退出,从而不会影响到当前服务端运行的整个服务。

package main

import (
	"fmt"
	"net"
	"os"
	"time"
)

func main() {
	service := ":1200"
	tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
	checkError(err)
	listener, err := net.ListenTCP("tcp", tcpAddr)
	checkError(err)
	for {conn, err := listener.Accept()
		if err != nil {continue}
		go handleClient(conn)
	}
}

func handleClient(conn net.Conn) {defer conn.Close()
	daytime := time.Now().String()
	conn.Write([]byte(daytime)) // don't care about return value
	// we're finished with this client
}
func checkError(err error) {
	if err != nil {fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
		os.Exit(1)
	}
}

4. 控制 TCP 连接

设置建立连接的超时时间,客户端和服务器端都适用,当超过设置时间时,连接自动关闭。

func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)

用来设置写入 / 读取一个连接的超时时间。当超过设置时间时,连接自动关闭。

func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error

设置 keepAlive 属性。操作系统层在 tcp 上没有数据和 ACK 的时候,会间隔性的发送 keepalive 包,操作系统可以通过该包来判断一个 tcp 连接是否已经断开,在 windows 上默认 2 个小时没有收到数据和 keepalive 包的时候认为 tcp 连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。

func (c *TCPConn) SetKeepAlive(keepalive bool) os.Error

5.UDP Socket

o 语言包中处理 UDP Socket 和 TCP Socket 不同的地方就是在服务器端处理多个客户端请求数据包的方式不同,UDP 缺少了对客户端连接请求的 Accept 函数。其他基本几乎一模一样,只有 TCP 换成了 UDP 而已。

func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

6.proxy 包

Go 语言中的 proxy 包提供了一些用于代理网络连接的方法和工具函数。

  • FromEnvironment() (*URL, error),用于从环境变量中获取代理配置信息。如果环境变量中存在代理配置信息,则该方法将返回一个代理 URL 对象(URL),否则将返回 nil。
  • FromURL(url *URL, options ...DialerOption) Dialer,用于根据指定的代理 URL 对象创建一个 Dialer 对象。Dialer 对象可以用于建立代理连接。该方法接受一个代理 URL 对象作为参数,以及一些可选的 Dialer 选项。如果创建成功,该方法将返回一个 Dialer 对象。
  • HTTPFromEnvironment() (*httpproxy.URL, error),用于从环境变量中获取 HTTP 代理配置信息。如果环境变量中存在 HTTP 代理配置信息,则该方法将返回一个 httpproxy.URL 对象,否则将返回 nil。
  • HTTPFromURL(url *url.URL, options ...http.RoundTripper) http.RoundTripper,用于根据指定的 HTTP 代理 URL 对象创建一个 RoundTripper 对象。RoundTripper 对象可以用于发起 HTTP 请求。该方法接受一个 HTTP 代理 URL 对象作为参数,以及一些可选的 RoundTripper 选项。如果创建成功,该方法将返回一个 RoundTripper 对象。
  • SOCKS5FromEnvironment(auth socks5.Authenticator) (*socks5.Dialer, error),用于从环境变量中获取 SOCKS5 代理配置信息。如果环境变量中存在 SOCKS5 代理配置信息,则该方法将返回一个 SOCKS5 Dialer 对象,否则将返回 nil。该方法接受一个 SOCKS5 认证器作为参数,用于验证 SOCKS5 代理的身份。
/* 使用代理 IP */
dialer, err := proxy.SOCKS5("tcp", GmailProxy, nil, proxy.Direct)

if err != nil {detail, _ := json.Marshal(Res{Code: 0, ErrMsg: "代理连接失败!"})
	return detail
}

搭建 Web 服务

1. 相关例子

package main

import (
	"fmt"
	"net/http"
	"strings"
	"log"
)

func sayhelloName(w http.ResponseWriter, r *http.Request) {r.ParseForm()  // 解析参数,默认是不会解析的
	fmt.Println(r.Form)  // 这些信息是输出到服务器端的打印信息
	fmt.Println("path", r.URL.Path)
	fmt.Println("scheme", r.URL.Scheme)
	fmt.Println(r.Form["url_long"])
	for k, v := range r.Form {fmt.Println("key:", k)
		fmt.Println("val:", strings.Join(v, ""))
	}
	fmt.Fprintf(w, "Hello astaxie!") // 这个写入到 w 的是输出到客户端的
}

func main() {http.HandleFunc("/", sayhelloName) // 设置访问的路由
	err := http.ListenAndServe(":9090", nil) // 设置监听的端口
	if err != nil {log.Fatal("ListenAndServe:", err)
	}
}

2. 相关概念

  • Request:用户请求的信息,用来解析用户的请求信息,包括 post、get、cookie、url 等信息
  • Response:服务器需要反馈给客户端的信息
  • Conn:用户的每次请求链接
  • Handler:处理请求和生成返回信息的处理逻辑
Golang 学习笔记,从入门到精通,持续记录

运行流程

3.Go 的 http 包详解

Go 为了实现高并发和高性能, 使用了 goroutines 来处理 Conn 的读写事件, 这样每个请求都能保持独立,相互不会阻塞,可以高效的响应网络事件。这是 Go 高效的保证。

客户端的每次请求都会创建一个 Conn,这个 Conn 里面保存了该次请求的信息,然后再传递到对应的 handler,该 handler 中便可以读取到相应的 header 信息,这样保证了每个请求的独立性。

常见问题

1.go 语言中的 Cgo 是什么

CGO 用于在 GO 代码中使用 C 语言编程,或者说是调用 C 代码封装的链接库文件中编写的函数,有些想用的东西 GOLANG 没有官方源码,但是 C 语言有时,就可以用 CGO 调用它。

2. 跨平台编译

  • CGO_ENABLE=1,打开 Cgo 标志,默认情况是关闭的。
  • GOOS=linux,编译目标系统为 Linux
  • GOARCH=amd64,编译目标的指令集架构为 64 位 x86 架构
Golang 学习笔记,从入门到精通,持续记录

跨平台编译

相关代码如下:

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go

2.go run 

go run 之后提示可执行文件不存在,可能的原因是跨平台编译的时候环境变量改成了 Linux。

3.os.Args[0]

os.Args[0],代表的是执行文件时,前面执行的那一部分,也就是参数之前的部分

// 获取可执行文件所在的路径
ex, err := os.Executable()
if err != nil {panic(err)
}
exPath := filepath.Dir(ex)
fmt.Println(exPath)

4.path/filepath

filepath 包实现了兼容各操作系统的文件路径的实用操作函数。

相关文档:https://studygolang.com/pkgdoc 

5. 三目运算符

Go 语言并不支持三目运算符,所以只能使用 if else。

6. 并发与并行

「多核」指的是有效利用 CPU 的多核提高程序执行效率

「并行」和「并发」一字之差,但其实是两个完全不同的概念,「并发」一般是由 CPU 内核通过时间片或者中断来控制的,遇到 IO 阻塞或者时间片用完时会交出线程的使用权,从而实现在一个内核上处理多个任务,而「并行」则是多个处理器或者多核处理器同时执行多个任务,同一时间有多个任务在调度,因此,一个内核是无法实现并行的,因为同一时间只有一个任务在调度。

多进程、多线程以及协程显然都是属于「并发」范畴的,可以实现程序的并发执行,至于是否支持「并行」,则要看程序运行系统是否是多核,以及编写程序的语言是否可以利用 CPU 的多核特性。

使用多核在 CPU 密集型计算中带来的性能提升还是非常显著的,不过对于 IO 密集型计算可能没有这么显著,甚至有可能比单核低,因为 CPU 核心之间的切换也是需要时间成本的,所以 IO 密集型计算并不推荐使用这种机制。

  • IO 密集型程序:程序在运行过程中执行的指令,其中涉及到一些 IO 操作,比如设备、文件、网络操作(等待客户端的链接)等,这些操作往往会使得当前程序阻塞住。比如数据库连接、网络请求等。
  • CPU 密集型程序:程序里的指令都是做计算用的,比如一些用于科学计算、高性能计算的程序,举个简单的例子(从 1 一直加到 10 亿)等。

7.Go 协程

在多核场景下,Go 语言的协程是并发与并行同时存在的。

Go 进程中的协程依托于线程,借助操作系统将线程调度到 CPU 执行,从而最终执行协程。

  • G — 表示 Goroutine,每一个 Goroutine 都包含堆栈、指令指针和其他用于调度的重要信息;
  • M — 表示操作系统的线程,它是被操作系统管理的线程;
  • P — 表示调度的上下文,它可以被看做一个运行于线程 M 上的本地调度器。

在某一时刻,一个 P 可能包含多个 G,同时一个 P 在任一时刻只能绑定一个 M。同时,一个 G 并不是固定绑定同一个 P 的,同样 P 绑定哪一个 M 也不固定。

每个线程中都有一个特殊的协程 G0,其作用是执行协程调度的一系列运行时代码,而一般的协程用于执行用户代码。

协程经历 G→G0→G 的过程完成一次循环调度。协程上下文切换要保存当前执行现场,并存储在 g.gobuf 结构体中,其中主要保存了几个 cup 的寄存器值 rsp,rip,rbp。为了避免栈溢出,协程 G0 的栈会重复使用。

相关文档:https://blog.csdn.net/A0AA0aaa/article/details/125352733

知识图谱

Golang 学习笔记,从入门到精通,持续记录

学习路线

HTTP 相关

net/http:https://pkg.go.dev/net/http

1. 常用方法

net/http 内的常用方法:

  • http.HandleFunc 方法用于注册 URL 模式和处理函数。
  • http.ListenAndServe 方法用于启动 Web 服务器并监听请求。
  • http.Redirect 方法用于将请求重定向到另一个 URL,http.Redirect(w, r, "/new-page", http.StatusFound)。
  • http.ServeFile 方法可以让你在 Web 服务器中提供静态文件,例如图像、样式表和 Javascript,http.ServeFile(w, r, "static/image.png")

Request 对象:

  • func (r *Request) AddCookie(c *Cookie)
  • func (r *Request) BasicAuth() (username, password string, ok bool)
  • func (r *Request) Clone(ctx context.Context) *Request
  • func (r *Request) Context() context.Context
  • func (r *Request) Cookie(name string) (*Cookie, error)
  • func (r *Request) Cookies() []*Cookie
  • func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)
  • func (r *Request) FormValue(key string) string
  • func (r *Request) MultipartReader() (*multipart.Reader, error)
  • func (r *Request) ParseForm() error
  • func (r *Request) ParseMultipartForm(maxMemory int64) error
  • func (r *Request) PostFormValue(key string) string
  • func (r *Request) ProtoAtLeast(major, minor int) bool
  • func (r *Request) Referer() string
  • func (r *Request) SetBasicAuth(username, password string)
  • func (r *Request) UserAgent() string
  • func (r *Request) WithContext(ctx context.Context) *Request
  • func (r *Request) Write(w io.Writer) error
  • func (r *Request) WriteProxy(w io.Writer) error

ResponseWrite 对象:

  • Header,方获取代表响应头的 map 类型数据。
  • Write,方法用于将响应数据写入传输的流中。可以使用它逐步发送数据,而不是一次性全部发送。
  • WriteHeader,方法用于将响应头写入传输的流中。它可以在响应正文发送之前设置 HTTP 状态码、响应头部和 cookie 等信息。

2. 解析 JSON 参数

var mail friend.SendMail
err := json.NewDecoder(r.Body).Decode(&mail)

if err != nil {http.Error(w, err.Error(), http.StatusBadRequest)
	return
}

问题总结

1.IDE 不识别模块

设置 -> Go -> Go 模块 -> 启用 Go 模块集成,这样 IDE 就能够识别 Gopath 内的模块了。

2. 新建 package

新建 package 的时候,一般 package 的名称和目录名保持一致,同一个目录内的文件都是在一个包的作用域内,多个文件的 package 需要一致。

其它模块使用这个 package 时,通过 package. 成员进行调用。import 其他目录内的包时,通过“程序包名. 包名”进行引入。同一个包内的文件,可以直接调用其他成员。

3.json 输出异常

使用 log.Printf、fmt.Fprint 这些函数写入时,会进行格式化输出,由于 json 包含 html 等特殊字符,会触发格式化,最终导致输出异常。

/* 避免 unicode 转义 */
buff := &bytes.Buffer{}
encoder := json.NewEncoder(buff)
encoder.SetEscapeHTML(false)
encoder.Encode(res{Code: 1, ErrMsg: "同步成功!", Data: &data})

4. 按值和引用

按值传递:

  •  bool
  • string
  • int, int8, int16, int32, int64
  • float32, float64
  • complex64, complex128
  • struct
  • array

按引用传递

在赋值操作中,无论是按值传递的基础类型还是引用类型,都是值复制。可以使用取地址运算符 "&" 来获取变量的地址。

5. 输出函数

fmt 包提供了一系列用于格式化输出的函数,常用的函数包括:

  • fmt.Print(a ...interface{}):将参数列表 a 中的内容格式化为字符串并打印到标准输出中,不会自动换行。
  • fmt.Println(a ...interface{}):将参数列表 a 中的内容格式化为字符串并打印到标准输出中,自动在末尾添加一个换行符。
  • fmt.Printf(format string, a ...interface{}):将格式化字符串 format 和参数列表 a 中的内容格式化为字符串并打印到标准输出中,可以使用类似 C 语言中的 printf() 函数的格式化字符串。
  • fmt.Sprintf:用于格式化输出字符串。

常用场景包括:

  • 输出简单的字符串或变量值。
  • 输出格式化的文本或变量值。

log 包提供了一系列用于日志记录的函数,常用的函数包括:

  • log.Print(a ...interface{}):将参数列表 a 中的内容格式化为字符串并写入日志输出,不会自动换行。
  • log.Println(a ...interface{}):将参数列表 a 中的内容格式化为字符串并写入日志输出,自动在末尾添加一个换行符。
  • log.Printf(format string, a ...interface{}):将格式化字符串 format 和参数列表 a 中的内容格式化为字符串并写入日志输出,可以使用类似 C 语言中的 printf() 函数的格式化字符串。

常用场景包括:

  • 记录错误信息或调试信息。
  • 记录应用程序的运行状态和性能数据。

log 包默认将日志输出到标准错误输出中,可以通过设置 log.SetOutput() 函数的参数来改变输出目标。另外,log 包还提供了一些其他的函数,例如 log.Fatal() 和 log.Panic(),用于在发生严重错误时终止程序的运行。

6. 命令行参数

flag 包提供了一组函数来解析命令行参数,包括标志和非标志参数。对于标志参数,可以使用 flag.String()、flag.Bool()、flag.Int() 等函数来定义。对于非标志参数,可以使用 flag.Args() 函数来获取。

import (
    "flag"
    "fmt"
)

func main() {port := flag.Int("p", 8080, "port number")
    flag.Parse()
    fmt.Println("port:", *port)
}

7. 类型转换

Go 标准库中的 strconv 包提供了一些常用的字符串和基本数据类型之间的转换方法。

  • strconv.Atoi(s string) (int, error) 函数可以将字符串转换为整数。
  • strconv.Itoa(i int) string 函数可以将整数转换为字符串。
  • strconv.ParseBool(str string) (bool, error) 函数可以将字符串转换为布尔值。
  • strconv.FormatBool(b bool) string 函数可以将布尔值转换为字符串。
  • strconv.ParseFloat(s string, bitSize int) (float64, error) 函数可以将字符串转换为浮点数。
  • strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int) string 函数可以将浮点数转换为字符串。

8. 字节操作

Go 语言中的 bytes 包提供了一些用于操作字节切片的方法和工具函数。

  • NewBuffer(buf []byte) *Buffer,用于创建一个 Buffer 对象,该对象包含一个字节切片(buf)。如果 buf 不为 nil,则 Buffer 对象将使用该切片作为缓冲区,否则将创建一个新的切片作为缓冲区。
  • NewBufferString(s string) *Buffer,用于创建一个 Buffer 对象,该对象包含一个字符串(s)对应的字节切片作为缓冲区。
  • (b *Buffer) Write(p []byte) (n int, err error),用于向 Buffer 对象中写入字节切片(p)。该方法返回写入的字节数(n)和任何可能的错误(err)。
  • (b *Buffer) WriteByte(c byte) error,用于向 Buffer 对象中写入一个字节(c)。该方法返回任何可能的错误(err)。
  • (b *Buffer) WriteString(s string) (n int, err error),用于向 Buffer 对象中写入一个字符串(s)。该方法返回写入的字节数(n)和任何可能的错误(err)。
  • (b *Buffer) Read(p []byte) (n int, err error),用于从 Buffer 对象中读取字节切片(p)。该方法返回读取的字节数(n)和任何可能的错误(err)。
  • (b *Buffer) ReadByte() (byte, error),用于从 Buffer 对象中读取一个字节。该方法返回读取的字节(byte)和任何可能的错误(err)。
  • (b *Buffer) ReadString(delim byte) (string, error),用于从 Buffer 对象中读取一个以指定分隔符(delim)结尾的字符串。该方法返回读取的字符串和任何可能的错误(err)。
  • (b *Buffer) Bytes() []byte,返回 Buffer 对象中的字节切片。
  • (b *Buffer) String() string,返回 Buffer 对象中的字符串。

9. 异常报错

突然报错:package main is not in GOROOT,重装、重建项目都试了,一直报错。

go run main.go  // 这个正确
go run main       // 这个报错,必须加后缀

10.filepath 包

  • Join(elem ...string) string,方法可以将多个字符串拼接成一个路径,自动添加路径分隔符。filepath.Join("dir1", "dir2", "file.txt")
  • Split(path string) (dir, file string) 方法可以将一个路径分成目录部分和文件名部分。filepath.Split("/path/to/file.txt")
  • Base(path string) string,返回路径的最后一个元素。在提取元素前会求掉末尾的路径分隔符。如果路径是 "",会返回".";如果路径是只有一个斜杆构成,会返回单个路径分隔符。
  • Dir(path string) string 方法可以获取路径中的目录部分。filepath.Dir("/path/to/file.txt")
  • Ext(path string) string,返回 path 文件扩展名。返回值是路径最后一个路径元素的最后一个 '.' 起始的后缀(包括 '.')。如果该元素没有 '.' 会返回空字符串。
  • Clean(path string) string 方法可以清理路径中的冗余部分,例如多余的路径分隔符、. 和 .. 等。filepath.Clean("/path/to/../dir/./file.txt")
  • Abs(path string) (string, error) 方法可以将相对路径转换为绝对路径。filepath.Abs("../dir/file.txt")
  • Walk(root string, walkFn WalkFunc) error,遍历 root 指定的目录下的文件树,对每一个该文件树中的目录和文件都会调用 walkFn,包括 root 自身。
  • ...

在处理文件路径时,应该始终使用 filepath 包中的方法,而不是手动拼接路径字符串,以确保在不同操作系统上的兼容性。

11.ioutil

ioutil 包是 Go 语言标准库中提供的一个工具类包,提供了一些方便的 I/O 操作函数。

  • ReadAll(r io.Reader) ([]byte, error),从 r 读取数据直到 EOF 或遇到 error,返回读取的数据和遇到的错误。
  • ReadFile(filename string) ([]byte, error),从 filename 指定的文件中读取数据并返回文件的内容。成功的调用返回的 err 为 nil 而非 EOF。因为本函数定义为读取整个文件,它不会将读取返回的 EOF 视为应报告的错误。
  • WriteFile(filename string, data []byte, perm os.FileMode) error,函数向 filename 指定的文件中写入数据。如果文件不存在将按给出的权限创建文件,否则在写入数据之前清空文件。
  • ReadDir(dirname string) ([]os.FileInfo, error),返回 dirname 指定的目录的目录信息的有序列表。
  • TempFile(dir, prefix string) (*os.File, error),在 dir 目录下创建一个新的、使用 prefix 为前缀的临时文件,以读写模式打开该文件并返回 os.File 指针。
  • TempDir(dir, prefix string) (string, error),在 dir 目录里创建一个新的、使用 prfix 作为前缀的临时文件夹,并返回文件夹的路径。不同程序同时调用该函数会创建不同的临时目录,调用本函数的程序有责任在不需要临时文件夹时摧毁它。

    正文完
     0
    Yojack
    版权声明:本篇文章由 Yojack 于2024-09-10发表,共计10788字。
    转载说明:
    1 本网站名称:优杰开发笔记
    2 本站永久网址:https://yojack.cn
    3 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长进行删除处理。
    4 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
    5 本站所有内容均可转载及分享, 但请注明出处
    6 我们始终尊重原创作者的版权,所有文章在发布时,均尽可能注明出处与作者。
    7 站长邮箱:laylwenl@gmail.com
    评论(没有评论)