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

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本质上说跟其它语言中的同名函数功能一样: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.初始值

  • 数值类型(包括complex64/128)为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)
  • 以下几种类型为 nil:
    var a *int
    var a []int
    var a map[string] int
    var a chan int
    var a func(string) int
    var a error // error 是接口

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(`<style[Ss]+?</style>`)
	src = re.ReplaceAllString(src, "")
	//去除HTMLUnscape的STYLE
	re, _ = regexp.Compile(`&lt;style[Ss]+?&lt;/style&gt;`)
	src = re.ReplaceAllString(src, "")

	//去除SCRIPT
	re, _ = regexp.Compile(`<script[Ss]+?</script>`)
	src = re.ReplaceAllString(src, "")
	//去除HTMLUnsapce的SCRIPT
	re, _ = regexp.Compile(`&lt;script[Ss]+?&lt;/script&gt;`)
	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 < flag.NArg(); i++ {
		if 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

按引用传递

  • slice
  • channel
  • map

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

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作为前缀的临时文件夹,并返回文件夹的路径。不同程序同时调用该函数会创建不同的临时目录,调用本函数的程序有责任在不需要临时文件夹时摧毁它。