1 Golang 基础入门
1.1 Golang简介
Go(Golang)是Google的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态强类型、编译型语言。Go语言的语法与 C 相近,但功能上有:内存安全、GC(垃圾回收),结构形态及CSP-style并发计算。
Go语言的特点:
1.背靠大厂,google背书,可靠
2.天生支持并发(最显著特点)
3.语法简单,容易上手
4.内置runtime,支持垃圾回收
5.可直接编译成机器码,不依赖其他库
6.丰富的标准库
7.跨平台编译
Go语言的应用领域:服务器编程、开发云平台、区块链、分布式系统、网络编程。
使用Go语言的公司(项目):
| 公司 | 项目 |
|---|---|
| Kubernetes | |
| facebookgo | |
| 腾讯 | 蓝鲸平台、容器技术 |
| 百度 | 运维项目BFE |
| 京东 | 消息推送系统、云存储、京东商城 |
| 小米 | 运维监控系统、小米互娱、小米商城、小米视频、小米生态链 |
| 360 | 日志搜索系统Poseidon |
1.2 Go语言开发环境
1.2.1 windows平台
1.下载安装包并配置环境变量
下载地址:https://golang.google.cn/dl/
安装路径选择一个比较好找的路径,例如:D:\Go,其他安装都选择”下一步“即可。
安装完成后,把D:\Go\bin目录添加到环境变量(系统变量-Path中添加),这里就可以使用go了,在命令行输入 go version查看版本,输出结果如下所示。
C:\Users\bestv>go version
go version go1.26.2 windows/amd64
2.配置Go环境
使用go mod管理库,需要科学上网
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct
$env:GO111MODULE = "on"
$env:GOPROXY = "https://goproxy.cn"
3.安装配置git
下载地址:https://www.git-scm.com/download/
安装路径选择一个比较好找的路径,例如:D:\Git,其他安装都选择”下一步“即可。
安装完成后,把D:\Git\bin目录添加到环境变量(系统变量-Path中添加),这里就可以使用go了,在命令行输入 git version查看版本,输出结果如下所示。
C:\Users\bestv>git version
git version 2.54.0.windows.1
# 测试git拉取项目
# git clone相当于go get
C:\Users\bestv>git clone https://github.com/gin-gonic/gin.git
Cloning into 'gin'...
remote: Enumerating objects: 11207, done.
remote: Counting objects: 100% (128/128), done.
remote: Compressing objects: 100% (83/83), done.
remote: Total 11207 (delta 100), reused 45 (delta 45), pack-reused 11079 (from 4)
Receiving objects: 100% (11207/11207), 10.39 MiB | 10.74 MiB/s, done.
Resolving deltas: 100% (6214/6214), done.

4.goroot和gopath
goroot就是go安装的根目录,gopath就是go项目所在的路径,高版本go项目已经不再依赖gopath来管理项目,使用go mod来管理项目。
1.2.2 Linux平台
如果不是要在Linux平台敲go代码就不需要在Linux平台安装Go,写好的go代码只需要跨平台编译好之后就可以拷贝到Linux服务器上运行,这也是go程序跨平台易部署的优势。
在版本选择页面选择并下载好go1.26.2.linux-amd64.tar.gz文件:
wget https://dl.google.com/go/go1.26.2.linux-amd64.tar.gz
将下载好的文件解压到/usr/local目录下:
tar -zxvf go1.26.2.linux-amd64.tar.gz -C /usr/local # 解压
如果提示没有权限,加上sudo以root用户的身份再运行。执行完就可以在/usr/local/下看到go目录了。
配置环境变量: Linux下有两个文件可以配置环境变量,其中/etc/profile是对所有用户生效的;$HOME/.profile是对当前用户生效的,根据自己的情况自行选择一个文件打开,添加如下两行代码,保存退出。
export GOROOT=/usr/local/go
export PATH=$PATH:$GOROOT/bin
修改/etc/profile后要重启生效,修改$HOME/.profile后使用source命令加载$HOME/.profile文件即可生效。 检查:
~ go version
go version go1.26.2 linux/amd64
1.2.3 Mac平台
下载可执行文件版,直接点击下一步安装即可,默认会将go安装到/usr/local/go目录下。
上一步安装过程执行完毕后,可以打开终端窗口,输入go version命令,查看安装的Go版本。
go version go1.26.2 darwin/amd64
1.3 Golang开发工具
golang的开发工具有很多,例如:vim、sublime、atom、LiteIDE、eclipse、goland、vscode
1.使用goland开发Go应用
goland是一款付费的、功能强大的golang集成开发环境,是Jetbrain公司的产品。下载地址:https://www.jetbrains.com/go/ goland非常智能,几乎不需要配置,安装即用。
2.使用vscode开发Go应用
1.下载安装vscode https://code.visualstudio.com/
2.下载安装插件,参考文档:https://marketplace.visualstudio.com/items?itemName=golang.go
3.安装code runner运行脚本
# 快捷键:pkgm---创建主包和主方法 fp---打印的快速补齐
# 运行:1.go run main.go 2.安装code runner插件后直接运行
package main
import "fmt"
func main() {
fmt.Println("Hello,Go")
}

1.4 Golang常用命令
在终端中输入 go help 即可显示所有的 go 命令以及相应命令功能简介,主要有下面这些:
| 命令 | 说明 | 命令 | 说明 |
|---|---|---|---|
| build | 编译包和依赖 | list | 列出包 |
| clean | 移除对象文件 | run | 编译并运行go程序 |
| doc | 显示包或者符号的文档 | test | 运行测试 |
| env | 打印go的环境信息 | tool | 运行go提供的工具 |
| bug | 启动错误 | version | 显示go的版本 |
| fix | 运行go tool fix | vet | 运行go tool vet |
| fmt | 运行gofmt进行格式化 | generate | 从processing source生成go文件 |
| install | 编译并安装包和依赖 | get | 下载并安装包和依赖 |
参考文档:https://golang.org/doc/cmd
PS C:\Users\bestv\Desktop\go_pro> go run main.go
Hello,Go
PS C:\Users\bestv\Desktop\go_pro> go build main.go
PS C:\Users\bestv\Desktop\go_pro> go mod init go_pro
go: creating new go.mod: module go_pro
go: to add module requirements and sums:
go mod tidy
PS C:\Users\bestv\Desktop\go_pro> go get github.com/go-sql-driver/mysql
go: downloading github.com/go-sql-driver/mysql v1.10.0
go: downloading filippo.io/edwards25519 v1.2.0
go: added filippo.io/edwards25519 v1.2.0
go: added github.com/go-sql-driver/mysql v1.10.0
PS C:\Users\bestv\Desktop\go_pro> go list
go_pro
PS C:\Users\bestv\Desktop\go_pro> go version
go version go1.26.2 windows/amd64
PS C:\Users\bestv\Desktop\go_pro>
1.5 VSCode快捷键
1.快捷键位置:File->Preferences->Keyboard Shortcuts
2.常用快捷键
1. 行注释 ctrl + /
2. 块注释 shift+alt+a (可以修改为ctrl+shift+/)
3. ctrl+a 全选
4. ctrl+c 复制
5. ctrl+v 粘贴
6. ctrl+shift+k 删除行
7. ctrl+e 查找文件
8. ctrl+shift+p 打开设置命令行
3.修改快捷键
1.打开快捷键 File->Preferences->Keyboard Shortcuts
2.双击要修改的快捷键
3.键盘输入想要使用的快捷键
4.快速生成代码片段
1.pkgm main包+main主函数
2.ff fmt.Printf("", var)
3.for for i := 0; i < count; i++ {}
4.forr for _, v := range v {}
5.fmain func main() {}
6.a.print! fmt.Printf("a: %v\\n", a)
1.6 编写Golang代码
1.代码组织
Go应用使用包和模块来组织代码,包对应到文件系统就是文件夹,模块就是**.go**的go源文件。一个包中会有多个模块,或者多个子包。
2.Go项目管理工具
早期的go项目使用gopath来管理项目,不方便而且容易出错,golang 1.11 开始使用gomod管理项目,当然还有第三方模块例如:govendor
3.实现步骤
3.1 创建项目:新建文件夹(Go_pro)
可通过在文件夹中通过cmd的方式,再输入code .以便于在vscode打开

3.2 初始化项目:通过go mod init 项目名去初始化项目
# go mod init 项目名
go mod init Go_pro

3.3 创建包:包名可以随意指定,一般包文件夹名称和包名是一致的
# user是包名
package user
func Hello() string {
return "Hello,Golang!"
}

3.4 创建模块与相互调用
package main
// 导入包user
import (
"Go_pro/user"
"fmt"
)
func main() {
// 调用方法 user.Hello().var!...
s := user.Hello()
fmt.Printf("s: %v\n", s)
// s := user.Hello()
// s.print! ===> fmt.Printf("s: %v\n", s)
}

2 变量、常量与数据类型
2.1 标识符
标识符的英文是identifier,通俗讲,就是给变量、常量、函数、方法、结构体、数组、切片、接口起名字。
标识符的组成:
1.标识符由数字、字母和下划线(_)组成。
2.只能以字母和下划线(_)开头。 如:abc123 _sysVar
3.标识符区分大小写。 如:name、Name、NAME
func main() {
// 正确的标识符
var name string
var age int
var _sys int
// 错误的标识符
var 1name string
var &age int
var !email
}
2.2 关键字
Go语言提供了25个关键字,如下所示:
| break | default | func | interface | select |
|---|---|---|---|---|
| case | defer | go | map | struct |
| chan | else | goto | package | switch |
| const | fallthrough | if | range | type |
| continue | for | import | return | var |
除了以上关键字,Go语言还有36个预定义标识符,其中包含了基本类型的名称和一些基本的内置函数,见下表:
| append | bool | byte | cap | close | cpmplex | complex64 | complex128 | uint16 |
|---|---|---|---|---|---|---|---|---|
| copy | false | float32 | float64 | imag | int | int8 | int16 | uint32 |
| int32 | int64 | iota | len | make | new | nil | panic | uint64 |
| println | real | recover | string | true | uint | uint8 | uintptr |
2.3 命名规则
2.3.1 Go语言区分大小写
命名规则涉及变量、常量、全局函数、结构、接口、方法等的命名。
Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。
当命名(常量、变量、类型、函数名、结构字段等)以一个大写字母开头,如:GetUserName,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public); 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。
2.3.2 包名称
保持package的名字和目录保持一致,尽量采取简短、有意义的包名,不和标准库冲突。包名应该为小写单词,不要使用下划线或者混合大小写。
package dao
package service
2.3.3 文件命名
尽量采取简短、有意义的文件名,应该为小写单词,使用下划线分隔各个单词。
customer_dao.go
2.3.4 结构体命名
采用驼峰命名法,首字母根据访问控制大写或者小写
struct 申明和初始化格式采用多行,例如下面:
type CustomerOrder struct {
Name string
Address string
}
order := CustomerOrder{"tom", "北京海淀"}
2.3.5 接口命名
命名规则基本和上面的结构体类型
单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。
type Reader interface {
Read(p []byte) (n int, err error)
}
2.3.6 变量命名
和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则:
如果变量为私有,且特有名词为首个单词,则使用小写,如 appService。
若变量类型为 bool 类型,则名称应以 Has、Is、Can 或 Allow 开头。
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
2.3.7 常量命名
常量均需使用全部大写字母组成,并使用下划线分词
const APP_URL = "https://www.duoke360.com"
如果是枚举类型的常量,需要先创建相应类型:
type Scheme string
const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)
2.3.8 错误处理
错误处理的原则是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。
接收到错误,要么返回err,或者使用log记录下来尽早return:一旦有错误发生,马上返回,尽量不要使用panic,除非你知道你在做什么,错误描述如果是英文必须为小写,不需要标点结尾,采用独立的错误流进行处理
// 错误写法
if err != nil {
// 错误处理
} else {
// 正常代码
}
// 正确写法
if err != nil {
// 错误处理
return // 或者继续
}
// 正常代码
2.3.9 单元测试
单元测试文件名命名规范为 example_test.go, 测试用例的函数名称必须以 Test 开头,例如:TestExample 每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试 。
2.4 Golang变量
变量是计算机语言中能储存计算结果或能表示值的抽象概念。
不同的变量保存的数据类型可能会不一样。
2.4.1 声明变量
Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。 并且Go语言的变量声明后必须使用。
1.声明变量的语法
var identifier type
var:声明变量关键字
identifier:变量名称
type:变量类型
func main() {
var name string
var age int
var b bool
}
2.4.2 批量声明变量
使用一个var关键字,把一些变量写在一个括号()里
func main() {
// 声明变量
/* var name string
var age int
var b bool */
// 批量声明变量
var (
name string
age int
b bool
)
name = "Tom"
age = 20
b = true
// name.Printf!、age.Printf!、m.Printf!......
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
fmt.Printf("b: %v\n", b)
}
2.4.3 变量初始化
Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串“”。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil。
1.变量初始化语法:var 变量名 类型 = 表达式
func main() {
// 初始化---赋初值
var name string = "小罗"
var site string = "https://linuxtech.top"
var age int = 27
}
2.类型推导:声明变量时,可根据初始化值进行类型推导,从而省略类型。
func main() {
// 类型推导
var name = "小罗"
var site = "https://linuxtech.top"
var age = 30
}
3.初始化多个变量:可以一次初始化多个变量,中间用逗号分隔。
func main() {
// 变量的批量初始化
var name, site, age = "小罗", "https://linuxtech.top", 27
}
2.4.4 短变量声明
在函数内部,可以使用 :=运算符对变量进行声明和初始化。
func main() {
// 短变量声明 :=
name := "小罗"
site := "https://linuxtech.top"
age := 27
}
注意:这种方法只适合在函数内部,函数外面不能使用。
2.4.5 匿名变量
如果接收到多个变量,有一些变量使用不到,可以使用下划线_表示变量名称,这种变量叫做匿名变量。例如:
/* func getData() (name string, site string, age int) {
return "小罗", "https://linuxtech.top", 27
} */
func getData() (string, string, int) {
return "小罗", "https://linuxtech.top", 27
}
func main() {
// 使用不到的变量可以用_表示变量名称
name, _, age := getData()
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
}
2.5 Golang常量
常量是程序编译阶段就确定下来的值,而程序在运行时则无法改变该值。在Go程序中,常量可以是数值类型(整型、浮点型和复数类型)、布尔类型、字符串类型等。
2.5.1 定义常量
定义一个常量使用const关键字,语法格式如下:
const constantName [type]= value
const:定义常量关键字
constantName:常量名称
type:常量类型
value:常量的值
func main() {
// 声明常量
const PI float64 = 3.14
const PI2 = 3.1415 // 可省略类型
const (
width = 100
height = 200
)
const a, b, c = "小罗", 27, "https://linuxtech.top/"
}
2.5.2 声明多个常量
const同时声明多个常量时,如果省略了值则表示和上面一行的值相同
func main() {
const (
m = 908
n
p
)
fmt.Printf("m: %v\n", m)
......
}
// 运行结果 m: 908 n: 908 p: 908
2.5.3 iota
iota 比较特殊,可以被认为是一个可被编译器修改的常量,它默认开始值是0,每调用一次加1。遇到 const 关键字时被重置为 0。
func main() {
const (
a1 = iota
a2 = iota
a3 = iota
)
fmt.Printf("a1: %v\n", a1)
......
}
// 运行结果 a1: 0 a2: 1 a3: 2
1.使用_跳过某些值
func main() {
const (
a1 = iota
_
a2 = iota
)
fmt.Printf("a1: %v\\n", a1)
......
}
// 运行结果 a1: 0 a2: 2
2.iota声明中间插队
func main() {
const (
a1 = iota
a2 = 567
a3 = iota
)
fmt.Printf("a1: %v\\n", a1)
......
}
// 运行结果 a1: 0 a2: 567 a3: 2
2.6 数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程需要用大数据的时候才需要申请大内存,就可以充分利用内存。
Go 语言按类别有以下几种数据类型:
| 数据类型 | 描述 |
|---|---|
| 数字类型 | 整型–int、浮点型–float32、float64,Go语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
| 布尔型 | 布尔型的值只可以是常量 true 或者 false。 |
| 字符串 | 字符串是一串固定长度的字符连接起来的字符序列。 Go的字符串是由单个字节连接起来的。 Go语言字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
| 派生类型 | 指针、数组、结构化、Channel、函数、切片、接口、Map |
func fun() {
}
func main() {
age := 29
b := true
var name string = "小罗"
// 数字、布尔、字符串
// 格式化输出 %T代表类型 \n代表换行 快捷键ff===>fmt.Printf("", var)
fmt.Printf("%T\n", age)
......
// 指针类型
a := 567
p := &a // &a表示取a的地址
// 数组类型
m := [2]int{23, 56} // 长度为2的数组
// 切片类型
n := []int{23, 56, 890} // int类型的切片
// 函数类型
fmt.Printf("%T\n", fun)
}
2.6.1 数字类型
Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
Go 也有基于架构的类型,例如:int、uint 和 uintptr。
类型的长度是根据运行程序所在的操作系统类型所决定的:
int 和 uint 在 32 位操作系统上,均使用 32 位(4 个字节),在 64 位操作系统上使用 64 位(8 个字节)。
uintptr 的长度被设定为足够存放一个指针即可。
Go 语言中没有 float 类型。
Go语言中只有 float32 和 float64,没有double类型。
与操作系统架构无关的类型都有固定的大小,并在类型的名称中就可以看出来:
int 型是计算最快的一种类型。
整型的零值为 0,浮点型的零值为 0.0。
| 数据类型 | 描述 |
|---|---|
| uint8 | 无符号 8 位整型 (0 到 255) |
| uint16 | 无符号 16 位整型 (0 到 65535) |
| uint32 | 无符号 32 位整型 (0 到 4294967295) |
| uint64 | 无符号 64 位整型 (0 到 18446744073709551615) |
| int8 | 有符号 8 位整型 (-128 到 127) |
| int16 | 有符号 16 位整型 (-32768 到 32767) |
| int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
| int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
各个数字类型的长度和取值范围如下:
package main
// 按住ctrl键,可以打开相关模块的外部链接
import (
"fmt"
"math"
"unsafe"
)
func main() {
var i8 int8
var i16 int16
var i32 int32
var i64 int64
var ui8 uint8
var ui16 uint16
var ui32 uint32
var ui64 uint64
fmt.Printf("%T %dB %v~%v\n", i8, unsafe.Sizeof(i8), math.MinInt8, math.MaxInt8)
fmt.Printf("%T %dB %v~%v\n", i16, unsafe.Sizeof(i16), math.MinInt16, math.MaxInt16)
fmt.Printf("%T %dB %v~%v\n", i32, unsafe.Sizeof(i32), math.MinInt32, math.MaxInt32)
fmt.Printf("%T %dB %v~%v\n", i64, unsafe.Sizeof(i64), math.MinInt64, math.MaxInt64)
fmt.Printf("%T %dB %v~%v\n", ui8, unsafe.Sizeof(ui8), 0, math.MaxUint8)
fmt.Printf("%T %dB %v~%v\n", ui16, unsafe.Sizeof(ui16), 0, math.MaxUint16)
fmt.Printf("%T %dB %v~%v\n", ui32, unsafe.Sizeof(ui32), 0, math.MaxUint32)
fmt.Printf("%T %dB %v~%v\n", ui64, unsafe.Sizeof(ui64), 0, uint64(math.MaxUint64))
var f32 float32
var f64 float64
fmt.Printf("%T %dB %v~%v\n", f32, unsafe.Sizeof(f32), -math.MaxFloat32, math.MaxFloat32)
fmt.Printf("%T %dB %v~%v\n", f64, unsafe.Sizeof(f64), -math.MaxFloat64, math.MaxFloat64)
var ui uint
ui = uint(math.MaxUint64) // 再+1会导致overflows错误
fmt.Printf("%T %dB %v~%v\n", ui, unsafe.Sizeof(ui), 0, ui)
var imax, imin int
imax = int(math.MaxInt64) // 再+1会导致overflows错误
imin = int(math.MinInt64) // 再-1会导致overflows错误
fmt.Printf("%T %dB %v~%v\n", imax, unsafe.Sizeof(imax), imin, imax)
}
二进制、八进制、十六进制浮点数的格式定义数字:
func main() {
// 十进制
var a int = 10
fmt.Printf("%d \n", a)
fmt.Printf("%b \n", a) // 占位符%b表示二进制
// 八进制 以0开头
var b int = 077
fmt.Printf("%o \n", b)
// 十六进制 以0x开头
var c int = 0xff
fmt.Printf("%x \n", c)
fmt.Printf("%X \n", c)
}
// 运行结果 10 1010 77 ff FF
2.6.2 浮点型
Go语言支持两种浮点型数:float32和float64,这两种浮点型数据格式遵循IEEE 754标准。
float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32。
float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64。
| 数据类型 | 描述 |
|---|---|
| float32 | IEEE-754 32位浮点型数 |
| float64 | IEEE-754 64位浮点型数 |
| complex64 | 32 位实数和虚数 |
| complex128 | 64 位实数和虚数 |
打印浮点数时,可以使用fmt包配合动词%f,代码如下:
func main() {
// 浮点类型
fmt.Printf("%f\n", math.Pi)
fmt.Printf("%.2f\n", math.Pi) // .2f表示两位小数
}
// 运行结果 3.141593 3.14
2.6.3 复数
复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。
func main() {
// 复数
var c1 complex64
c1 = 1 + 6i
var c2 complex128
c2 = 3 + 8i
fmt.Println(c1)
fmt.Println(c2)
}
// 运行结果 (1+6i)(3+8i)
2.6.4 布尔类型
Go语言中的布尔类型有两个常量值:true和false。布尔类型经常用在条件判断语句、循环语句,也可用在逻辑表达式中。
func main() {
var b1 bool = true
var b2 bool = false
var b3 = true
var b4 = false
b5 := true
b6 := false
fmt.Printf("b1: %v\n", b1)
......
}
1.用在条件判断中
func main() {
// 1.用在条件判断中
age := 16
r := age >= 18
if r {
fmt.Println("你已经成年了!")
} else {
fmt.Println("你还未成年!")
}
}
// 运行结果: 你还未成年!
2.用在循环语句中
func main() {
// 2.用在循环语句中
count := 10
for i := 0; i < count; i++ {
fmt.Printf("i: %v\n", i) // 快捷键---i.Printf
}
}
3.用在逻辑表达式中
func main() {
// 3.用在逻辑表达式中
Age := 18
gender := "男"
if Age >= 18 && gender == "男" {
fmt.Println("你是成年男子!")
}
}
注意:不能使用
0和非0表示真假
func main() {
i := 1
if i {
// 编译错误
}
}
2.6.5 其他数字类型
以下列出了其他更多的数字类型:
| 数据类型 | 描述 |
|---|---|
| byte | 类似 uint8 |
| rune | 类似 int32 |
| uint 32 或 64 位 | uint 32 或 64 位 |
| int | 与 uint 一样大小 |
| uintptr | 无符号整型,用于存放一个指针 |
2.7 字符串
一个Go语言字符串是一个任意字节的常量序列。
字符串就是byte类型的字节数组—[] byte
2.7.1 字符串字面量
在Go语言中,字符串字面量使用双引号 "" 或者反引号 ' 创建。双引号用来创建可解析的字符串,支持转义,但不能用来引用多行;反引号用来创建原生的字符串字面量,可能由多行组成,但不支持转义,并且可以包含除了反引号外其他所有字符。双引号创建可解析的字符串应用最广泛,反引号用来创建原生的字符串则多用于书写多行消息,HTML以及正则表达式。
func main() {
var str1 string = "你好,中国!"
var str2 = "Hello,Golang!"
str3 := "Golang,Java,C#..."
str4 := `
<html>
<head><title>Hello golang!</title>
</html>
`
fmt.Printf("str1: %v\n", str1)
......
}
2.7.2 字符串连接
1.加号
虽然Go语言中的字符串是不可变的,但是字符串支持 + 级联操作和+=追加操作。
func main() {
// 1.字符串连接--使用加号
name := "小罗"
age := "30"
msg := name + " " + age
fmt.Printf("msg: %v\n", msg)
fmt.Println("-------------")
msg = ""
msg += name
msg += " "
msg += age
fmt.Printf("msg: %v\n", msg)
}
Golang 里面的字符串都是不可变的,每次运算都会产生一个新的字符串,所以会产生很多临时的无用的字符串,不仅没有用,还会给 gc 带来额外的负担,所以性能比较差。
2.fmt.Sprintf()函数
func main() {
// 2.字符串连接--fmt.Sprintf()函数
Name := "小罗"
Age := "30"
Msg := fmt.Sprintf("Name=%s,Age=%s", Name, Age)
fmt.Printf("msg: %v\n", Msg)
}
内部使用
[]byte实现,不像直接运算符这种会产生很多临时的字符串,但是内部的逻辑比较复杂,有很多额外的判断,还用到了interface,所以性能也不是很好。
3.strings.Join()
func main() {
// 3.字符串连接--strings.Join()方法
nAme := "小罗"
aGe := "30"
mSg := strings.Join([]string{nAme, aGe}, ",")
fmt.Printf("mSg: %v\n", mSg)
}
join会先根据字符串数组的内容,计算出一个拼接之后的长度,然后申请对应大小的内存,一个一个字符串填入,在已有一个数组的情况下,这种效率会很高,但是本来没有,去构造这个数据的代价也不小。
4.buffer.WriteString()
func main() {
// 4.字符串连接--buffer.WriteString()方法
var buffer bytes.Buffer
buffer.WriteString("小罗")
buffer.WriteString(",")
buffer.WriteString("30")
fmt.Printf("buffer.String(): %v\n", buffer.String())
}
这个比较理想,可以当成可变字符使用,对内存的增长也有优化,如果能预估字符串的长度,还可以用
buffer.Grow()接口来设置 capacity。
2.7.3 转义字符
Go语言的字符串常见转义字符包括:回车、换行、单双引号、制表符等,如下所示:
| 转义字符 | 含义 |
|---|---|
\r |
回车符(返回行首) |
\n |
换行符(直接跳到下一行的同列位置) |
\t |
制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
反斜杠 |
func main() {
// 5.字符串转义字符
fmt.Print("Hello\tworld\n")
fmt.Print("\"c:\\test\\a\"")
}
// 输出结果: Hello world "c:\test\a"
2.7.4 字符串切片操作
Go 里字符串底层是 byte 切片,str[n] 取出的是 byte 类型(本质是uint8 数字):
%v 通用格式化:直接打印 byte 对应字符串的ASCII 码值(十进制数字)
%c 专门用来把数值转成对应字符打印出来(肉眼看到的文字/符号)
func main() {
str := "Hi,Golang!"
n := 3
m := 5
fmt.Println("字符串str的字节长度len(str):", len(str))
runeStr := []rune(str)
fmt.Println("字符串str的字符长度len([]runeStr):", len(runeStr))
fmt.Printf("str[n]: %v\n", str[n]) // 获取字符串索引位置为n的原始字节
fmt.Printf("str[n]: %c\n", str[n])
fmt.Printf("str[n:m]: %v\n", str[n:m]) // 截取字符串索引位置为n到m-1的字符串
fmt.Printf("str[n:]: %v\n", str[n:]) // 截取字符串索引位置为n到len(str)-1的字符串
fmt.Printf("str[:m]: %v\n", str[:m]) // 截取字符串索引位置为0到m-1的字符串
}
2.7.5 字符串长度
1.取字节长度(英文/数字正常,中文不准)--- len(str)
原理:Go 字符串是 UTF-8 字节流,len() 统计字节个数
英文/字母/数字:1个字符 = 1字节,长度正常
中文:1个汉字占 3字节,len() 结果会偏大
s1 := "你好go"
fmt.Println(len(s1)) // 输出8,不是4个字符
2.取字符个数(真正汉字、字母总个数)--- 用rune转切片:
s2 := "你好Go语言"
fmt.Println("字符串s2的字节长度len(s2):", len(s2))
runeSlice := []rune(s2)
fmt.Println("字符串s2的字符长度len([]runeSlice):", len(runeSlice))
3.简洁一行写法 length := len([]rune(s))
4.总结:
要字节长度(存文件、网络传输):用 len(str)
要实际字符个数(展示、排版、截取):用 len([]rune(str))
2.7.6 字符串常用方法
| 方法 | 介绍 |
|---|---|
| len(str) | 求长度 |
| +或fmt.Sprintf | 拼接字符串 |
| strings.Split | 分割 |
| strings.contains | 判断是否包含 |
| strings.HasPrefix,strings.HasSuffix | 前缀/后缀判断 |
| strings.Index(),strings.LastIndex() | 子串出现的位置 |
| strings.ToUpper(),strings.ToLower() | 大小写转换 |
| strings.Join(a[]string, sep string) | join操作 |
func main() {
// 字符串常用方法
Gostr := "Hello,Go语言!"
fmt.Printf("len(Gostr):%v\n", len(Gostr))
fmt.Printf("strings.Split(Gostr, \" \"): %v\n", strings.Split(Gostr, ",")) // 按逗号(,)分割
fmt.Printf("strings.Contains(Gostr, \"语言\"): %v\n", strings.Contains(Gostr, "语言")) // 是否包含语言
fmt.Printf("strings.HasPrefix(Gostr, \"Hello\"): %v\n", strings.HasPrefix(Gostr, "Hello")) // 前缀判断
fmt.Printf("strings.HasSuffix(Gostr, \"语言\"): %v\n", strings.HasSuffix(Gostr, "语言")) // 后缀判断
fmt.Printf("strings.Index(Gostr, \"Hello\"): %v\n", strings.Index(Gostr, "Hello")) // 子串出现的位置
fmt.Printf("strings.LastIndex(Gostr, \"Go\"): %v\n", strings.LastIndex(Gostr, "Go")) // 子串出现的位置
fmt.Printf("strings.ToLower(Gostr): %v\n", strings.ToLower(Gostr)) // 转换为小写
fmt.Printf("strings.ToUpper(Gostr): %v\n", strings.ToUpper(Gostr)) // 转换为大写
}
2.8 byte和rune类型
组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:
func main() {
// byte和rune类型
var t = '中'
var k = 'b'
fmt.Printf("t: %v,%c\n", t, t)
fmt.Printf("k: %v,%c\n", k, k)
}
Go 语言的字符有以下两种:
1.uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
2.rune类型,代表一个 UTF-8字符。
当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32。
Go使用了特殊的rune类型处理Unicode,让基于Unicode的文本处理更为方便,也可以使用byte型进行默认字符串处理,性能和扩展性都有照顾。
2.9 格式化输出
下面实例使用到的结构体:
// 快捷方式---tys
type webSite struct {
Name string
}
func main() {
// Golang的结构化输出
// 定义结构体变量
// var site = webSite{Name: "linuxtech.top"}
site := webSite{Name: "linuxtech.top"}
}
2.9.1 普通占位符
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %v | 相应值的默认格式 | fmt.Printf(“site: %v\n”, site) | site: {linuxtech.top} |
| %+v | “加号”标记(%+v)会添加字段名 | fmt.Printf(“site: %+v\n”, site) | site: {Name:linuxtech.top} |
| %#v | 相应值的Go语法表示 | fmt.Printf(“site: %#v\n”, site) | site: main.webSite{Name:”linuxtech.top”} |
| %T | 相应值的类型的Go语法表示 | fmt.Printf(“site: %T\n”, site) | site: main.webSite |
| %% | 字面上的百分号,并非值的占位符 | fmt.Printf(“site: %%\n”, site) | site: %% |
2.9.2 布尔占位符
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %t | 单词 true 或 false。 | fmt.Printf(“b: %t\n”, b) | b: true |
func main() {
b := true
fmt.Printf("b: %t\n", b)
}
2.9.3 整数占位符
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %b | 二进制表示 | fmt.Printf(“%b\n”, 5) | 101 |
| %c | 相应Unicode码点所表示的字符 | fmt.Printf(“%c\n”, 0x4E2D) | 中 |
| %d | 十进制表示 | fmt.Printf(“%d\n”, 0x12) | 18 |
| %o | 八进制表示 | fmt.Printf(“%o\n”, 10) | 12 |
| %q | 单引号围绕的字符字面值,由Go语法安全地转义 | fmt.Printf(“%q\n”, 0x4E2D) | ‘中’ |
| %x | 十六进制表示,字母形式为小写 a-f | fmt.Printf(“%x\n”, 13) | d |
| %X | 十六进制表示,字母形式为大写 A-F | fmt.Printf(“%x\n”, 13) | d |
| %U | Unicode格式:U+1234,等同于 “U+%04X” | fmt.Printf(“%U\n”, 0x4E2D) | U+4E2D |
2.9.4 实部和虚部
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %b | 无小数部分的,指数为二的幂的科学计数法, 与 strconv.FormatFloat的 ‘b’ 转换格式一致。例如 -123456p-78 |
fmt.Printf(“%b\n”, 10.2) | 5742089524897382p-49 |
| %e | 科学计数法,例如 -1234.456e+78 | fmt.Printf(“%e\n”, 10.2) | 1.020000e+01 |
| %E | 科学计数法,例如 -1234.456E+78 | fmt.Printf(“%E\n”, 10.2) | 1.020000E+01 |
| %f | 有小数点而无指数,例如 123.456 | fmt.Printf(“%f\n”, 10.2) | 10.200000 |
| %g | 根据情况选择 %e 或 %f 以产生更紧凑的(无末尾的0)输出 | fmt.Printf(“%g\n”, 10.20) | 10.2 |
| %G | 根据情况选择 %E 或 %f 以产生更紧凑的(无末尾的0)输出 | fmt.Printf(“%G\n”, 10.20+2i) | (10.2+2i) |
2.9.5 字符串与字节切片
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %s | 输出字符串表示(string类型或[]byte) | fmt.Printf(“%s\n”, []byte(“中国政府网”)) | 中国政府网 |
| %q | 双引号围绕的字符串,由Go语法安全地转义 | fmt.Printf(“%q\n”, “中国政府网”) | “中国政府网” |
| %x | 十六进制,小写字母,每字节两个字符 | fmt.Printf(“%x\n”, “中国政府网”) | e4b8ade59bbde694bfe5ba9ce7bd91 |
| %X | 十六进制,大写字母,每字节两个字符 | fmt.Printf(“%X\n”, “中国政府网”) | E4B8ADE59BBDE694BFE5BA9CE7BD91 |
2.9.6 指针
| 占位符 | 说明 | 举例 | 输出 |
|---|---|---|---|
| %p | 十六进制表示,前缀 0x | fmt.Printf(“%p”, &site) | 0x3ab4cdd1c030 |
2.10 运算符
Go语言内置的运算符有:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符
2.10.1 算术运算符
| 运算符 | 描述 |
|---|---|
| + | 相加 |
| - | 相减 |
| * | 相乘 |
| / | 相除 |
| % | 求余 |
++(自增)和--(自减)在Go语言中是单独的语句,并不是运算符。
func main() {
a := 100
b := 10
fmt.Printf("(a + b): %v\n", (a + b))
fmt.Printf("(a - b): %v\n", (a - b))
fmt.Printf("(a * b): %v\n", (a * b))
fmt.Printf("(a / b): %v\n", (a / b))
fmt.Printf("(a %% b): %v\n", (a % b))
a++
fmt.Printf("a: %v\n", a)
b--
fmt.Printf("b: %v\n", b)
}
2.10.2 关系运算符
| 运算符 | 描述 |
|---|---|
| == | 检查两个值是否相等,如果相等返回 True 否则返回 False。 |
| != | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 |
| > | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 |
| >= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 |
| < | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 |
| <= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 |
func main() {
m := 1
n := 2
fmt.Printf("(m > n): %v\n", (m > n))
fmt.Printf("(m < n): %v\n", (m < n))
fmt.Printf("(m >= n): %v\n", (m >= n))
fmt.Printf("(m <= n): %v\n", (m <= n))
fmt.Printf("(m == n): %v\n", (m == n))
fmt.Printf("(m != n): %v\n", (m != n))
}
2.10.3 逻辑运算符
| 运算符 | 描述 |
|---|---|
| && | 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False。 |
| \ | \ |
| ! | 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True。 |
func main() {
p := true
q := false
fmt.Printf("(p && q): %v\n", (p && q))
fmt.Printf("(p || q): %v\n", (p || q))
fmt.Printf("(!p): %v\n", (!p))
fmt.Printf("(!q): %v\n", (!q))
}
2.10.4 位运算符
位运算符对整数在内存中的二进制位进行操作。
| 运算符 | 描述 |
|---|---|
| & | 参与运算的两数各对应的二进位相与。 (两位均为1才为1) |
| \ | |
| ^ | 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 (两位不一样则为1) |
| << | 左移n位就是乘以2的n次方。 “a<<b”是把a的各二进位全部左移b位,高位丢弃,低位补0。 |
| >> | 右移n位就是除以2的n次方。 “a>>b”是把a的各二进位全部右移b位。 |
func main() {
a := 4 // 二进制 100
fmt.Printf("a: %b\n", a)
b := 8 // 二进制 1000
fmt.Printf("b: %b\n", b)
fmt.Printf("(a & b): %v, %b \n", (a & b), (a & b))
fmt.Printf("(a | b): %v, %b\n", (a | b), (a | b))
fmt.Printf("(a ^ b): %v, %b\n", (a ^ b), (a ^ b))
fmt.Printf("(a << 2): %v, %b\n", (a << 2), (a << 2))
fmt.Printf("(b >> 2): %v, %b\n", (b >> 2), (b >> 2))
}
2.10.5 赋值运算符
| 运算符 | 描述 |
|---|---|
| = | 简单的赋值运算符,将一个表达式的值赋给一个左值 |
| += | 相加后再赋值 |
| -= | 相减后再赋值 |
| *= | 相乘后再赋值 |
| /= | 相除后再赋值 |
| %= | 求余后再赋值 |
| <<= | 左移后赋值 |
| >>= | 右移后赋值 |
| &= | 按位与后赋值 |
| \ | = |
| ^= | 按位异或后赋值 |
func main() {
var a int
a = 100
fmt.Printf("a: %v\n", a)
a += 1 // a = a + 1
fmt.Printf("a: %v\n", a)
a -= 1 // a = a -1
fmt.Printf("a: %v\n", a)
a *= 2 // a = a * 2
fmt.Printf("a: %v\n", a)
a /= 2 // a = a / 2
fmt.Printf("a: %v\n", a)
}
3 流程控制语句
3.1 Go语言中的条件
条件语句是用来判断给定的条件是否满足(表达式值是否为true或者false),并根据判断的结果(真或假)决定执行的语句。
3.1.1 条件语句与循环语句
1.if 语句:if 语句由一个布尔表达式后紧跟一个或多个语句组成。
2.if…else 语句: if 语句后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。
3.if 嵌套语句: 在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。
4.switch 语句: switch 语句用于基于不同条件执行不同动作。
5.select 语句: select 语句类似于 switch 语句,但select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
6.for循环与for range循环:Go语言中的循环只有for循环,去除了while、do while循环,使用起来更加简洁。
3.1.2 流程控制关键字
break、continue、goto
3.2 if语句
Go语言中的if语句和其他语言中的类似,都是根据给定的条件表达式运算结果来,判断执行流程。
3.2.1 语法
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
func main() {
a := 1
b := 3
if a > b {
fmt.Println(a)
} else {
fmt.Println(b)
}
}
注意:在go语言中 布尔表达式不用使用括号。
3.2.2 使用提示
1.不能使用0或非0表示真假
func test4() {
var i = 1
if i { // 编译失败
fmt.Println("here")
}
fmt.Printf("程序运行结束")
}
2. 不需使用括号将条件包含起来
3. 大括号{}必须存在,即使只有一行语句
4. 左括号必须在if或else的同一行
5. 在if之后,条件语句之前,可以添加变量初始化语句,使用`;`进行分隔
3.2.3 实例
1.布尔值Flag判断
func main() {
Flag := true
if Flag {
fmt.Printf("Flag: %v\n", Flag)
}
fmt.Printf("程序运行结束")
}
// 程序运行结果: Flag: true 程序运行结束
2.判断是否成年
func main() {
age := 20
if age >= 18 {
fmt.Println("你已成年!")
} else {
fmt.Println("你还未成年!")
}
fmt.Printf("程序运行结束!")
}
// 程序运行结果:你已成年! 程序运行结束!
注明:初始变量可以声明在布尔表达式里面,注意作用域
func main() {
if age := 20; age >= 18 {
fmt.Println("你已成年!")
fmt.Printf("age: %v\n", age)
} else {
fmt.Println("你还未成年!")
fmt.Printf("age: %v\n", age)
}
fmt.Printf("程序运行结束!")
}
// 本例中的age不能在if_else区域外打印,即作用域已限制
3.3 if else语句
Go语言中的if else语句可以根据给定条件二选一。
3.3.1 语法
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
3.3.2 实例
1.比较两个数的大小
func f1() {
a := 1
b := 2
if a > b {
fmt.Printf("a: %v\n", a)
} else {
fmt.Printf("b: %v\n", b)
}
}
2.判断奇偶数
// 判断奇偶数
func f2() {
var num int
fmt.Println("请输入一个数字:")
fmt.Scan(&num)
if num%2 == 0 {
fmt.Print("num是偶数\n")
} else {
fmt.Print("num是奇数\n")
}
fmt.Print("num的值是:", num)
}
// 终端运行:go run .\test_if_else.go
3.判断是否成年
func f3() {
age := 20
if age >= 18 {
fmt.Println("你是成年人")
} else {
fmt.Println("你还未成年")
}
}
// 特殊写法,在if前面添加执行语句
func f4() {
if age := 20; age >= 18 {
fmt.Println("你是成年人")
} else {
fmt.Println("你还未成年")
}
}
3.4 接收用户输入
使用fmt语句中的Scan()方法
// Go语言接收用户输入
func main() {
var name string
var age int
var email string
fmt.Println("请输入name,age,email,用空格分隔")
fmt.Scan(&name,&age,&email)
fmt.Printf("name: %v\n", name)
fmt.Printf("age: %v\n", age)
fmt.Printf("email: %v\n", email)
}
3.5 if else if语句
Go语言if语句可以进行多重嵌套使用,进行多重判断。
3.5.1 语法
if 布尔表达式1 {
// do something
} else if 布尔表达式2 {
// do something else
}else {
// catch-all or default
}
3.5.2 实例
1.根据分数判断等级
func Score() {
if score := 90; score >= 60 && score <= 70 {
fmt.Println("C")
} else if score > 70 && score <= 90 {
fmt.Println("B")
} else {
fmt.Println("A")
}
fmt.Println("程序运行结束")
}
2.判断星期几
// 输入星期几的第一个字母来判断一下是星期几,如果第一个字母一样,则继续判断第二个字母
// Monday Tuesday Wednesday Thursday Friday Saturday Sunday
func Week() {
var char string
fmt.Println("请输入第一个字符:")
fmt.Scan(&char)
if char == "M" {
fmt.Println("Monday")
} else if char == "T" {
fmt.Println("请输入第二个字符:")
fmt.Scan(&char)
if char == "u" {
fmt.Println("Tuesday")
} else {
fmt.Println("Thursday")
}
} else if char == "W" {
fmt.Println("Wednesday")
} else if char == "F" {
fmt.Println("Friday")
} else if char == "S" {
fmt.Println("请输入第二个字符:")
fmt.Scan(&char)
if char == "a" {
fmt.Println("Saturday")
} else {
fmt.Println("Sunday")
}
}
}
3.6 嵌套if语句
Go语言if语句可以嵌套多级进行判断。
3.6.1 语法
if 布尔表达式 1 {
/* 在布尔表达式 1 为 true 时执行 */
if 布尔表达式 2 {
/* 在布尔表达式 2 为 true 时执行 */
}
}
3.6.2 实例
// a>b a>c ==> a
// b>a b>c ==> b else c
/* func value_Size() {
a, b, c := 100, 20, 45
if a > b {
if a > c {
fmt.Println("a最大")
}
} else {
// b > a
if b > c {
fmt.Println("b最大")
} else {
fmt.Println("c最大")
}
}
}
*/
func value_Size() {
a, b, c := 10, 20, 45
if a > b {
if a > c {
fmt.Println("a最大")
}
} else if b > a && b > c {
fmt.Println("b最大")
} else {
fmt.Println("c最大")
}
}
func aDult() {
// 判断男女生及年龄
gender := "男生"
age := 14
if gender == "男生" {
fmt.Println("男生")
if age >= 18 {
fmt.Println("成年男性")
} else {
fmt.Println("未成年男性")
}
} else {
fmt.Println("女生")
if age >= 18 {
fmt.Println("成年女性")
} else {
fmt.Println("未成年女性")
}
}
}
3.7 switch语句
Go语言中的switch语句,可以非常容易的判断多个值的情况。
3.7.1 语法
switch var1 {
case val1:
...
case val2:
...
default:
...
}
3.7.2 实例
func fun_switch() {
grade := "B"
switch grade {
case "A":
fmt.Println("优秀")
case "B":
fmt.Println("良好")
default:
fmt.Println("一般")
}
}
3.7.3 多条件匹配
Go语言switch语句,可以同时匹配多个条件,中间用逗号分隔,有其中一个匹配成功即可。
func Fun_switch() {
Day := 7
switch Day {
case 1, 2, 3, 4, 5:
fmt.Println("工作日")
case 6, 7:
fmt.Println("休息日")
default:
fmt.Println("非法输入......")
}
}
// case可以是条件表达式
func fun_Switch() {
score := 90
switch {
case score >= 90:
fmt.Println("享受假期")
case score >= 80 && score < 90:
fmt.Println("好好学习吧!")
default:
fmt.Println("玩命学习吧!")
}
}
// fallthrough可以可以执行满足条件的下一个case
func Fun_Switch() {
a := 800
switch a {
case 800:
fmt.Println("800")
fallthrough
case 900:
fmt.Println("900")
case 1000:
fmt.Println("1000")
default:
fmt.Println("other")
}
}
3.7.4 注意事项
1.支持多条件匹配
2.不同的 case 之间不使用 break 分隔,默认只会执行一个 case。
3.如果想要执行多个 case,需要使用 fallthrough 关键字,也可用 break 终止。
4.分支还可以使用表达式,例如:a>10.
3.8 for循环
Go语言中的for循环,只有for关键字,去除了像其他语言中的while和do while.
3.8.1 语法
for 初始语句;条件表达式;结束语句{
循环体语句
}
注意:for表达式不用加括号
3.8.2 实例
func for1() {
for i := 1; i <= 10; i++ {
fmt.Printf("i: %v\n", i)
}
}
// 初始条件,可以写到外面
func for2() {
k := 1
for ; k <= 10; k++ {
fmt.Printf("k: %v\n", k)
}
}
// 初始条件和结束条件都可以省略
func for3() {
m := 1 // 初始条件
for m <= 10 {
fmt.Printf("m: %v\n", m)
m++ // 结束条件
}
}
// 永真循环---类似其他语言的while循环
func for4() {
for {
fmt.Println("我一直在执行~")
}
}
for循环可以通过break、goto、return、panic语句强制退出循环。
3.9 for range循环
Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel)。 通过for range遍历的返回值有以下规律:
1.数组、切片、字符串返回索引和值。
2.map返回键和值。
3.通道(channel)只返回通道内的值。
3.9.1 循环数组
// 循环数组
// for i, v := range arr { // i索引 , v值 }
func for_range_array() {
// var array = [...]int{1, 2, 3, 4, 5} // ...表示省略数组长度
var array = [5]int{1, 2, 3, 4, 5}
for i, v := range array {
fmt.Printf("i: %d,v:%v\n", i, v)
}
}
3.9.2 循环字符串
// 循环字符串
// for i, v := range str { // i索引 , v值 }
func for_range_str() {
var str = "中国政府网,人民的战争,Linux学习"
for i, v := range str {
fmt.Printf("i: %d,v:%c\n", i, v) // %c--按字符输出
}
list := strings.Split(str, ",")
for i, v := range list {
fmt.Printf("i: %d,v:%s\n", i, v) // %s---输出字符串表示(string类型或[]byte)
}
}
3.9.3 循环切片
// 循环切片--切片是动态的数组
// for i, v := range slice { // i索引 , v值 }
func for_range_slice() {
var slice = []int{4, 6, 7, 9, 0, 56, 7, 9}
for i, v := range slice {
fmt.Printf("i: %d,v:%v\n", i, v)
}
}
3.9.4 循环map
// 循环map
// for i, v := range map { // k键 , v值 }
func for_range_map() {
m := make(map[string]string)
m["name"] = "小罗"
m["age"] = "30"
m["email"] = "Luovipyu@163.com"
m["site"] = "https://linuxtech.top/"
for k, v := range m {
fmt.Printf("k: %v,v:%v\n", k, v)
}
}
3.9.5 循环通道
// 循环通道
// for v := range ch { // 只有v,没有索引 }
// 发送数据到通道
func sendData(ch chan int) {
nums := []int{90, 89, 34, 70, 80, 20, 10, 30, 40}
for _, v := range nums {
ch <- v
}
// 发送完毕关闭通道
close(ch)
}
// 遍历通道,读取数据
func readData(ch chan int) {
// for range 遍历通道,只返回值
for v := range ch {
fmt.Println("通道值:", v)
}
fmt.Println("通道已关闭,循环结束")
}
func main() {
// 创建无缓冲通道
ch := make(chan int)
// 开启协程执行发送函数
go sendData(ch)
// 调用读取函数,range遍历通道
readData(ch)
}
3.10 流程控制关键字
3.10.1 break
break语句可以结束for、switch和select的代码块。
1.注意事项
1.单独在select中使用break和不使用break没有啥区别。
2.单独在表达式switch语句,并且没有fallthough,使用break和不使用break没有啥区别。
3.单独在表达式switch语句,并且有fallthough,使用break能够终止fallthough后面的case语句的执行。
4.带标签的break,可以跳出多层select/ switch作用域。让break更加灵活,写法更加简单灵活,不需要使用控制变量一层一层跳出循环,没有带break的只能跳出当前语句块。
2.实例
// 跳出for循环
func for_break() {
for i := 0; i < 10; i++ {
if i == 5 {
break // 退出循环
}
fmt.Printf("i: %v\n", i)
}
}
// 跳出switch循环
func switch_break() {
i := 2
switch i {
case 1:
fmt.Println("等于1")
break
case 2:
fmt.Println("等于2")
break
fallthrough
case 3:
fmt.Println("等于3")
break
default:
fmt.Println("不关心")
break
}
}
// 跳到标签处
func label_break() {
MY_LABEL:
for i := 0; i < 10; i++ {
fmt.Printf("i: %v\n", i)
if i >= 5 {
break MY_LABEL
}
fmt.Printf("i: %v\n", i)
}
fmt.Println("end...")
}
3.10.2 continue
continue只能用在循环中,在go中只能用在for循环中,它可以终止本次循环,进行下一次循环。
在 continue语句后添加标签时,表示开始标签对应的循环。
// 输出1到10之间的偶数
func even_number() {
for i := 0; i < 10; i++ {
if i%2 == 0 {
fmt.Printf("i: %v\n", i)
} else {
continue // 终止本次循环,进行下一次循环。
}
}
}
// 跳转到label
func label_even_number() {
MY_LABEL:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i == 2 && j == 2 {
continue MY_LABEL // 当i=1,j=2时,跳到第1个循环
}
fmt.Printf("i=%d,j=%d\n", i, j)
}
}
}
3.10.3 goto
goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。 例如双层嵌套的for循环要退出时:
// 跳转到指定标签
func label_goto() {
i := 1
if i >= 2 {
fmt.Println("other")
} else {
goto LABEL1
}
LABEL1:
fmt.Printf("next...\n")
}
// 跳出双重循环
func for_goto() {
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if i >= 2 && j >= 2 {
goto END
}
fmt.Printf("%v,%v\n", i, j)
}
}
END:
fmt.Println("跳出双重循环...")
}
4 数组
数组是相同数据类型的一组数据的集合。
数组定义后,长度不能修改,可以通过**下标(索引)**访问元素。
4.1 语法
var variable_name [SIZE] variable_type
参数说明:
1.variable_name:数组名称
2.SIZE:数组长度,必须是常量
3.variable_type:数组保存元素的类型
func Array() {
// 数组
var a1 [3]int // 定义一个int类型的数组a1,长度是3
var s1 [2]string // 定义一个字符串类型的数组s1,长度是2
var b [4]bool // 定义一个布尔类型的数组b,长度是4
fmt.Printf("a1: %T\n", a1)
fmt.Printf("s1: %T\n", s1)
fmt.Printf("b: %T\n", b)
fmt.Printf("a1: %v\n", a1)
fmt.Printf("s1: %v\n", s1)
fmt.Printf("b: %v\n", b)
}
// 运行结果: a1: [3]int s1: [2]string b: [4]bool a1: [0 0 0] s1: [ ] b: [false false false false]
// 从上面运行结果可以看出,数组和长度和元素类型共同组成了数组的类型。
4.2 初始化
初始化,就是给数组的元素赋值,没有初始化的数组,默认元素值都是零值,布尔类型是false,字符串是空字符串。
4.2.1 未初始化的数组
func uninitialized_Array() {
// 未初始化的数组
var a2 [3]int // 定义一个int类型的数组a2,长度是3
var s2 [2]string // 定义一个字符串类型的数组s2,长度是2
var b1 [2]bool
fmt.Printf("a2: %v\n", a2)
fmt.Printf("s2: %v\n", s2)
fmt.Printf("b: %v\n", b1)
}
// 运行结果: a2: [0 0 0] s2: [ ] b1: [false false]
4.2.2 初始化-使用列表
使用初始化列表,就是将值写在大括号里面。
func initialized_Array() {
// 使用初始化列表
var a3 = [5]int{1, 6, 7, 8, 90}
var s3 = [2]string{"小罗", "https://linuxtech.top/"}
var b2 = [2]bool{true, false}
fmt.Printf("a3: %v\n", a3)
fmt.Printf("s3: %v\n", s3)
fmt.Printf("b2: %v\n", b2)
a4 := [3]int{67, 98, 456} // 类型推断
fmt.Printf("a4: %v\n", a4)
}
// 运行结果: a3: [1 6 7 8 90] s3: [小罗 https://linuxtech.top/] b2: [true false] a4: [67 98 456]
4.2.3 初始化-省略数组长度
数组长度可以省略,使用...代替,根据初始化值的数量自动推断。
func nulen_Array() {
// 省略数组长度---初始化
var a4 = [...]int{34, 78, 67, 21}
var s4 = [...]string{"小罗", "https://linuxtech.top/"}
var b3 = [...]bool{true, false}
a5 := [...]int{7, 5, 6, 3, 2, 1, 0}
fmt.Printf("a4: %v\n", a4)
fmt.Printf("s4: %v\n", s4)
fmt.Printf("b3: %v\n", b3)
fmt.Printf("a5: %v\n", a5)
fmt.Printf("len(a4): %v\n", len(a4)) // 数组长度
}
// 运行结果: a4: [34 78 67 21] s4: [小罗 https://linuxtech.top/] b3: [true false] a5: [7 5 6 3 2 1 0] len(a4): 4
4.2.4 初始化-指定索引值
可通过指定所有的方式来初始化,未指定所有的默认未零值。
func indexValue_Array() {
// 初始化---指定索引值
var a6 = [...]int{0: 1, 2: 6}
var s5 = [...]string{1: "小罗", 2: "https://linuxtech.top/"}
var b4 = [...]bool{2: true, 6: false}
fmt.Printf("a6: %v\n", a6)
fmt.Printf("s5: %v\n", s5)
fmt.Printf("b4: %v\n", b4)
}
// 运行结果:a6: [1 0 6] s5: [ 小罗 https://linuxtech.top/] b4: [false false true false false false false]
4.3 修改数组
func edit_array() {
// 修改数组的元素-直接下标修改
var a7 = [...]int{78, 79, 80}
fmt.Printf("a7: %v\n", a7)
a7[1] = 90
fmt.Printf("a7: %v\n", a7)
}
4.4 访问数组元素
可通过下标的方式来访问数组元素。数组的最大下标为数组长度-1,大于这个下标会发生数组越界。
func access_index_array() {
var a [2]int // 数组长度是2,最大下标是1
a[0] = 100
a[1] = 200
fmt.Printf("a[0]: %v\n", a[0])
fmt.Printf("a[1]: %v\n", a[1])
// 修改a[0]、a[1]
a[0] = 1
a[1] = 2
fmt.Printf("a[0]: %v\n", a[0])
fmt.Printf("a[1]: %v\n", a[1])
// 数组长度越界 a[3]=1000
}
4.4.1 数组长度遍历数组
可根据数组长度,通过for循环的方式遍历数组,数组的长度可以使用len函数获得。
func len_array() {
// 根据数组的长度和下标遍历数组
a1 := [...]int{1, 5, 7, 0, 3, 4}
/*
fmt.Printf Go标准库的格式化打印函数,可以自定义输出文字格式。
"a1[%v]:%v\n" 格式化字符串
%v:Go里的通用占位符,会自动匹配变量的值格式来输出
第一个%v:用来填充下标i
第二个%v:用来填充数组元素a1[i]
i 循环里的索引(下标),从0一直递增到数组最大长度-1
a1[i] 通过下标i 访问数组a1里对应的元素(就是你刚才问的「访问索引」用法)
*/
for i := 0; i < len(a1); i++ { // i < len(a1) 等同于 i <= len(a1)-1
fmt.Printf("a1[%v]:%v\n", i, a1[i])
}
}
4.4.2 for range遍历数组
可以使用for range循环遍历数组,range返回数组下标和对应的值
func for_range_array() {
// 使用for range循环遍历数组
a2 := [...]int{2, 4, 6, 8, 10}
for i, v := range a2 {
fmt.Printf("a2[%v]: %v\n", i, v)
}
}
5 切片
数组是固定长度,可以容纳相同数据类型的元素的集合。当长度固定时,使用还是带来一些限制,比如:申请的长度太大浪费内存,太小又不够用。
切片是可变长度的数组,底层就是使用数组实现的,增加了自动扩容功能。
切片(Slice)是一个拥有相同类型元素的可变长度的序列。
5.1 语法
1.声明切片和声明数组类似,只要不添加长度就可以
var identifier []type
2.切片是引用类型,可以使用`make`函数创建切片
var slice1 []type = make([]type, len)
// 也可以简写为:
slice1 := make([]type, len)
3.也可以指定容量,其中capacity为可选参数。
make([]T, length, capacity)
注明:`len`是数组的长度,并且也是切片的初始长度。
func slice() {
// 切片示例-声明
// 这种声明的方式还没分配内存
var names []string
var numbers []int
fmt.Printf("names: %v\n", names)
fmt.Printf("numbers: %v\n", numbers)
}
func make_slice() {
// 通过make声明切片--make的方式已经分配了内存,只不过没赋值
var s = make([]int, 2)
fmt.Printf("s: %v\n", s)
}
5.2 切片的长度和容量
切片拥有自己的长度和容量,可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。
func len_cap_slice() {
// 切片的长度和容量
var Names = []string{"小罗", "https://linuxtech.top/"}
var Numbers = []int{4, 7, 9, 0}
fmt.Printf("len(Names): %d cap(Names):%d \n", len(Names), cap(Names))
fmt.Printf("len(Numbers): %d cap(Numbers):%d \n", len(Numbers), cap(Numbers))
var s1 = make([]string, 56, 78)
fmt.Printf("len(s1): %d cap(s1):%d \n", len(s1), cap(s1))
}
5.3 切片初始化
切片可以直接初始化,也可以使用数组初始化。
5.3.1 切片的切分
func slice() {
// 切片的切分
var s = []int{2, 6, 7, 8, 9, 0} // 索引: 0 1 2 3 4 5
s1 := s[0:3] // [)---前闭后开,不包含3
fmt.Printf("s1[0:3]: %v\n", s1)
s2 := s[3:]
fmt.Printf("s2[3:]: %v\n", s2)
s3 := s[2:5]
fmt.Printf("s3[2:5]: %v\n", s3)
s4 := s[:]
fmt.Printf("s4[:]: %v\n", s4)
}
5.3.2 直接初始化
func slice_init() {
// 直接初始化
s_init := []int{3, 6, 9, 0}
fmt.Printf("s_init: %v\n", s_init)
}
5.3.3 使用数组初始化
func slice_array_init() {
// 使用数组初始化
array_init := [...]int{2, 4, 6, 8, 10, 9}
slice_array_init1 := array_init[:3] // [2 4 6]
slice_array_init2 := array_init[:3] // [2 4 6]
slice_array_init_all := array_init[:] // 取数组中的所有元素
fmt.Printf("slice_array_init1: %v\n", slice_array_init1)
fmt.Printf("slice_array_init2: %v\n", slice_array_init2)
fmt.Printf("slice_array_init_all: %v\n", slice_array_init_all)
}
5.3.4 切片表达式
使用数组的部分元素初始化(切片表达式)
切片的底层就是一个数组,所以可以基于数组通过切片表达式得到切片。
切片表达式中的low和high表示一个索引范围(左包含,右不包含),得到的切片长度=high-low,容量等于得到切片的底层数组的容量。
func slice_init_array_element() {
// 使用数组的部分元素初始化(切片表达式)
Array := [...]int{34, 35, 36, 37, 38, 39}
s1_Array := Array[2:5]
fmt.Printf("s1_Array: %v\n", s1_Array)
s2_Array := Array[2:]
fmt.Printf("s2_Array: %v\n", s2_Array)
s3_Array := Array[:3]
fmt.Printf("s3_Array: %v\n", s3_Array)
}
5.4 空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0,容量为0.
func nil_slice() {
var nilSlice []int
fmt.Println(nilSlice == nil)
fmt.Printf("len(nilSlice):%d,cap(nilSlice):%d \n", len(nilSlice), cap(nilSlice))
}
// 运行结果: true len(nilSlice):0,cap(nilSlice):0
5.5 切片的遍历
切片的遍历和数组的遍历类似,可使用for循环索引遍历,或者for range循环。
5.5.1 for循环索引
func index_slice() {
// for循环索引遍历
indexSlice := []int{2, 4, 6, 8, 0} // var indexSlice = []int{2, 4, 6, 8, 0}
sliceLength := len(indexSlice)
/* for i := 0; i < sliceLength; i++ {
fmt.Printf("indexSlice[%v]:%v\n", i, indexSlice[i])
} */
for i := 0; i <= sliceLength-1; i++ { // 快捷键 fori...
fmt.Printf("indexSlice[%v]:%v\n", i, indexSlice[i]) // 通过下标取值---indexSlice[i]
}
}
5.5.2 for range循环
func range_slice() {
// for range循环遍历
rangeSlice := []int{1, 3, 5, 7, 9}
for i, v := range rangeSlice { // 快捷键forr...
fmt.Printf("rangeSlice[%v]:%v\n", i, v)
}
}
5.6 操作切片元素
切片是一个动态数组,可使用append()函数添加元素,Go语言中没有删除切片元素的专用方法,可以使用切片本身的特性删除元素。
切片是引用类型,通过赋值的方式,会修改原有内容,Go提供了copy()函数来拷贝切片。
切片可以模拟数据库的增删改查,即db CRUD
5.6.1 添加元素
func sliceAppend() {
// 添加元素
s_append := []int{}
s_append = append(s_append, 1)
s_append = append(s_append, 3)
s_append = append(s_append, 5, 7, 9) // 添加多个元素
fmt.Printf("s_append: %v\n", s_append)
s := []int{11, 13, 15}
s_append = append(s_append, s...)
fmt.Printf("s_append: %v\n", s_append) // 添加另外一个切片
}
5.6.2 删除元素
从切片Slice中删除索引为index的元素,操作方法是Slice = append(Slice[:index], Slice[index+1:]...)
核心原理:新建了一个切片,重新添加元素
func sliceDelete() {
// 删除元素
s_delete := []int{90, 91, 92, 93, 94, 95}
s_delete = append(s_delete[:2], s_delete[3:]...) // 删除索引为2的元素
fmt.Printf("s_delete: %v\n", s_delete)
}
5.6.3 修改元素
func modify_slice() {
// 修改元素
s_modify := []int{3, 5, 7, 0, 11}
s_modify[2] = 9
fmt.Printf("s_modify: %v\n", s_modify)
}
5.6.4 查询元素
func query_slice() {
// 查询元素
var s_query = []int{5, 8, 9, 12, 13, 45}
var key = 13
for i, v := range s_query {
if v == key {
fmt.Printf("s_query[%v]:%v\n", i, s_query[i])
fmt.Printf("i:%d,s_query[i]:%d\n", i, s_query[i])
}
}
}
5.6.5 拷贝切片
func copySlice() {
// 拷贝元素
s1 := []string{"小罗", "哈尔滨工业大学", "北京大学"}
s2 := s1 // 赋值时相当于是把内存地址给S2,所以修改s2时,s1也被修改了
s2[0] = "西安交通大学"
fmt.Printf("s1: %v\n", s1)
fmt.Printf("s2: %v\n", s2)
fmt.Println("---------------------")
s3 := []string{"小罗", "哈尔滨工业大学", "北京大学"}
s4 := make([]string, 3) // 类型为字符串,分类内存为3
copy(s4, s3) // copy后,再修改时,s3未被修改
s4[0] = "西北工业大学"
fmt.Printf("s3: %v\n", s3)
fmt.Printf("s4: %v\n", s4)
}
// 赋值的情况下,原来的变量被修改了,使用copy函数,原来的变量没有被修改。
6 Map
map是一种key:value键值对的数据结构容器。map内部实现的是哈希表(hash)。
map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
map是引用类型的。
6.1 语法
可使用内建函数make或map关键字定义map
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable = make(map[key_data_type]value_data_type)
参数说明:
1.map_variable:map变量名称
2.key_data_type:key的数据类型
3.value_data_type:值得数据类型
func Map() {
var Map map[string]string // 类型的声明
Map = make(map[string]string)
fmt.Printf("strMap: %v\n", Map)
fmt.Printf("strMap: %T\n", Map)
}
func strMap() {
var strMap1 = map[string]string{
"Name": "小罗",
"Age": "30",
"Email": "luovipyu@163.com",
"Site": "https://linuxtech.top/",
}
fmt.Printf("strMap1: %v\n", strMap1)
strMap2 := make(map[string]string)
strMap2["Name"] = "小罗"
strMap2["Age"] = "30"
strMap2["Email"] = "luovipyu@163.com"
strMap2["Site"] = "https://linuxtech.top/"
fmt.Printf("strMap2: %v\n", strMap2)
}
6.2 访问map
通过下标key获得其值。
func value_map() {
// 通过下标key获得其值
var valueMap = map[string]string{
"Name": "小罗",
"Age": "30",
"Email": "luovipyu@163.com",
"Site": "https://linuxtech.top/",
}
var key = "Age"
var value = valueMap[key]
fmt.Printf("value: %v\n", value)
}
6.3 判断键是否存在
Go语言中有判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
// 如果ok为`true`,存在;否则,不存在。
func key_map() {
// 判断键是否存在
keyMap := make(map[string]string)
keyMap["Name"] = "小罗"
keyMap["Age"] = "30"
keyMap["Email"] = "luovipyu@163.com"
keyMap["Site"] = "https://linuxtech.top/"
v, ok := keyMap["address"]
if ok {
fmt.Println("键存在")
fmt.Println(v)
} else {
fmt.Println("键不存在")
}
}
6.4 遍历map
可使用for range循环进行map遍历,得到key和value值。
6.4.1 遍历key
func iterate_key_map() {
// 遍历map---key
iterateKey := make(map[string]string)
iterateKey["name"] = "小明"
iterateKey["age"] = "23"
iterateKey["email"] = "xiaoming@163.com"
for key := range iterateKey {
fmt.Println(key)
}
}
6.4.2 遍历key和value
func iterate_key_value_map() {
// 遍历map---key和value
var iterateKeyValue = map[string]string{
"name": "小红",
"age": "21",
"school": "清华大学",
}
for key, value := range iterateKeyValue {
fmt.Println(key + ":" + value)
}
}
7 函数
7.1 简介
函数是Go语言中的一级公民,所有的功能单元都定义在函数中,可以重复使用。函数包含函数的名称、参数列表和返回值类型,这些构成了函数的签名(signature)。
7.2 特性
1.Go语言中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。
2.Go语言中不允许函数重载(overload),也就是说不允许函数同名。
3.Go语言中的函数不能嵌套函数,但可以嵌套匿名函数。
4.函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数。
5.函数可以作为参数传递给另一个函数。
6.函数的返回值可以是一个函数。
7.函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。
8.函数参数可以没有名称。
7.3 定义和调用
函数在使用之前必须先定义,可以调用函数来完成某个任务。
函数可以重复调用,从而达到代码重用。
7.3.1 定义
func function_name( [parameter list] ) [return_types]
{
函数体
}
语法解析:
func:函数由func开始声明function_name:函数名称,函数名和参数列表一起构成了函数签名。[parameter list]:参数列表,参数就像一个占位符,当函数被调用时,可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。return_types:返回类型,函数返回一列值。return_types是该列值的数据类型。有些功能不需要返回值,这种情况下return_types不是必须的。- 函数体:函数定义的代码集合。
package main
func sum(a int, b int) (reSum int) { // 形式参数--形参
reSum = a + b
return reSum // 这里也可只写return
}
func conpare(m int, n int) (max int) {
if m > n {
max = m
} else {
max = n
}
return max // 这里也可只写return
}
7.3.2 调用
当完成某个任务时,可以调用函数来完成。调用函数要传递参数,如何有返回值可以获得返回值。
func main() {
res := sum(3, 90) // 调用的时候是实参
println("计算结果:", res)
maxResult := conpare(-1, 7)
println("这两个数中比较大的数是:", maxResult)
}
7.4 返回值
函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过return关键字来指定。
return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。Go中的函数可以有多个返回值。
1.return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称。
2.当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值。
3.即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高。
4.命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义。
5.return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的。
func test1() {
// 既没参数,也没返回值
fmt.Println("我既没参数,也没返回值...")
}
func Sum(a int, b int) (ret int) {
// 有一个返回值
ret = a + b
return ret
}
func Sum2(m int, n int) int {
// 有一个返回值
// RET := m + n
return m + n
}
func test3() (name string, age int) {
// 多个返回值,且在return中指定返回的内容
name = "小罗"
age = 31
return name, age
}
func test4() (name string, age int) {
// 多个返回值,返回值名臣没有被使用
name = "小罗"
age = 31
return //等价于 return name, age
}
func test5() (name string, age int) {
// return覆盖命名返回值,返回值名称没有被使用
n := "小罗"
a := 31
return n, a
}
func test5() (string, int) {
// return覆盖命名返回值,返回值名称没有被使用
n := "小罗"
a := 31
return n, a
}
Go中经常使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如
return value,exists、return value,ok、return value,err等。
当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。
函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线
_丢弃这些返回值。例如下面的f1函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a。
func f1() (int, int) {
return 1, 2
}
func main() {
_, x := f1()
fmt.Printf("x: %v\\n", x)
}
7.5 参数
Go语言函数可以有0或多个参数,参数需要指定数据类型。
声明函数时的参数列表叫做形参,调用时传递的参数叫做实参。
Go语言是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。
Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。
// 形参列表
func f1(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
func f2(a int) {
a = 200
fmt.Printf("a: %v\n", a)
}
func main() {
// 实参列表
r := f1(6, 0)
fmt.Printf("r: %v\n", r)
// 演示参数传递,按值传递
x := 100
f2(x)
fmt.Printf("x: %v\n", x)
}
// 从运行结果可以看到,调用函数f2后,x的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。
map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。
func f3(s []int) {
s[0] = 900
}
func main() {
s := []int{1, 3, 4}
f3(s)
fmt.Printf("a: %v\n", s)
}
// 运行结果:s: [900 3 4]
从运行结果发现,调用函数后,slice内容被改变了。
// 可变参数---变长参数
func f4(args ...int) {
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func f5(name string, ok bool, args ...int) {
fmt.Printf("name: %v\n", name)
fmt.Printf("ok: %v\n", ok)
for _, v := range args {
fmt.Printf("v: %v\n", v)
}
}
func main() {
f4(3, 7, 8, 9, 0)
f5("小罗", true, 31, 173, 87)
}
7.6 类型与变量
可使用type关键字来定义一个函数类型,语法格式如下:
// 定义一个`fun`函数类型,它是一种函数类型,这种函数接收两个`int`类型的参数,并且返回一个`int`类型的返回值。
type fun func(int, int) int
定义一个fun函数类型,把sum和max赋值给它。
func Sum(a int, b int) int {
return a + b
}
func Max(m int, n int) int {
if m > n {
return m
} else {
return n
}
}
func main() {
type fun func(int, int) int
var funType fun
funType = Sum
NumberSum := funType(45, 5)
fmt.Printf("NumberSum: %v\n", NumberSum)
funType = Max
NumberMax := funType(45, 67)
fmt.Printf("NumberMax: %v\n", NumberMax)
}
7.7 高阶函数
Go语言的函数,可以作为函数的参数,传递给另外一个函数,作为另外一个函数的返回值返回。
7.7.1 函数作为参数
package main // 可执行程序,不是库
import "fmt" // 导入格式化输入输出包,用来打印内容
// 函数作为参数
// 定义一个普通函数sayHello,接收1个string类型参数name,用来打印Hello,+传入的名字
func sayHello(name string) {
fmt.Printf("Hello,%s\n", name)
}
func sayBye(name string) {
fmt.Printf("拜拜,%s", name)
}
// f1是高阶函数 两个参数:1.普通字符串name 2.一个函数,函数名叫f,这个函数要求:接收1个string参数,没有返回值
func f1(name string, f func(string)) {
f(name) // 函数体:调用传进来的函数f,并且把name传给它
}
// 调用函数
func main() {
f1("小罗", sayHello)
f1("小罗", sayBye)
}
7.7.2 函数作为返回值
// 声明当前包为主包,可独立运行
package main
// 导入格式化输出标准库
import "fmt"
// 定义加法函数:接收两个整型,返回相加结果
func numAdd(a int, b int) int {
addResult := a + b // 计算两数之和并赋值
return addResult // 返回计算结果
}
// 定义减法函数:接收两个整型,返回相减结果
func numSub(a int, b int) int {
subResult := a - b // 计算两数之差并赋值
return subResult // 返回计算结果
}
// 高阶函数:接收运算符字符串,返回【两个入参、一个返回值】的函数
func cal(operator string) func(int, int) int {
// 根据运算符匹配对应逻辑
switch operator {
case "+":
return numAdd // 匹配加号,返回加法函数
case "-":
return numSub // 匹配减号,返回减法函数
default:
return nil // 未知运算符,返回空
}
}
// 程序入口函数
func main() {
// 短变量声明:调用cal获取加法函数,赋值给funResult
funResult := cal("+")
// 调用获取到的加法函数,计算结果并声明变量存储
calResult := funResult(1, 2)
// 打印计算结果
fmt.Printf("calResult: %v\n", calResult)
// 变量重新赋值:funResult已声明,直接用=覆盖,获取减法函数
funResult = cal("-")
// 调用减法函数,重新给已有变量赋值
calResult = funResult(1, 2)
// 打印计算结果
fmt.Printf("calResult: %v\n", calResult)
}
7.8 匿名函数
Go语言函数不能嵌套,但是在函数内部可以定义匿名函数。
匿名函数就是没有名称的函数。
语法格式如下:
func (参数列表)(返回值)
匿名函数既可以没有参数,也可以没有返回值
/* func main() {
// 匿名函数
max := func(a int, b int) int {
if a > b {
return a
} else {
return b
}
}
i := max(1, 2)
fmt.Printf("i: %v\n", i)
} */
func test() {
name := "小罗 "
age := "31"
// 匿名函数在函数内部做一些运算
fun1 := func() string {
return name + age
}
msg := fun1()
fmt.Printf("msg: %v\n", msg)
}
func main() {
// 自己执行
func(a int, b int) {
max := 0
if a > b {
max = a
} else {
max = b
}
fmt.Printf("max: %v\n", max)
}(1, 2)
test()
}
8 闭包
闭包可以理解成定义在一个函数内部的函数。
在本质上,闭包是将函数内部和函数外部连接起来的桥梁。或者说是函数和其引用环境的组合体。
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境。
// 返回一个函数
func add() func(y int) int {
var x int
return func(y int) int {
x += y
return x
}
}
func main() {
var f = add()
r := f(10)
fmt.Printf("r: %v\n", r) // fmt.Println(f(10))
r = f(20)
fmt.Printf("r: %v\n", r)
r = f(30)
fmt.Printf("r: %v\n", r)
fmt.Println("------------")
f1 := add()
r = f1(100)
fmt.Printf("r: %v\n", r)
}
变量f是一个函数并且它引用了其外部作用域中的x变量,此时f就是一个闭包。 在f的生命周期内,变量x也一直有效。
闭包进阶示例1:
func add(x int) func(int) int {
return func(y int) int {
x += y
return x
}
}
func main() {
var f = add(10)
fmt.Println(f(10))
fmt.Println(f(20))
fmt.Println(f(30))
fmt.Println("----------")
f1 := add(20)
fmt.Println(f1(40))
fmt.Println(f1(50))
}
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string {
return func(name string) string {
if !strings.HasSuffix(name, suffix) {
return name + suffix
}
return name
}
}
func main() {
jpgFunc := makeSuffixFunc(".jpg")
txtFunc := makeSuffixFunc(".txt")
fmt.Println(jpgFunc("test"))
fmt.Println(txtFunc("test"))
}
闭包进阶示例3:
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
add, sub := calc(100)
r := add(100)
fmt.Printf("r: %v\n", r)
r = sub(50)
fmt.Printf("r: %v\n", r) // fmt.Println(f1(5), f2(6))
}
闭包其实并不复杂,只要牢记闭包=函数+引用环境。
9 递归
函数内部调用函数自身的函数称为递归函数。
使用递归函数最重要的三点:
1.递归就是自己调用自己。
2.必须先定义函数的退出条件,没有退出条件,递归将成为死循环。
3.Go语言递归函数很可能会产生一大堆的goroutine,也很可能会出现栈空间内存溢出问题。
// 阶乘
func f1() {
var s int = 1
// for循环实现阶乘
for i := 1; i <= 5; i++ { // 5的阶乘 5!=5x4x3x2x1
s *= i
}
fmt.Printf("s: %v\n", s)
}
func f2(a int) int {
if a == 1 {
// 1.退出条件
return 1
} else {
// 2.自己调用自己
return a * f2(a-1)
}
}
func main() {
a := 5
r := f2(a) // r := f2(5)
fmt.Printf("r: %v\n", r)
f1()
}
斐波那契数列:
// 斐波那契数列
// 计算公式:f(n)=f(n-1)+f(n-2)且f(2)=f(1)=1
func f3(n int) int {
// 退出点判断
if n == 1 || n == 2 {
return 1
}
// 递归表达式
return f3(n-1) + f3(n-2)
}
func main() {
result := f3(5)
fmt.Printf("result: %v\n", result)
}
10 defer语句
Go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。
10.1 defer特性
1.关键字 defer 用于注册延迟调用。
2.这些调用直到 return 前才被执。因此,可以用来做资源清理。
3.多个defer语句,按先进后出的方式执行。
4.defer语句中的变量,在defer声明时就决定了。
10.2 defer用途
1.关闭文件句柄
2.锁资源释放
3.数据库连接释放
10.3 defer语句实例
func fun_defer() {
fmt.Println("start...")
defer fmt.Println("step1...")
defer fmt.Println("step2...")
defer fmt.Println("step3...")
fmt.Println("end...")
}
func main() {
fun_defer()
}
// 运行结果: start... end... step3... step2... step1...
11 init函数
Golang有一个特殊的函数,它是init函数,先于main函数执行,实现包级别的一些初始化操作。
11.1 init函数主要特点
- init函数先于main函数自动执行,不能被其他函数调用;
- init函数没有输入参数、返回值;
- 每个包可以有多个init函数;
- 包的每个源文件也可以有多个init函数;
- 同一个包的init执行顺序,Golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
- 不同包的init函数按照包导入的依赖关系决定执行顺序。
11.2 Golang 初始化顺序
初始化顺序:变量初始化 –> init() –> main()
var a int = initVar()
// 快捷键---finit
func init() {
fmt.Println("init...")
}
func initVar() int {
fmt.Println("init var...")
return 100
}
func main() {
fmt.Println("main...")
}
// 运行结果:init var... init... main...
12 指针
Go语言中的函数传参都是值拷贝,当想要修改某个变量的时候,可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。
类型指针不能进行偏移和运算。
Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。
12.1 地址和类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。
Go语言中使用&字符放在变量前面对变量进行取地址操作。
Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。
12.2 语法
一个指针变量指向了一个值的内存地址。(声明了一个指针之后,可以像变量赋值一样,把一个值的内存地址放入到指针当中。)
类似于变量和常量,在使用指针前需要声明指针。指针声明格式如下:
var var_name *var-type
参数说明:
1.var-type:指针类型
2.var_name:指针变量名
3.*:指定变量是作为一个指针。
12.3 声明与使用
// 指针声明:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
func pointer() {
var ip *int // 声明指针变量
fmt.Printf("ip变量储存的指针地址: %v\n", ip) // ip: <nil> 空指针
fmt.Printf("ip变量的类型: %T\n", ip) // *int --- int类型的指针类型
var i int = 66 // 声明实际变量
ip = &i // 指针变量的存储地址
fmt.Printf("i变量的地址: %x\n", &i)
// 指针变量的存储地址
fmt.Printf("ip变量储存的指针地址: %v\n", ip)
// 使用指针访问值
fmt.Printf("ip变量的值: %d\n", *ip)
var sp *string
var s string = "小罗"
sp = &s
fmt.Printf("s变量的地址: %x\n", &s)
fmt.Printf("sp变量储存的指针地址: %v\n", sp)
fmt.Printf("sp变量的值: %v\n", *sp)
}
12.4 指向数组的指针
12.4.1 语法
var ptr [MAX]*int; 表示数组里面元素的类型是指针类型
12.4.2 实例
const MAX int = 3
func maxArry() {
pointerArry := []string{"美国", "加拿大", "墨西哥"} // 定义字符串切片
var ptr [MAX]*string // 定义一个指针数组,每个元素都是*string类型
fmt.Printf("ptr: %v\n", ptr) // 初始化打印
// 把每个字符串的地址赋值给指针数组
var i int
for i = 0; i < MAX; i++ {
ptr[i] = &pointerArry[i]
}
fmt.Printf("ptr: %v\n", ptr)
// 遍历指针数组,打印指针指向的字符串
for i = 0; i < MAX; i++ {
fmt.Printf("pointerArry[%d] = %s\n", i, *ptr[i])
}
}
13 类型定义和类型别名
13.1 类型定义
// 类型定义的语法: type NewType Type
// 类型定义---i为MyInt类型
func defineType() {
type MyInt int
var i MyInt
i = 500
fmt.Printf("i的类型是: %T\ni的值是:%v\n", i, i)
}
13.2 类型别名
// 类型别名的语法: type NewType = Type
// 类型别名---i还是int类型
func aliasType() {
type MyInt2 = int
var i MyInt2
i = 56
fmt.Printf("i的类型是: %T\ni的值是: %v\n", i, i)
}
13.3 区别
1.类型定义相当于定义了一个全新的类型,与之前的类型不同;类型别名并没有定义一个新的类型,而是使用一个别名来替换之前的类型
2.类型别名只会在代码中存在,在编译完成之后并不会存在该别名
3.类型别名和原来的类型是一致的,所以原来类型所拥有的方法,类型别名中也可以调用,但是如果是重新定义的一个类型,那么不可以调用之前的任何方法
14 结构体
Go语言没有面向对象的概念了,但是可以使用结构体来实现,面向对象编程的一些特性,例如:继承、封装、组合、多态等特性。
14.1 定义
结构体的定义和类型定义类似,只不过多了一个struct关键字,语法结构如下:
type struct_variable_type struct {
member definition;
member definition;
...
member definition;
}
参数说明:
1.`type`:结构体定义关键字
2.`struct_variable_type`:结构体类型名称
3.`struct`:结构体定义关键字
4.`member definition;`:成员定义
14.2 实例
定义一个人的结构体Person:
type Person struct {
id int
name string
age int
email string
}
// 以上定义一个Person结构体,有四个成员,来描述一个Person的信息。
// 同类型的可以合并到一行,例如:
type Person struct {
id, age int
name, email string
}
14.3 声明结构体变量
声明一个结构体变量和声明一个普通变量相同,例如:
// 自定义了一个类型
type Person struct {
id, age int
name, email string
}
type Customer struct {
id, age int
name, email string
}
func main() {
// 声明结构体变量
var Tom Person
fmt.Printf("Tom: %v\n", Tom)
// 声明结构体变量
kite := Customer{}
fmt.Printf("kite: %v\n", kite)
}
// 运行结果: Tom: {0 0 } kite: {0 0 }
// 结构体成员,在没有赋值之前都是零值。
14.4 访问结构体成员
可使用点运算符(.),来访问结构体成员,例如:
// 自定义了一个类型
type Customer struct {
id, age int
name, email string
}
func main() {
// 声明结构体变量
kite := Customer{}
kite.id = 001
kite.name = "kite"
kite.age = 31
kite.email = "kite@163.com"
fmt.Printf("kite: %v\n", kite)
fmt.Printf("kite.email: %v\n", kite.email)
}
// 运行结果如下:kite: {1 31 kite kite@163.com} kite.email: kite@163.com
14.5 匿名结构体
如果结构体是临时使用,可以不用起名字,直接使用,例如:
func dog() {
var Dog struct {
id, age int
name, address string
}
Dog.id = 999
Dog.age = 10
Dog.name = "毛毛"
Dog.address = "陕西省渭南市临渭区xx村"
fmt.Printf("Dog: %v\n", Dog)
}
func main() {
dog()
}
14.6 结构体初始化
未初始化的结构体,成员都是零值:int—0、float—0.0、bool—false、string—nil
type Person struct {
id, age int
name, email string
}
func main() {
pite := Person{}
fmt.Printf("pite: %v\n", pite)
}
// 运行结果:pite: {0 0 "" ""}
14.6.1 键值对对结构体初始化
type kv_struct struct {
id, age int
name, email string
}
func main() {
var Tom kv_struct
Tom = kv_struct{
id: 1,
name: "Tom",
age: 20,
email: "Tom@163.com",
}
fmt.Printf("Tom: %v\n", Tom)
}
// 运行结果:Tom: {1 20 Tom Tom@163.com}
14.6.2 值的列表初始化
type vlist_struct struct {
id, age int
name, email string
}
func main() {
John := vlist_struct{
1,
31,
"John",
"John@163.com",
}
fmt.Printf("John: %v\n", John)
}
// 运行结果:John: {1 31 John John@163.com}
注意:
- 必须初始化结构体的所有字段。
- 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
- 该方式不能和键值初始化方式混用。
14.6.3 部分成员初始化
用不到的成员,可以不进行初始化
type member_struct struct {
id, age int
name, email string
}
func main() {
Dave := member_struct{
id: 1,
name: "Dave",
}
fmt.Printf("Dave: %v\n", Dave)
}
// 运行结果:Dave: {1 0 Dave }
14.7 结构体指针
14.7.1 普通变量的指针
func variable_pointer() {
var name string
name = "pite"
fmt.Printf("name变量的值: %v\n", name)
fmt.Printf("name变量的地址: %x\n", &name)
var p_name *string // p_name 指针类型
p_name = &name // &name 取name地址
// 输出指针地址
fmt.Printf("p_name变量储存的指针地址: %v\n", p_name)
fmt.Printf("p_name变量的类型: %T\n", p_name)
// 输出指针指向的内容值
fmt.Printf("*p_name变量的值: %v\n", *p_name)
}
// 运行结果:name变量的值: pite name变量的地址: 35edf2708030 p_name变量储存的指针地址: 0x35edf2708030
// p_name变量的类型: *string *p_name变量的值: pite
14.7.2 Go的结构体指针
func struct_pointer() {
type Person struct {
id, age int
address string
}
John := Person{
189,
24,
"四川成都",
}
var p_person *Person
p_person = &John
fmt.Printf("John: %v\n", John)
fmt.Printf("&John: %p\n", &John)
fmt.Printf("p_person: %p\n", p_person)
fmt.Printf("*p_person: %v\n", *p_person)
}
14.7.3 创建结构体指针
可以使用new关键字创建结构体指针,还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。
func new_struct() {
type Person struct {
id int
name, address string
}
var p_Lihua = new(Person)
p_Lihua.id = 45
p_Lihua.name = "Lihua"
fmt.Printf("变量p_Lihua的值: %v\n", *p_Lihua)
fmt.Printf("变量p_Lihua存储的指针地址: %p\n", p_Lihua)
fmt.Printf("变量p_Lihua的类型: %T\n", p_Lihua)
}
// 从运行结果看p_Lihua为指针类型
14.7.4 访问结构体指针
访问结构体指针成员,也使用点运算符(.)。
func main() {
type Person struct {
id int
name string
}
var p_person = new(Person)
fmt.Printf("p_person: %T\n", p_person)
p_person.id = 1
p_person.name = "tom"
fmt.Printf("*p_person: %v\n", *p_person)
}
// 运行结果: p_person: *main.Person *p_person: {1 tom}
14.8 结构体作为函数参数
Go结构体可以像普通变量一样,作为函数的参数,传递给函数,这里分为两种情况:
1.直接传递结构体,这是一个副本(拷贝),在函数内部不会改变外面结构体内容。
2.传递结构体指针,这时在函数内部,能够改变外部结构体内容。
14.8.1 直接传递结构体
type Person struct {
id int
name string
}
// 值传递拷贝了一份副本
func showPerson(per Person) {
per.id = 98
per.name = "pite"
fmt.Printf("per: %v\n", per)
}
func main() {
Tom := Person{
id: 100,
name: "Tom",
}
fmt.Printf("Tom: %v\n", Tom)
fmt.Println("------------------")
showPerson(Tom)
fmt.Printf("Tom: %v\n", Tom)
}
// 运行结果:
Tom: {100 Tom}
------------------
per: {98 pite}
Tom: {100 Tom}
// 从运行结果可以看出,函数内部改变了结构体内容,函数外面并没有被改变。
14.8.2 传递结构体指针
type Person struct {
id int
name string
}
func showPerson2(per *Person) {
per.id = 102
per.name = "kite"
}
func main() {
Tom := Person{
id: 100,
name: "Tom",
}
per := &Tom
fmt.Printf("Tom: %v\n", Tom)
showPerson2(per)
fmt.Println("---------------")
fmt.Printf("per: %v\n", *per)
}
// 运行结果:
Tom: {100 Tom}
---------------
per: {102 kite}
// 从运行结果可以看到,调用函数后,参数被改变了。
14.9 嵌套结构体
Go语言没有面向对象编程思想,也没有继承关系,但是可以通过结构体嵌套来实现这种效果。
通过实例演示如何实现结构体嵌套,假如有一个人Person结构体,这个人还养了一个宠物Dog结构体。
// Dog结构体
type Dog struct {
name, color string
age int
}
// Person结构体
type Person struct {
MyDog Dog // Dog结构体类型的变量
name string
age int
}
func main() {
dog := Dog{
name: "毛毛",
age: 2,
color: "黄色",
}
per := Person{
MyDog: dog, // 这里的dog就是上面初始化时声明好的dog
name: "Tom",
age: 32,
}
fmt.Printf("per.MyDog.name: %v\n", per.MyDog.name)
fmt.Printf("per: %v\n", per)
}
15 方法
Go语言没有面向对象的特性,也没有类对象的概念。但是,可以使用结构体来模拟这些特性,我们都知道面向对象里面有类方法等概念。也可以声明一些方法,属于某个结构体。
15.1 语法
Go中的方法,是一种特殊的函数,定义于struct之上(与struct关联、绑定),被称为struct的接受者(receiver)。
通俗的讲,方法就是有接收者的函数。
语法格式如下:
type mytype struct{}
func (recv mytype) my_method(para) return_type {}
func (recv *mytype) my_method(para) return_type {}
// 参数说明:
1.`mytype`:定义一个结构体
2.`recv`:接受该方法的结构体(receiver)
3.`my_method`:方法名称
4.`para`:参数列表
5.`return_type`:返回值类型
// 从语法格式可以看出,方法和函数非常相似,多了一个接受类型。
15.2 实例
type Person struct {
name string
}
type Customer struct {
name string
}
// 属性和方法分开写
// (per Person)---接收者(receiver)
// eat是属于Person这个结构体的
// 方法是通过接收者与结构体关联在一起的
func (per Person) eat() {
fmt.Printf("%v,eat...", per.name)
}
func (per Person) sleep() {
fmt.Printf("%v,sleep...", per.name)
}
func (cust Customer) login(name string, pwd string) bool {
fmt.Printf("cust.name: %v\n", cust.name)
if name == "kite" && pwd == "123" {
return true
} else {
return false
}
}
func main() {
per_method := Person{
name: "kite",
}
per_method.eat()
per_method.sleep()
cus := Customer{
name: "kite",
}
b := cus.login("kite", "123")
fmt.Printf("b: %v\n", b)
}
15.3 注意事项
1.方法的receiver type并非一定要是struct类型,type定义的类型别名、slice、map、channel、func类型等都可以。
2.struct结合它的方法就等价于面向对象中的类。只不过struct可以和它的方法分开,并非一定要属于同一个文件,但必须属于同一个包。
3.方法有两种接收类型:(T Type)和(T *Type),它们之间有区别。
4.方法就是函数,所以Go中没有方法重载(overload)的说法,也就是说同一个类型中的所有方法名必须都唯一。
5.如果receiver是一个指针类型,则会自动解除引用。
6.方法和type是分开的,意味着实例的行为(behavior)和数据存储(field)是分开的,但是它们通过receiver建立起关联关系。
15.4 结构体类型
结构体有值类型和指针类型,方法的接收者是结构体,那么也有值类型和指针类型。区别就是接收者是否复制结构体副本。值类型复制,指针类型不复制。
type Person struct {
name string
}
func test1() {
p1 := Person{
name: "Tom",
}
p2 := &Person{
name: "Tom",
}
fmt.Printf("p1: %T\n", p1)
fmt.Printf("p2: %T\n", p2)
}
// 运行结果:p1: main.Person p2: *main.Person
// 从运行结果,可以看出p1是值类型,p2是指针类型。
15.4.1 值类型结构体
type Person struct {
name string
}
func showPerson1(per Person) {
per.name = "Tom..."
}
func test2() {
p1 := Person{
name: "Tom",
}
showPerson1(p1)
fmt.Printf("p1: %v\n", p1)
}
// 运行结果:p1: {Tom}
// 从运行结果看,p1是值传递,拷贝了副本,地址发生了改变
15.4.2 指针类型结构体
type Person struct {
name string
}
func showPerson2(per *Person) {
// per.name 自动解引用
// (*per).name = "Tom..."
per.name = "Tom..."
}
func test2() {
p2 := &Person{
name: "Tom",
}
fmt.Println("------------------")
showPerson2(p2)
fmt.Printf("p2: %v\n", *p2)
}
// 运行结果:p2: {Tom...}
// 从运行结果看,p2是指针类型,地址没有改变。
15.5 方法接收者类型
结构体有值类型和指针类型,方法的接收者是结构体,那么也有值类型和指针类型。区别就是接收者是否复制结构体副本。值类型复制,指针类型不复制。
15.5.1 方法的值类型接收者
值类型和指针类型接收者,本质上和函数传参道理相同。
type Person struct {
name string
}
func (per Person) showPerson3() {
per.name = "Tom..."
}
func test3() {
p1 := Person{
name: "Tom",
}
p1.showPerson3()
fmt.Printf("p1: %v\n", p1)
}
// 运行结果:p1: {Tom}
// 从运行结果看,p1是值传递,拷贝了副本,地址发生了改变。
15.5.2 方法的指针类型接收者
值类型和指针类型接收者,本质上和函数传参道理相同。
type Person struct {
name string
}
func (per *Person) showPerson4() {
// per.name 自动解引用
// (*per).name = "Tom..."
per.name = "Tom..."
}
func test3() {
p2 := &Person{
name: "Tom",
}
p2.showPerson4()
fmt.Printf("p2: %v\n", *p2)
}
// 运行结果:p2: {Tom...}
// 从运行结果看,p2是指针类型,地址没有改变。
16 接口
接口像是一个公司里面的领导,会定义一些通用规范,只设计规范,而不实现规范。
Go语言的接口,是一种新的类型定义,把所有具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
语法格式和方法非常类似。
16.1 语法
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* 定义结构体 */
type struct_name struct {
/* variables */
}
/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {
/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* 方法实现*/
}
在接口定义中定义,若干个空方法。这些方法都具有通用性。
16.2 实例
定义一个USB接口,有读(read)和写(write)两个方法,再定义一个电脑Computer和一个手机Mobile来实现这个接口。
// 系统中有一些接口,如:writer、Reader...
// USB接口
type USB interface {
read()
write()
}
// Computer结构体
type Computer struct {
name string
}
// Mobile结构体
type Mobile struct {
model string
}
// Computer实现USB接口的方法
func (c Computer) read() {
fmt.Printf("c.name: %v\n", c.name)
fmt.Println("Computer read...")
}
func (c Computer) write() {
fmt.Printf("c.name: %v\n", c.name)
fmt.Println("Computer wtite...")
}
// Mobile实现USB接口的方法
func (m Mobile) read() {
fmt.Printf("m.model: %v\n", m.model)
fmt.Println("Mobile read...")
}
func (m Mobile) wtite() {
fmt.Printf("m.model: %v\n", m.model)
fmt.Println("Mobile write...")
}
// 测试
func main() {
c_windows := Computer{
name: "联想",
}
c_windows.read()
c_windows.write()
m_mobile := Mobile{
model: "华为Nova 15",
}
m_mobile.read()
m_mobile.wtite()
}
16.3 接口注意事项
实现接口必须实现接口中的所有方法。
定义一个OpenClose接口,里面有两个方法open和close,定义一个Door结构体,实现其中一个方法。
type OpenClose interface {
open()
close()
}
type Door struct {
}
func (d Door) open() {
fmt.Println("open door...")
}
func main() {
var oc OpenClose
oc = Door{} // 这里编译错误,提示只实现了一个接口
}
16.4 接口接收者类型
16.4.1 值类型接收者
本质上和方法的值类型接收者的思考方法是一样的,值接收者是一个拷贝,是一个副本。
// 定义一个Pet接口
type Pet interface {
eat(string) string
}
// 定义一个Dog结构体
type Dog struct {
name string
}
// 实现Pet接口--接收者是值类型
func (MyDog Dog) eat(name string) string {
fmt.Printf("&MyDog: %p\n", &MyDog)
MyDog.name = "毛毛..."
fmt.Printf("name: %v\n", name)
return "吃的很好呦!"
}
func main() {
dog := Dog{
name: "毛毛",
}
fmt.Printf("dog: %p\n", &dog)
foodDog := dog.eat("火鸡")
fmt.Printf("foodDog: %v\n", foodDog)
fmt.Printf("dog: %v\n", dog)
}
// 运行结果: dog: 0x3400b03d0070 &MyDog: 0x3400b03d0090 name: 火鸡 foodDog: 吃的很好呦! dog: {毛毛}
// 从运行结果,我们看出dog的地址变了,说明是复制了一份,dog的name没有变说明,外面的dog变量没有被改变。
16.4.2 指针类型接收者
本质上和方法的指针类型接收者的思考方法是一样的,指针接收者,传递的是指针。
// 定义一个Pet接口
type Pet interface {
eat(string) string
}
// 定义一个Dog结构体
type Dog struct {
name string
}
// 实现Pet接口--接收者是指针类型
func (MyDog *Dog) eat(name string) string {
fmt.Printf("&MyDog: %p\n", MyDog)
MyDog.name = "毛毛..."
fmt.Printf("name: %v\n", name)
return "吃的很好呦!"
}
func main() {
dog := &Dog{
name: "毛毛",
}
foodDog := dog.eat("火鸡")
fmt.Printf("foodDog: %v\n", foodDog)
fmt.Printf("dog: %p\n", dog)
fmt.Printf("dog: %v\n", *dog)
}
// 运行结果: &MyDog: 0xf6061cba070 foodDog: 吃的很好呦! dog: 0xf6061cba070 dog: {毛毛...}
// 从运行结果,dog是指针类型,地址没有改变。
16.5 接口和类型的关系
1.一个类型可以实现多个接口
2.多个类型可以实现同一个接口(多态)
16.5.1 一个类型实现多个接口
一个类型实现多个接口,例如:有一个Player接口可以播放音乐,有一个Video接口可以播放视频,一个手机Mobile实现这两个接口,既可以播放音乐,又可以播放视频。
// 定义一个Player接口
type Player interface {
PlayMusic()
}
// 定义一个Video接口
type Video interface {
PlayVideo()
}
// 定义Mobile结构体
type Mobile struct {
model string
}
// 实现两个接口
func (m Mobile) PlayMusic() {
fmt.Printf("m.model: %v\n", m.model)
fmt.Println("播放音乐...")
}
func (m Mobile) PlayVideo() {
fmt.Printf("m.model: %v\n", m.model)
fmt.Println("播放视频...")
}
// 测试
func main() {
Phone := Mobile{
model: "华为手机",
}
Phone.PlayMusic()
Phone.PlayVideo()
}
16.5.2 多个类型实现一个接口
比如,一个宠物接口Pet,猫类型Cat和狗类型Dog都可以实现该接口,都可以把猫和狗当宠物类型对待,这在其他语言中叫做多态。
// 定义一个Pet接口
type Pet interface {
eat()
}
// 定义一个Dog结构体
type Dog struct {
name string
}
// 定义一个Cat结构体
type Cat struct {
name string
}
// 实现接口
func (dog Dog) eat() {
fmt.Printf("dog.name: %v\n", dog.name)
fmt.Println("dog eat...")
}
func (cat Cat) eat() {
fmt.Printf("cat.name: %v\n", cat.name)
fmt.Println("cat eat...")
}
// 测试
func main() {
// myDog := Dog{
// name: "毛毛",
// }
// MyCat := Cat{
// name: "花花",
// }
// myDog.eat()
// MyCat.eat()
var MyPet Pet
MyPet = Dog{
name: "毛毛",
}
MyPet.eat()
MyPet = Cat{
name: "花花",
}
MyPet.eat()
}
16.6 接口嵌套
接口可以通过嵌套创建新的接口。例如:飞鱼,既可以飞,又可以游泳。创建一个飞Flyer接口,创建一个游泳接口Swimmer,飞鱼接口(FlyFish)由这两个接口组成。
// 创建Flyer接口
type Flyer interface {
fly()
}
// 创建Swimmer接口
type Swimmer interface {
swim()
}
// 组合一个接口FlyFish
type FlyFish interface {
Flyer
Swimmer
}
// 创建一个结构体Fish
type Fish struct {
name string
}
// 实现组合接口
func (fish Fish) fly() {
fmt.Printf("fish.name: %v\n", fish.name)
fmt.Println("fly...")
}
func (fish Fish) swim() {
fmt.Printf("fish.name: %v\n", fish.name)
fmt.Println("swim...")
}
// 测试
func main() {
var BigFish FlyFish
BigFish = Fish{
name: "中华鲟",
}
BigFish.fly()
BigFish.swim()
}
16.7 通过接口实现OCP设计原则
面向对象可复用设计的第一块基石,便是所谓的”开-闭“原则(Open-Closed Principle,常缩写为OCP)。虽然,Go不是面向对象语言,但也可以模拟实现这个原则。对扩展是开放的,对修改是关闭的。
通过一个人养宠物的例子,来解释OCP设计原则。
// 定义一个宠物接口Pet
type Pet interface {
eat()
sleep()
}
// 定义一个Dog结构体
type Dog struct {
name string
age int
}
// Dog实现Pet接口
func (MyDog Dog) eat() {
fmt.Println("MyDog eat...")
}
func (MyDog Dog) sleep() {
fmt.Println("MyDog sleep...")
}
// 定义一个Cat结构体
type Cat struct {
name string
age int
}
// Cat实现Pet接口
func (MyCat Cat) eat() {
fmt.Println("MyCat eat...")
}
func (MyCat Cat) sleep() {
fmt.Println("MyCat sleep...")
}
// 定义一个Person结构体
type Person struct {
name string
}
// 为Person添加一个养宠物方法
// pet既可以传递Dog,也可以传递Cat
func (per Person) care(pet Pet) {
pet.eat()
pet.sleep()
}
func main() {
dog := Dog{
name: "毛毛",
age: 2,
}
cat := Cat{
name: "小花",
age: 3,
}
person := Person{
name: "小李",
}
person.care(dog)
person.care(cat)
}
使用接口的这种设计方法,可以很好的解耦合代码,实现软件设计的OCP原则(即开闭原则),这样设计,如果再添加一个宠物,例如:一个鸟Bird,原有的代码不用修改,直接添加就可以。
17 继承
Golang本质上没有oop的概念,也没有继承的概念,但是可以通过结构体嵌套实现这个特性。
type Animal struct {
name string
age int
}
func (a Animal) eat() {
fmt.Println("eat...")
}
func (a Animal) sleep() {
fmt.Println("sleep...")
}
type Dog struct {
Animal // 可理解为继承
color string
}
type Cat struct {
Animal
weight string
}
func main() {
dog := Dog{
Animal: Animal{
name: "毛毛",
age: 2,
},
color: "黄色",
}
dog.eat()
dog.sleep()
fmt.Printf("dog.color: %v\n", dog.color)
fmt.Printf("dog.age: %v\n", dog.age)
cat := Cat{
Animal: Animal{
name: "花花",
age: 4,
},
weight: "13公斤",
}
cat.eat()
cat.sleep()
fmt.Printf("cat.weight: %v\n", cat.weight)
}
17.1 模拟OOP的属性和方法
Golang没有面向对象和封装的概念,但可以通过结构体struct和函数绑定来实现OOP的属性和方法等特性。接收者 receiver 方法。
例如,想要定义一个Person类,有name和age属性,有eat/sleep/work方法。
type Person struct {
name string
age int
}
func (per Person) eat() {
fmt.Println("eat...")
}
func (per Person) sleep() {
fmt.Println("sleep...")
}
func (per Person) work() {
fmt.Println("work...")
}
func main() {
per := Person{
name: "tom",
age: 20,
}
fmt.Printf("per: %v\n", per)
per.eat()
per.sleep()
per.work()
}
18 构造函数
Golang没有构造函数的概念,可以使用函数来模拟构造函数的的功能。
type Person struct {
name string
age int
}
func NewPerson(name string, age int) (*Person, error) {
if name == "" {
return nil, fmt.Errorf("name不能为空")
}
if age < 0 {
return nil, fmt.Errorf("age不能小于0")
}
return &Person{name: name, age: age}, nil
}
func main() {
per, err := NewPerson("Tom", 34)
if err == nil {
fmt.Printf("per: %v\n", *per)
} else {
fmt.Printf("err: %v\n", err)
}
}
19 包
包可以区分命令空间(一个文件夹中不能有两个同名文件),也可以更好的管理项目。
Go中创建一个包,一般是创建一个文件夹,在该文件夹里面的go文件中,使用package关键字声明包名称,通常,文件夹名称和包名称相同。并且,同一个文件下面只有一个包
19.1 创建包
1.创建一个名为dao的文件夹。
2.创建一个dao.go文件。
3.在该文件中声明包。
package dao
import "fmt"
func Test1() {
fmt.Println("test package")
}
19.2 导入包
要使用某个包下面的变量或者方法,需要导入该包,导入包时,要导入从GOPATH开始的包路径,例如,在service.go中导入dao包
package main
import "dao"
func main() {
dao.Test1()
}
19.3 包注意事项
- 一个文件夹下只能有一个package
import后面的其实是GOPATH开始的相对目录路径,包括最后一段。但由于一个目录下只能有一个package,所以import一个路径就等于是import了这个路径下的包。- 注意,这里指的是“直接包含”的go文件。如果有子目录,那么子目录的父目录是完全两个包。
- 比如你实现了一个计算器package,名叫
calc,位于calc目录下;但又想给别人一个使用范例,于是在calc下可以建个example子目录(calc/example/),这个子目录里有个example.go(calc/example/example.go)。此时,example.go可以是main包,里面还可以有个main函数。 - 一个package的文件不能在多个文件夹下
- 如果多个文件夹下有重名的package,它们其实是彼此无关的package。
- 如果一个go文件需要同时使用不同目录下的同名package,需要在
import这些目录时为每个目录指定一个package的别名。
19.4 包管理工具go module
19.4.1 简介
go module 是 golang 1.11 新加的特性,用来管理模块中包的依赖关系。
19.4.2 使用方法
1.初始化模块
go mod init <项目模块名称>
2.依赖关系处理 ,根据go.mod文件
go mod tidy
3.将依赖包复制到项目下的 vendor目录。
go mod vendor
如果包被屏蔽(墙),可以使用这个命令,随后使用go build -mod=vendor编译
4.显示依赖关系
go list -m all
5.显示详细依赖关系
go list -m -json all
6.下载依赖
go mod download [path@version]
[path@version]是非必写的
19.5 实例
项目名称:Go_Project
项目结构如下:

1.初始化:go mod init luovip/Go_Project
PS D:\Go_Project\service> go mod init luovip/Go_Project

2.执行go build将某个包编译到缓存中,这样其他包就可以搜索到
PS D:\Go_Project\service> go build

3.在main.go中导入service包
package main
import (
"fmt"
"luovip/Go_Project/service" // 导入service包
)
func main() {
fmt.Println("Hello World!")
service.TestUserService() // 调用
service.TestCustomerService() // 调用
}
4.调用远程包
网址和路径:go.dev—Packages
假如要使用gin包,可以直接搜索,按文档说明导入即可。在终端执行go mod tidy 下载依赖后再执行go run main.go。
在go.mod中会生成相关的依赖:

20 并发编程
20.1 协程
Golang 中的并发是函数相互独立运行的能力。
Goroutines 是并发运行的函数。
Golang 提供了 Goroutines 作为并发处理操作的一种方式。
创建一个协程非常简单,就是在一个任务函数前面添加一个go关键字:
go task()
实例1:
func show(msg string) {
for i := 0; i < 5; i++ {
fmt.Printf("msg:%v\n", msg)
time.Sleep(time.Millisecond * 100) // 每运行一次,睡眠100毫秒
}
}
func main() {
// 加go关键字相当于指定给任务重新启动了一个协程,同时main也有自己的协程,所以出现交替的效果,给用户的体验是程序没有阻塞,一直在运行
// 不加go就是单线程执行,加上go就是三个协程
// 在main协程中执行,如果它前面也添加go,程序没有输出
go show("java") // 加go关键字,相当于重新启动了一个协程来执行
go show("Golang")
time.Sleep(time.Millisecond * 2000)
fmt.Println("end...") // 主函数退出,程序就结束了,所以需要添加sleep等待时间再执行
}
查看 go 关键字去掉的运行效果
实例2:
// responseSize获取url响应体字节长度,返回长度与错误
func responseSize(url string) (int, error) {
fmt.Println("Step1: ", url)
resp, err := http.Get(url)
if err != nil {
return 0, fmt.Errorf("发起http请求失败:%w", err)
}
// 拿到resp后立即注册defer,防止忘记关闭body造成连接泄露
defer func() {
closeErr := resp.Body.Close()
if closeErr != nil {
log.Printf("关闭响应体异常:%v", closeErr)
}
}()
fmt.Println("Step2: ", url)
//fmt.Println("Step3: ", url)
body, err := io.ReadAll(resp.Body)
if err != nil {
return 0, fmt.Errorf("读取响应体失败:%w", err)
}
fmt.Println("Step3: ", len(body))
return len(body), nil
}
func main() {
go func() {
size, err := responseSize("https://linuxtech.top/")
if err != nil {
log.Printf("linuxtech请求出错:%v", err)
} else {
fmt.Printf("linuxtech页面长度:%d\n", size)
}
}()
go func() {
size, err := responseSize("https://jd.com")
if err != nil {
log.Printf("jd请求出错:%v", err)
} else {
fmt.Printf("jd页面长度:%d\n", size)
}
}()
time.Sleep(10 * time.Second)
fmt.Println("等待结束,程序退出!")
}
20.2 通道channel
Go 提供了一种称为通道的机制,用于在 goroutine(协程) 之间共享数据。当作为 goroutine 执行并发活动时,需要在 goroutine 之间共享资源或数据,通道充当 goroutine 之间的管道并提供一种机制来保证同步交换。
需要在声明通道时指定数据类型。
可以共享内置、命名、结构和引用类型的值和指针。
数据在通道上传递:在任何给定时间只有一个 goroutine 可以访问数据项:因此按照设计不会发生数据竞争。
根据数据交换的行为,有两种类型的通道:无缓冲通道和缓冲通道。无缓冲通道用于执行 goroutine 之间的同步通信,而缓冲通道用于执行异步通信。无缓冲通道保证在发送和接收发生的瞬间执行两个 goroutine 之间的交换。缓冲通道没有这样的保证。
20.2.1 无缓冲通道
通道由 make 函数创建,该函数指定 chan 关键字和通道的元素类型。
在无缓冲通道中,在接收到任何值之前没有能力保存它。在这种类型的通道中,发送和接收 goroutine 在任何发送或接收操作完成之前的同一时刻都准备就绪。如果两个 goroutine 没有在同一时刻准备好,则通道会让执行其各自发送或接收操作的 goroutine 首先等待。同步是通道上发送和接收之间交互的基础。没有另一个就不可能发生。
1.创建无缓冲通道:
// 无缓冲通道:make只传通道类型,缓冲区长度为0
Unbuffered := make(chan int) // 整型无缓冲通道
参数说明:
1.使用内置函数`make`创建无缓冲通道。
2.`make`的第一个参数需要关键字`chan`,然后是通道允许交换的数据类型。
2.将值发送到通道:
func main() {
// 无缓冲通道:make 只传通道类型,缓冲区长度为0
goroutine1 := make(chan string)
// 新开goroutine接收,否则直接发送会阻塞报错
go func() {
val := <-goroutine1
fmt.Println(val)
}()
// 向无缓冲通道发送数据
goroutine1 <- "Australia"
close(goroutine1)
}
goroutine1 := make(chan string, 5) // 字符串缓冲通道
goroutine1 <- "Australia" // 通过通道发送字符串
3.从通道接收值:
func main() {
// 无缓冲通道:make 只传通道类型,缓冲区长度为0
goroutine1 := make(chan string)
// 新开goroutine接收,否则直接发送会阻塞报错
go func() {
val := <-goroutine1
fmt.Println(val)
}()
// 向无缓冲通道发送数据
goroutine1 <- "Australia"
close(goroutine1)
}
goroutine1 := make(chan string, 5) // 字符串缓冲通道
goroutine1 <- "Australia" // 通过通道发送字符串
20.2.2 缓冲通道
通道由 make 函数创建,该函数指定 chan 关键字和通道的元素类型。
在缓冲通道中,有能力在接收到一个或多个值之前保存它们。在这种类型的通道中,不要强制 goroutine 在同一时刻准备好执行发送和接收。当发送或接收阻塞时也有不同的条件。只有当通道中没有要接收的值时,接收才会阻塞。仅当没有可用缓冲区来放置正在发送的值时,发送才会阻塞。
1.创建缓冲通道:
buffered := make(chan int, 10) // 整型有缓冲通道
参数说明:
1.使用内置函数`make`创建无缓冲通道。
2.`make`的第一个参数需要关键字`chan`,然后是通道允许交换的数据类型。
2.将值发送到通道:将值发送到通道的代码块需要使用 <- 运算符。
// 一个包含 5 个值的缓冲区的字符串类型的 goroutine1 通道。然后我们通过通道发送字符串“Australia”。
goroutine1 := make(chan string, 5) // 字符串缓冲通道
goroutine1 <- "Australia" // 通过通道发送字符串
3.从通道接收值:
data := <-goroutine1 // 从通道接收字符串
// `<-` 运算符附加到通道变量(goroutine1)的左侧,以接收来自通道的值。
20.2.3 发送和接收特性
1.对于同一个通道,发送操作之间是互斥的,接收操作之间也是互斥的。
2.发送操作和接收操作中对元素值的处理都是不可分割的。
3.发送操作在完全完成之前会被阻塞。接收操作也是如此。
// 创建int类型通道,只能传入int类型值
var data = make(chan int)
func sendData() {
rand.Seed(time.Now().UnixNano()) // 发送时指定为随机值
value := rand.Intn(10) // 取10范围内的随机值
fmt.Printf("send: %v\n", value) // 格式化输出要发送的数据
time.Sleep(time.Second * 5) // 发送完需要等待5秒才能接收到
data <- value // 将值放到通道中
}
func receiveData() {
// 从通道接收值
defer close(data) // defer是延迟操作,只有main主函数退出时才会关闭
go sendData() // 启动一个协程
fmt.Printf("wait...\n")
valuedata := <-data
fmt.Printf("receicve: %v\n", valuedata)
fmt.Println("end...")
}
func main() {
receiveData()
}
20.2.4 channel遍历
1.for循环+if判断
// 创建整型的无缓冲通道
var c = make(chan int)
func channelIf() {
// 开启子协程
go func() {
for i := 0; i < 5; i++ {
c <- i // 向通道发送数据
}
close(c) // 关闭通道,如果第3个数据读不到,就读取默认值
}()
// for循环+if判断
for {
v, ok := <-c
if ok {
fmt.Printf("v: %v\n", v)
} else {
break
}
}
}
func main() {
channelIf()
}
2.for range
// 创建整型的无缓冲通道
var c = make(chan int)
func channelIf() {
// 开启子协程
go func() {
for i := 0; i < 5; i++ {
c <- i // 向通道发送数据
}
close(c) // 关闭通道,如果第3个数据读不到,就读取默认值
}()
// 从通道接收数据
// 1.for循环读取
//for i := 0; i < 6; i++ {
// r := <-c
// fmt.Printf("r: %v\n", r)
//
//}
// 2.for_range读取---最常用
for v := range c {
fmt.Printf("v: %v\n", v)
}
}
func main() {
channelIf()
}
注意:如果通道关闭,读多写少,没有了就是默认值,例如,int 就是0,如果没有关闭就会死锁。
20.3 WaitGroup实现同步
同步:两个协程之间相互等待
1.不添加WaitGroup
// 不添加`WaitGroup`时
func showMsg(i int) {
fmt.Printf("i: %v\n", i)
}
func main() {
for i := 0; i < 10; i++ {
// 启动一个协程来执行
go showMsg(i)
}
// 主协程--执行到主协程时,函数就结束了,未执行的协程不再执行,输出结果不固定
fmt.Println("end...")
}
2.添加WaitGroup–希望主协程等待其他协程运行结束
// 添加`WaitGroup`时
var wp sync.WaitGroup
func showMsg(i int) {
// defer wp.Add(-1)
defer wp.Done() // 等价于 defer wp.Add(-1)
fmt.Printf("i: %v\n", i)
}
func main() {
for i := 0; i < 10; i++ {
// 启动一个协程来执行
go showMsg(i)
wp.Add(1)
}
wp.Wait()
// 主协程--执行到主协程时,函数就结束了,未执行的协程不再执行,输出结果不固定
fmt.Println("end...")
}
20.4 runtime包
runtime包里面定义了一些协程管理相关的api。
20.4.1 Gosched()
让出CPU时间片,重新等待安排任务
// runtime.Gosched()
func show(msg string) {
for i := 0; i < 2; i++ {
fmt.Printf("msg: %v\n", msg)
}
}
func main() {
go show("java") // 子协程来运行
// 主协程
for i := 0; i < 2; i++ {
// 切一下,再次分配任务
runtime.Gosched() // 有权利执行任务了,让给你(其他子协程来执行)
fmt.Printf("Golang:%v\n", "Golang")
}
fmt.Println("end...")
}
20.4.2 Goexit()
退出当前协程。
// runtime.Goexit()
func show() {
for i := 0; i < 10; i++ {
fmt.Printf("i: %v\n", i)
if i >= 5 {
runtime.Goexit()
}
}
}
func main() {
go show()
time.Sleep(time.Second)
}
20.4.3 GOMAXPROCS
设置最大CPU核心数。
// runtime.GOMAXPROCS
func a() {
for i := 0; i < 10; i++ {
fmt.Println("A", i)
time.Sleep(time.Millisecond * 100)
}
}
func b() {
for i := 0; i < 10; i++ {
fmt.Println("B", i)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
fmt.Printf("runtime.NumCPU(): %v\n", runtime.NumCPU()) // 查看当前CPU核心数
runtime.GOMAXPROCS(2) // 修改为1查看效果
go a()
go b()
time.Sleep(time.Second)
}
20.5 Mutex互斥锁
除了使用channel实现同步之外,还可以使用Mutex互斥锁的方式实现同步。
var i int = 100
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
defer wg.Done()
lock.Lock()
i += 1
fmt.Printf("i++: %v\n", i)
time.Sleep(time.Millisecond * 10)
lock.Unlock()
}
func sub() {
lock.Lock()
defer wg.Done()
i -= 1
fmt.Printf("i--: %v\n", i)
time.Sleep(time.Millisecond * 2)
lock.Unlock()
}
func main() {
for i := 0; i < 100; i++ {
wg.Add(1)
go add()
wg.Add(1)
go sub()
}
wg.Wait()
fmt.Printf("end i: %v\n", i)
}
20.6 select
1.select是Go中的一个控制结构,类似于switch语句,用于处理异步IO操作。select会监听case语句中channel的读写操作,当case中channel读写操作为非阻塞状态(即能读写)时,将会触发相应的动作。
select中的case语句必须是一个channel操作,即可读或可写
select中的default子句总是可运行的。
2.如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行。
3.如果没有可运行的case语句,且有default语句,那么就会执行default的动作。
4.如果没有可运行的case语句,且没有default语句,select将阻塞,直到某个case通信可以运行
// 创建通道
var chanInt = make(chan int, 0)
var chanStr = make(chan string)
func chanSelect() {
// 开启子协程
go func() {
// 向通道发送数据
chanInt <- 100
chanStr <- "中国"
// 关闭通道,如果数据读不到,就取默认值
defer close(chanInt)
defer close(chanStr)
}()
// 永真for循环
for {
select {
// 从通道接收数据
case r := <-chanInt:
fmt.Printf("chanInt: %v\n", r)
case r := <-chanStr:
fmt.Printf("chanStr: %v\n", r)
default:
fmt.Println("default...")
}
time.Sleep(time.Second) // 每执行一次,睡眠一秒
}
}
20.7 Timer
Timer是定时器的意思,可以实现一些定时操作,内部也是通过channel来实现的。
func channelTimer() {
timer1 := time.NewTimer(time.Second * 2)
fmt.Printf("time.Now(): %v\n", time.Now())
t1 := <-timer1.C // 阻塞的,直到时间到了
fmt.Printf("t1: %v\n", t1)
// 如果只是想单纯等待的话,可以使用time.Sleep实现
timer2 := time.NewTimer(time.Second * 2)
<-timer2.C // C的类型是通道
fmt.Println("2s后")
fmt.Printf("time.Now(): %v\n", time.Now())
time.Sleep(time.Second * 2)
fmt.Println("再一次2s后")
fmt.Printf("time.Now(): %v\n", time.Now())
<-time.After(time.Second * 2) // time.After函数的返回值是chan Time
fmt.Println("再再一次2s后")
fmt.Printf("time.Now(): %v\n", time.Now())
timer3 := time.NewTimer(time.Second) // 等待1秒
go func() {
<-timer3.C
fmt.Println("Timer 3 expired")
}()
stop := timer3.Stop() // 停止定时器
// stop 阻止timer事件发生,当该函数执行后,timer计时器停止,相应的事件不再执行
if stop {
fmt.Println("Timer 3 stopped")
}
time.Sleep(time.Second * 3)
fmt.Println("main end...")
// reset 修改时间
fmt.Println("before")
timer4 := time.NewTimer(time.Second * 5) // 原来设置5s
timer4.Reset(time.Second * 10) // 重新设置时间,即修改NewTimer的时间
<-timer4.C
fmt.Println("after")
}
20.8 Ticker
Timer只执行一次,Ticker可以周期的执行。
func channelTicker1() {
ticker := time.NewTicker(time.Second)
counter := 1
for _ = range ticker.C {
fmt.Println("ticker...")
counter++
if counter >= 6 {
break
}
}
ticker.Stop()
}
func channelTicker2() {
ticker := time.NewTicker(time.Second)
chanInt := make(chan int)
go func() {
for _ = range ticker.C {
select {
case chanInt <- 1:
fmt.Println("send 1")
case chanInt <- 2:
fmt.Println("send 2")
case chanInt <- 3:
fmt.Println("send 3")
}
}
}()
sum := 0
for v := range chanInt {
fmt.Println("收到: ", v)
sum += v
if sum >= 10 {
break
}
}
}
20.9 原子变量
var i = 100
var lock sync.Mutex
func add() {
lock.Lock()
i++
lock.Unlock()
}
func sub() {
lock.Lock()
i--
lock.Unlock()
}
func main() {
for i := 0; i < 100; i++ {
go add()
go sub()
}
time.Sleep(time.Second * 2)
fmt.Printf("i: %v\n", i)
}
这是我们之前的写法,使用锁实现协程的同步
下面使用原子操作
var i int32 = 100
// 使用的原理:cas---compare and swap
// 操作之前,现在的值和之前的值进行比较,只有一样的时候才进行添加
func add() {
atomic.AddInt32(&i, 1) // 取i的指针,并且加1
}
func sub() {
atomic.AddInt32(&i, -1)
}
func main() {
for i := 0; i < 100; i++ {
go add()
go sub()
}
time.Sleep(time.Second * 2)
fmt.Printf("i: %v\n", i)
}
20.10 原子操作
atomic 提供的原子操作能够确保任一时刻只有一个goroutine对变量进行操作,善用 atomic 能够避免程序中出现大量的锁操作。
atomic常见操作有:增减、载入(read)、比较并交换(cas)、交换、存储(write)
20.10.1 增减
atomic 包中提供了如下以Add为前缀的增减操作:
- func AddInt32(addr *int32, delta int32) (new int32)
- func AddInt64(addr *int64, delta int64) (new int64)
- func AddUint32(addr *uint32, delta uint32) (new uint32)
- func AddUint64(addr *uint64, delta uint64) (new uint64)
- func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)
func add_sub() {
var i int32 = 100
atomic.AddInt32(&i, 1)
fmt.Printf("i: %v\n", i)
atomic.AddInt32(&i, 1)
fmt.Printf("i: %v\n", -i)
}
20.10.2 载入
atomic 包中提供了如下以Load为前缀的增减操作:
- func LoadInt32(addr *int32) (val int32)
- func LoadInt64(addr *int64) (val int64)
- func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)
- func LoadUint32(addr *uint32) (val uint32)
- func LoadUint64(addr *uint64) (val uint64)
- func LoadUintptr(addr *uintptr) (val uintptr)
func load_store() {
var i int32 = 100
atomic.LoadInt32(&i) // read
fmt.Printf("i: %v\n", i)
atomic.StoreInt32(&i, 345) // write
fmt.Printf("i: %v\n", i)
}
载入操作能够保证原子的读变量的值,当读取的时候,任何其他CPU操作都无法对该变量进行读写,其实现机制受到底层硬件的支持。
20.10.3 比较并交换
该操作简称 CAS(Compare And Swap)。 这类操作的前缀为 CompareAndSwap :
- func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
- func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
- func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)
- func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
- func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
- func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func cas() {
var i int32 = 100
b := atomic.CompareAndSwapInt32(&i, 100, 67)
fmt.Printf("b: %v\n", b)
fmt.Printf("i: %v\n", i)
}
该操作在进行交换前首先确保变量的值未被更改,即仍然保持参数
old所记录的值,满足此前提下才进行交换操作。CAS的做法类似操作数据库时常见的乐观锁机制。
20.10.4 交换
此类操作的前缀为 Swap:
- func SwapInt32(addr *int32, new int32) (old int32)
- func SwapInt64(addr *int64, new int64) (old int64)
- func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)
- func SwapUint32(addr *uint32, new uint32) (old uint32)
- func SwapUint64(addr *uint64, new uint64) (old uint64)
- func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
相对于CAS,明显此类操作更为暴力直接,并不管变量的旧值是否被改变,直接赋予新值然后返回背替换的值。
20.10.5 存储
此类操作的前缀为 Store:
- func StoreInt32(addr *int32, val int32)
- func StoreInt64(addr *int64, val int64)
- func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)
- func StoreUint32(addr *uint32, val uint32)
- func StoreUint64(addr *uint64, val uint64)
- func StoreUintptr(addr *uintptr, val uintptr)
此类操作确保了写变量的原子性,避免其他操作读到了修改变量过程中的脏数据。
21 标准库实用模块
21.1 标准库os模块
21.1.1 文件目录
os标准库实现了平台(操作系统)无关的编程接口。
标准库文档:https://pkg.go.dev/std
1.创建文件
// 创建文件
func createFile() {
file, err := os.Create("a.txt") // 写全返回值的快捷方式:os.Create("a.txt").var
if err != nil {
fmt.Printf("err: %v\n", err)
} else {
fmt.Printf("file.Name(): %v\n", file.Name())
}
}
2.创建目录
// 创建目录
func makeDir() {
//err := os.Mkdir("a", os.ModePerm) // os.ModePerm---赋予目录最高权限777
//if err != nil {
// fmt.Printf("err: %v\n", err)
//}
err2 := os.MkdirAll("a/b/c", os.ModePerm)
if err2 != nil {
fmt.Printf("err2: %v\n", err2)
}
}
3.删除文件或目录
// 删除目录或文件
func remove() {
//err := os.Remove("a.txt")
//if err != nil {
// fmt.Printf("err: %v\n", err)
//}
err := os.RemoveAll("a") // 删除整个目录
if err != nil {
fmt.Printf("err: %v\n", err)
}
}
4.操作工作目录
// 操作工作目录
// 1.获取当前工作目录
func wd() {
// 1.获取当前工作目录
dir, _ := os.Getwd()
fmt.Printf("dir: %v\n", dir)
// 2.修改当前工作目录
os.Chdir("d:/")
dir, _ = os.Getwd()
fmt.Printf("dir: %v\n", dir)
// 3.获得当前临时目录
tmpDir := os.TempDir()
fmt.Printf("tmpDir: %v\n", tmpDir)
}
5.重命名文件
// 重命名文件
func renameFile() {
err := os.Rename("a.txt", "a2.txt")
if err != nil {
fmt.Printf("err: %v\n", err)
}
}
6.读文件
// 读文件
func readFile() {
b, err := os.ReadFile("a2.txt")
if err != nil {
fmt.Printf("err: %v\n", err)
} else {
fmt.Printf("b: %v\n", string(b[:]))
}
}
7.写文件
// 写文件
func writeFile() {
writeData := "2026年男足世界杯正在美国,加拿大,墨西哥这三个国家举行!"
os.WriteFile("a2.txt", []byte(writeData), os.ModePerm)
}
21.1.2 File文件读操作
File结构体相关的文件读操作。
1.文件的打开与关闭
// 文件的打开与关闭
func openClose() {
// 只能读
//file, err := os.Open("a.txt")
//if err != nil {
// fmt.Printf("err: %v\n", err)
//} else {
// fmt.Printf("file.Name(): %v\n", file.Name())
// file.Close()
//}
// 打开的文件不存在时,会创建,并赋予权限
// 根据第二个参数 可以读写或者创建
file, err := os.OpenFile("a1.txt", os.O_RDWR|os.O_CREATE, 755)
if err != nil {
fmt.Printf("err: %v\n", err)
} else {
fmt.Printf("file.Name(): %v\n", file.Name())
file.Close()
}
}
2.创建文件
// 创建文件
func createFile() {
// 等价于:OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
f, _ := os.Create("a2.txt")
fmt.Printf("f.Name(): %v\n", f.Name())
// 第一个参数 目录默认:Temp 第二个参数 文件名前缀
f2, _ := os.CreateTemp("", "temp")
fmt.Printf("f2.Name(): %v\n", f2.Name())
}
3.读文件
// 读文件
func readOps() {
// 循环读取
f, _ := os.Open("a.txt")
buf := make([]byte, 4)
for {
n, err := f.Read(buf)
if err == io.EOF {
break
}
fmt.Printf("n: %v\n", n)
fmt.Printf("string(buf): %v\n", string(buf))
}
f.Close()
//
//n, _ := f.ReadAt(buf, 3)
//fmt.Printf("n: %v\n", n)
//fmt.Printf("string(buf): %v\n", string(buf))
//
//de, _ := os.ReadDir("a/")
//for _, v := range de {
// fmt.Printf("v.IsDir(): %v\n", v.IsDir())
// fmt.Printf("v.Name(): %v\n", v.Name())
//
//}
}
4.定位
func readOps() {
// 定位
f, _ := os.Open("a.txt")
f.Seek(3, 0)
buf := make([]byte, 10)
n, _ := f.Read(buf)
fmt.Printf("n: %v\n", n)
fmt.Printf("string(buf): %v\n", string(buf))
f.Close()
}
21.1.3 File文件写操作
File结构体相关的文件写操作。
// 写入字节数组
func write() {
// os.O_APPEND---追加 os.O_TRUNC---覆盖
f, _ := os.OpenFile("a.txt", os.O_RDWR|os.O_APPEND, 0755)
f.Write([]byte("hello golang"))
f.Close()
}
// 写入字符串
func writeString() {
f, _ := os.OpenFile("a.txt", os.O_RDWR|os.O_APPEND, 0755)
f.WriteString("hello java")
f.Close()
}
// 随机写入
func writeAt() {
f, _ := os.OpenFile("a.txt", os.O_RDWR, 0755)
f.WriteAt([]byte("aaa"), 3)
f.Close()
}
21.1.4 进程相关操作
func main() {
// 获得当前正在运行的进程id
fmt.Printf("os.Getpid(): %v\n", os.Getpid())
// 父id
fmt.Printf("os.Getppid(): %v\n", os.Getppid())
//设置新进程的属性
attr := &os.ProcAttr{
//files指定新进程继承的活动文件对象
//前三个分别为,标准输入、标准输出、标准错误输出
Files: []*os.File{os.Stdin, os.Stdout, os.Stderr},
//新进程的环境变量
Env: os.Environ(),
}
//开始一个新进程
p, err := os.StartProcess(`D:\\Notepad++\\notepad++.exe`, []string{`D:\\Notepad++\notepad++.exe`, `D:\\a.txt`}, attr)
if err != nil {
fmt.Println(err)
}
fmt.Println(p)
fmt.Println("进程ID:", p.Pid)
//通过进程ID查找进程
p2, _ := os.FindProcess(p.Pid)
fmt.Println(p2)
//等待10秒,执行函数
time.AfterFunc(time.Second*10, func() {
//向p进程发送退出信号
p.Signal(os.Kill)
})
//等待进程p的退出,返回进程状态
ps, _ := p.Wait()
fmt.Println(ps.String())
}
21.1.5 环境相关方法
func main() {
// 获得所有环境变量
fmt.Printf("os.Environ(): %v\n", os.Environ())
// 获得某个环境变量
s := os.Getenv("GOPATH")
fmt.Printf("s: %v\n", s)
fmt.Printf("os.Getenv(\"JAVA_HOME\"): %v\n", os.Getenv("JAVA_HOME"))
// 查找
s, b := os.LookupEnv("GOPATH")
if b {
fmt.Printf("s: %v\n", s)
}
// 设置环境变量
//os.Setenv("env1","env1")
// 替换
os.Setenv("NAME", "gopher")
os.Setenv("BURROW", "/usr/gopher")
fmt.Println(os.ExpandEnv("$NAME lives in ${BURROW}."))
// 清空环境变量
//os.Clearenv()
}
21.2 标准库io包
Go 语言中,为了方便开发者使用,将 IO 操作封装在了如下几个包中:
io 为 IO 原语(I/O primitives)提供基本的接口 os File Reader Writer
io/ioutil 封装一些实用的 I/O 函数
fmt 实现格式化 I/O,类似 C 语言中的 printf 和 scanf format fm
bufio 实现带缓冲I/O
21.2.1 基本的 IO 接口
在 io 包中最重要的是两个接口:Reader 和 Writer 接口。各种 IO 包都跟这两个接口有关,只要实现了这两个接口,就有了 IO 的功能。
1.Reader 接口
type Reader interface {
Read(p []byte) (n int, err error)
}
2.Writer 接口
type Writer interface {
Write(p []byte) (n int, err error)
}
3.实现Reader和Writer接口的类型
os.File 同时实现了 io.Reader 和 io.Writer
strings.Reader 实现了 io.Reader
bufio.Reader/Writer 分别实现了 io.Reader 和 io.Writer
bytes.Buffer 同时实现了 io.Reader 和 io.Writer
bytes.Reader 实现了 io.Reader
compress/gzip.Reader/Writer 分别实现了 io.Reader 和 io.Writer
crypto/cipher.StreamReader/StreamWriter 分别实现了 io.Reader 和 io.Writer
crypto/tls.Conn 同时实现了 io.Reader 和 io.Writer
encoding/csv.Reader/Writer 分别实现了 io.Reader 和 io.Writer
4.示例
func ioReader() {
r := strings.NewReader("世界杯!")
buf := make([]byte, 10)
r.Read(buf)
fmt.Printf("string(buf): %v\n", string(buf))
}
func ioCopy() {
r := strings.NewReader("2026美加墨世界杯!")
_, err := io.Copy(os.Stdout, r)
if err != nil {
log.Fatal(err)
}
}
21.2.2 ioutil包
封装一些实用的 I/O 函数
| 名称 | 作用 |
|---|---|
| ReadAll | 读取数据,返回读到的字节 slice |
| ReadDir | 读取一个目录,返回目录入口数组 []os.FileInfo |
| ReadFile | 读一个文件,返回文件内容(字节slice) |
| WriteFile | 根据文件路径,写入字节slice |
| TempDir | 在一个目录中创建指定前缀名的临时目录,返回新临时目录的路径 |
| TempFile | 在一个目录中创建指定前缀名的临时文件,返回 os.File |
21.3 标准库fmt包
fmt包实现了格式化的I/O函数,这点类似C语言中的printf和scanf,但是更加简单. format
21.3.1 Scanning
一组类似的函数通过扫描已格式化的文本来产生值。
1.Scan、Scanf 和 Scanln 从os.Stdin 中读取;
2.Fscan、Fscanf 和 Fscanln 从指定的 io.Reader 中读取;
3.Sscan、Sscanf 和 Sscanln 从实参字符串中读取。
4.Scanln、Fscanln 和 Sscanln在换行符处停止扫描,且需要条目紧随换行符之后;
5.Scanf、Fscanf 和 Sscanf需要输入换行符来匹配格式中的换行符;其它函数则将换行符视为空格。
6.Scanf、Fscanf 和 Sscanf 根据格式字符串解析实参,类似于 Printf。例如,%x会将一个整数扫描为十六进制数,而 %v 则会扫描该值的默认表现格式。
// scan
func test1() {
var age int
fmt.Println("请输入年龄:")
fmt.Scan(&age)
fmt.Printf("age: %v\\n", age)
}
// Scanf
func test2() {
var name string
fmt.Println("请输入姓名:")
fmt.Scanf("%s", &name)
fmt.Printf("name: %v\\n", name)
}
// Scanln
func test3() {
var age int
fmt.Println("请输入年龄:")
fmt.Scanln(&age)
fmt.Printf("age: %v\\n", age)
}
其他实例参考官网
21.3.2 函数
func Errorf(format string, a ...interface{}) error
Errorf 根据于格式说明符进行格式化,并将字符串作为满足 error 的值返回,其返回类型是error.
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
Fprint 使用其操作数的默认格式进行格式化并写入到 w。当两个连续的操作数均不为字符串时,它们之间就会添加空格。它返回写入的字节数以及任何遇到的错误。
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) //
Fprintf 根据于格式说明符进行格式化并写入到 w。它返回写入的字节数以及任何遇到的写入错误。
func Fprintln(w io.Writer, a ...interface{}) (n int, err error)
Fprintln 使用其操作数的默认格式进行格式化并写入到 w。其操作数之间总是添加空格,且总在最后追加一个换行符。它返回写入的字节数以及任何遇到的错误。
21.4 标准库bufio
bufio包实现了有缓冲的I/O。它包装一个io.Reader或io.Writer接口对象,创建另一个也实现了该接口,且同时还提供了缓冲和一些文本I/O的帮助函数的对象。
21.4.1 常量
const (
defaultBufSize = 4096
)
21.4.2 变量
var (
ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
ErrBufferFull = errors.New("bufio: buffer full")
ErrNegativeCount = errors.New("bufio: negative count")
)
var (
ErrTooLong = errors.New("bufio.Scanner: token too long")
ErrNegativeAdvance = errors.New("bufio.Scanner: SplitFunc returns negative advance count")
ErrAdvanceTooFar = errors.New("bufio.Scanner: SplitFunc returns advance count beyond input")
)
会被Scanner类型返回的错误。
21.4.3 type Reader
type Reader struct {
buf []byte
rd io.Reader // reader provided by the client
r, w int // buf read and write positions
err error
lastByte int // last byte read for UnreadByte; -1 means invalid
lastRuneSize int // size of last rune read for UnreadRune; -1 means invalid
}
Reader实现了给一个io.Reader接口对象附加缓冲。
1.func NewReader
func NewReader(rd io.Reader) *Reader
NewReader创建一个具有默认大小缓冲、从r读取的*Reader。NewReader 相当于 NewReaderSize(rd, 4096)
2.func NewReaderSize
func NewReaderSize(rd io.Reader, size int) *Reader
NewReaderSize创建一个具有最少有size尺寸的缓冲、从r读取的Reader。如果参数r已经是一个具有足够大缓冲的 Reader类型值,会返回r。
3.func (*Reader)Reset(r io.Reader)
func (b *Reader) Reset(r io.Reader)
Reset丢弃缓冲中的数据,清除任何错误,将b重设为其下层从r读取数据。
func main() {
s := strings.NewReader("ABCEFG")
str := strings.NewReader("123455")
br := bufio.NewReader(s)
b, _ := br.ReadString('\\n')
fmt.Println(b)
br.Reset(str)
b, _ = br.ReadString('\\n')
fmt.Println(b)
}
4.func (*Reader)Read
func (b *Reader) Read(p []byte) (n int, err error)
Read读取数据写入p。本方法返回写入p的字节数。本方法一次调用最多会调用下层Reader接口一次Read方法,因此返回值n可能小于len(p)。读取到达结尾时,返回值n将为0而err将为io.EOF。
func main() {
s := strings.NewReader("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")
br := bufio.NewReader(s)
p := make([]byte, 10)
for {
n, err := br.Read(p)
if err == io.EOF {
break
} else {
fmt.Printf("string(p): %v\\n", string(p[0:n]))
}
}
}
5.func (*Reader)ReadByte
func (b *Reader) ReadByte() (c byte, err error)
ReadByte读取并返回一个字节。如果没有可用的数据,会返回错误。
6.func (*Reader)UnreadByte
func (b *Reader) UnreadByte() error
UnreadByte吐出最近一次读取操作读取的最后一个字节。(只能吐出最后一个,多次调用会出问题)
func main() {
s := strings.NewReader("ABCDEFG")
br := bufio.NewReader(s)
c, _ := br.ReadByte()
fmt.Printf("%c\\n", c)
c, _ = br.ReadByte()
fmt.Printf("%c\\n", c)
br.UnreadByte()
c, _ = br.ReadByte()
fmt.Printf("%c\\n", c)
}
7.func (*Reader)ReadRune
func (b *Reader) ReadRune() (r rune, size int, err error)
ReadRune读取一个utf-8编码的unicode码值,返回该码值、其编码长度和可能的错误。如果utf-8编码非法,读取位置只移动1字节,返回U+FFFD,返回值size为1而err为nil。如果没有可用的数据,会返回错误。
8.func (*Reader)UnreadRune
func (b *Reader) UnreadRune() error
UnreadRune吐出最近一次ReadRune调用读取的unicode码值。如果最近一次读取不是调用的ReadRune,会返回错误。(从这点看,UnreadRune比UnreadByte严格很多)
func main() {
s := strings.NewReader("你好,世界!")
br := bufio.NewReader(s)
c, size, _ := br.ReadRune()
fmt.Printf("%c %v\\n", c, size)
c, size, _ = br.ReadRune()
fmt.Printf("%c %v\\n", c, size)
br.UnreadRune()
c, size, _ = br.ReadRune()
fmt.Printf("%c %v\\n", c, size)
}
9.func (*Reader)ReadLine
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
ReadLine是一个低水平的行数据读取原语。大多数调用者应使用ReadBytes(‘\n’)或ReadString(‘\n’)代替,或者使用Scanner。
ReadLine尝试返回一行数据,不包括行尾标志的字节。如果行太长超过了缓冲,返回值isPrefix会被设为true,并返回行的前面一部分。该行剩下的部分将在之后的调用中返回。返回值isPrefix会在返回该行最后一个片段时才设为false。返回切片是缓冲的子切片,只在下一次读取操作之前有效。ReadLine要么返回一个非nil的line,要么返回一个非nil的err,两个返回值至少一个非nil。
返回的文本不包含行尾的标志字节(”\r\n”或”\n”)。如果输入流结束时没有行尾标志字节,方法不会出错,也不会指出这一情况。在调用ReadLine之后调用UnreadByte会总是吐出最后一个读取的字节(很可能是该行的行尾标志字节),即使该字节不是ReadLine返回值的一部分。
func main() {
s := strings.NewReader("ABC\\nDEF\\r\\nGHI\\r\\nGHI")
br := bufio.NewReader(s)
w, isPrefix, _ := br.ReadLine()
fmt.Printf("%q %v\\n", w, isPrefix)
w, isPrefix, _ = br.ReadLine()
fmt.Printf("%q %v\\n", w, isPrefix)
w, isPrefix, _ = br.ReadLine()
fmt.Printf("%q %v\\n", w, isPrefix)
w, isPrefix, _ = br.ReadLine()
fmt.Printf("%q %v\\n", w, isPrefix)
}
10.func (*Reader)ReadSlice
func (b *Reader) ReadSlice(delim byte) (line []byte, err error)
ReadSlice读取直到第一次遇到delim字节,返回缓冲里的包含已读取的数据和delim字节的切片。该返回值只在下一次读取操作之前合法。如果ReadSlice放在在读取到delim之前遇到了错误,它会返回在错误之前读取的数据在缓冲中的切片以及该错误(一般是io.EOF)。如果在读取到delim之前缓冲就被写满了,ReadSlice失败并返回ErrBufferFull。因为ReadSlice的返回值会被下一次I/O操作重写,调用者应尽量使用ReadBytes或ReadString替代本法功法。当且仅当ReadBytes方法返回的切片不以delim结尾时,会返回一个非nil的错误。
func main() {
s := strings.NewReader("ABC,DEF,GHI,JKL")
br := bufio.NewReader(s)
w, _ := br.ReadSlice(',')
fmt.Printf("%q\\n", w)
w, _ = br.ReadSlice(',')
fmt.Printf("%q\\n", w)
w, _ = br.ReadSlice(',')
fmt.Printf("%q\\n", w)
}
11.func (*Reader)ReadBytes
func (b *Reader) ReadBytes(delim byte) (line []byte, err error)
ReadBytes读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的切片。如果ReadBytes方法在读取到delim之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是io.EOF)。当且仅当ReadBytes方法返回的切片不以delim结尾时,会返回一个非nil的错误。
func main() {
s := strings.NewReader("ABC DEF GHI JKL")
br := bufio.NewReader(s)
w, _ := br.ReadBytes(' ')
fmt.Printf("%q\\n", w)
w, _ = br.ReadBytes(' ')
fmt.Printf("%q\\n", w)
w, _ = br.ReadBytes(' ')
fmt.Printf("%q\\n", w)
}
12.func (*Reader)ReadString
func (b *Reader) ReadString(delim byte) (line string, err error)
ReadString读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的字符串。如果ReadString方法在读取到delim之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是io.EOF)。当且仅当ReadString方法返回的切片不以delim结尾时,会返回一个非nil的错误。
func main() {
s := strings.NewReader("ABC DEF GHI JKL")
br := bufio.NewReader(s)
w, _ := br.ReadString(' ')
fmt.Printf("%q\\n", w)
w, _ = br.ReadString(' ')
fmt.Printf("%q\\n", w)
w, _ = br.ReadString(' ')
fmt.Printf("%q\\n", w)
}
13.func (*Reader)WriteTo
func (b *Reader) WriteTo(w io.Writer) (n int64, err error)
WriteTo方法实现了io.WriterTo接口。
func main() {
s := strings.NewReader("ABCEFGHIJKLMN")
br := bufio.NewReader(s)
b := bytes.NewBuffer(make([]byte, 0))
br.WriteTo(b)
fmt.Printf("%s\\n", b)
}
21.4.4 type Writer
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
Writer实现了为io.Writer接口对象提供缓冲。如果在向一个Writer类型值写入时遇到了错误,该对象将不再接受任何数据,且所有写操作都会返回该错误。在说有数据都写入后,调用者有义务调用Flush方法以保证所有的数据都交给了下层的io.Writer。
1.func NewWriter
func NewWriter(w io.Writer) *Writer
NewWriter创建一个具有默认大小缓冲、写入w的*Writer。NewWriter 相当于 NewWriterSize(wr, 4096)
2.func NewWriterSize
func NewWriterSize(w io.Writer, size int) *Writer
NewWriterSize创建一个具有最少有size尺寸的缓冲、写入w的Writer。如果参数w已经是一个具有足够大缓冲的Writer类型值,会返回w。
3.func (*Writer)Reset
func (b *Writer) Reset(w io.Writer)
Reset丢弃缓冲中的数据,清除任何错误,将b重设为将其输出写入w。
func main() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b)
bw.WriteString("123456789")
c := bytes.NewBuffer(make([]byte, 0))
bw.Reset(c)
bw.WriteString("456")
bw.Flush()
fmt.Println(b)
fmt.Println(c)
}
4.func (*Writer)Bufferd
func (b *Writer) Buffered() int
Buffered返回缓冲中已使用的字节数。
5.func (*Writer)Available
func (b *Writer) Available() int
Available返回缓冲中还有多少字节未使用。
6.func (*Writer) Write
func (b *Writer) Write(p []byte) (nn int, err error)
Write将p的内容写入缓冲。返回写入的字节数。如果返回值nn < len(p),还会返回一个错误说明原因。
7.func (*Writer) WriteString
func (b *Writer) WriteString(s string) (int, error)
WriteString写入一个字符串。返回写入的字节数。如果返回值nn < len(s),还会返回一个错误说明原因。
8.func (*Writer) WriteByte
func (b *Writer) WriteByte(c byte) error
WriteByte写入单个字节。
9.func (*Writer) WriteRune
func (b *Writer) WriteRune(r rune) (size int, err error)
WriteRune写入一个unicode码值(的utf-8编码),返回写入的字节数和可能的错误。
10.func (*Writer) Flush
func (b *Writer) Flush() error
Flush方法将缓冲中的数据写入下层的io.Writer接口。
11.func (*Writer) ReadFrom
func (b *Writer) ReadFrom(r io.Reader) (n int64, err error)
ReadFrom实现了io.ReaderFrom接口。
func main() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b)
fmt.Println(bw.Available()) // 4096
fmt.Println(bw.Buffered()) // 0
bw.WriteString("ABCDEFGHIJKLMN")
fmt.Println(bw.Available())
fmt.Println(bw.Buffered())
fmt.Printf("%q\\n", b)
bw.Flush()
fmt.Println(bw.Available())
fmt.Println(bw.Buffered())
fmt.Printf("%q\\n", b)
}
func main() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b)
// 写入缓存
// byte等同于 int8
bw.WriteByte('H')
bw.WriteByte('e')
bw.WriteByte('l')
bw.WriteByte('l')
bw.WriteByte('o')
bw.WriteByte(' ')
// rune等同于int32
bw.WriteRune('世')
bw.WriteRune('界')
bw.WriteRune('!')
// 写入b
bw.Flush()
fmt.Println(b)
}
func main() {
b := bytes.NewBuffer(make([]byte, 0))
s := strings.NewReader("Hello 世界!")
bw := bufio.NewWriter(b)
bw.ReadFrom(s)
//bw.Flush() //ReadFrom无需使用Flush,其自己已经写入.
fmt.Println(b) // Hello 世界!
}
21.4.5 type ReadWriter
type ReadWriter struct {
*Reader
*Writer
}
ReadWriter类型保管了指向Reader和Writer类型的指针,(因此)实现了io.ReadWriter接口。
1.func NewReadWriter
func NewReadWriter(r *Reader, w *Writer) *ReadWriter
NewReadWriter申请创建一个新的、将读写操作分派给r和w 的ReadWriter。
func main() {
b := bytes.NewBuffer(make([]byte, 0))
bw := bufio.NewWriter(b)
s := strings.NewReader("123")
br := bufio.NewReader(s)
rw := bufio.NewReadWriter(br, bw)
p, _ := rw.ReadString('\\n')
fmt.Println(string(p)) //123
rw.WriteString("asdf")
rw.Flush()
fmt.Println(b) //asdf
}
21.4.6 type SplitFunc
type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
SplitFunc类型代表用于对输出作词法分析的分割函数。
参数data是尚未处理的数据的一个开始部分的切片,参数atEOF表示是否Reader接口不能提供更多的数据。返回值是解析位置前进的字节数,将要返回给调用者的token切片,以及可能遇到的错误。如果数据不足以(保证)生成一个完整的token,例如需要一整行数据但data里没有换行符,SplitFunc可以返回(0, nil, nil)来告诉Scanner读取更多的数据写入切片然后用从同一位置起始、长度更长的切片再试一次(调用SplitFunc类型函数)。
如果返回值err非nil,扫描将终止并将该错误返回给Scanner的调用者。
除非atEOF为真,永远不会使用空切片data调用SplitFunc类型函数。然而,如果atEOF为真,data却可能是非空的、且包含着未处理的文本。
SplitFunc 的作用很简单,从 data 中找出你感兴趣的数据,然后返回并告诉调用者,data 中有多少数据你已经处理过了
1.func ScanBytes
func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanBytes是用于Scanner类型的分割函数(符合SplitFunc),本函数会将每个字节作为一个token返回。
2.func ScanRunes
func ScanRunes(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanRunes是用于Scanner类型的分割函数(符合SplitFunc),本函数会将每个utf-8编码的unicode码值作为一个token返回。本函数返回的rune序列和range一个字符串的输出rune序列相同。错误的utf-8编码会翻译为U+FFFD = “\xef\xbf\xbd”,但只会消耗一个字节。调用者无法区分正确编码的rune和错误编码的rune。
3.func ScanWords
func ScanWords(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanRunes是用于Scanner类型的分割函数(符合SplitFunc),本函数会将空白(参见unicode.IsSpace)分隔的片段(去掉前后空白后)作为一个token返回。本函数永远不会返回空字符串。用来找出 data 中的单行数据并返回(包括空行)
4.func ScanLines
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error)
ScanLines是用于Scanner类型的分割函数(符合SplitFunc),本函数会将每一行文本去掉末尾的换行标记作为一个token返回。返回的行可以是空字符串。换行标记为一个可选的回车后跟一个必选的换行符。最后一行即使没有换行符也会作为一个token返回。
21.4.7 type Scanner
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
}
Scanner类型提供了方便的读取数据的接口,如从换行符分隔的文本里读取每一行。成功调用的Scan方法会逐步提供文件的token,跳过token之间的字节。token由SplitFunc类型的分割函数指定;默认的分割函数会将输入分割为多个行,并去掉行尾的换行标志。本包预定义的分割函数可以将文件分割为行、字节、unicode码值、空白分隔的word。调用者可以定制自己的分割函数。扫描会在抵达输入流结尾、遇到的第一个I/O错误、token过大不能保存进缓冲时,不可恢复的停止。当扫描停止后,当前读取位置可能会远在最后一个获得的token后面。需要更多对错误管理的控制或token很大,或必须从reader连续扫描的程序,应使用bufio.Reader代替。
1.func NewScanner
func NewScanner(r io.Reader) *Scanner
NewScanner创建并返回一个从r读取数据的Scanner,默认的分割函数是ScanLines。
2.func (*Scanner) Split
func (s *Scanner) Split(split SplitFunc)
Split设置该Scanner的分割函数。本方法必须在Scan之前调用。
func main() {
s := strings.NewReader("ABC DEF GHI JKL")
bs := bufio.NewScanner(s)
bs.Split(bufio.ScanWords)
for bs.Scan() {
fmt.Println(bs.Text())
}
}
3.func (*Scanner) Scan
func (s *Scanner) Scan() bool
Scan方法获取当前位置的token(该token可以通过Bytes或Text方法获得),并让Scanner的扫描位置移动到下一个token。当扫描因为抵达输入流结尾或者遇到错误而停止时,本方法会返回false。在Scan方法返回false后,Err方法将返回扫描时遇到的任何错误;除非是io.EOF,此时Err会返回nil。
func main() {
s := strings.NewReader("Hello 世界!")
bs := bufio.NewScanner(s)
bs.Split(bufio.ScanBytes)
for bs.Scan() {
fmt.Printf("%s ", bs.Text())
}
}
4.func (*Scanner) Bytes
func (s *Scanner) Bytes() []byte
Bytes方法返回最近一次Scan调用生成的token。底层数组指向的数据可能会被下一次Scan的调用重写。
func main() {
s := strings.NewReader("Hello 世界!")
bs := bufio.NewScanner(s)
bs.Split(bufio.ScanRunes)
for bs.Scan() {
fmt.Printf("%s ", bs.Text())
}
}
5.func (*Scanner) Text
func (s *Scanner) Text() string
Bytes方法返回最近一次Scan调用生成的token,会申请创建一个字符串保存token并返回该字符串。
6.func (*Scanner) Err
func (s *Scanner) Err() error
Err返回Scanner遇到的第一个非EOF的错误。
21.5 标准库log
Golang内置了log包,实现简单的日志服务。通过调用log包的函数,可以实现简单的日志打印功能。
21.5.1 使用
log包中有3个系列的日志打印函数,分别print系列、panic系列、fatal系列。
| 函数系列 | 作用 |
|---|---|
| 单纯打印日志 | |
| panic | 打印日志,抛出panic异常 |
| fatal | 打印日志,强制结束程序(os.Exit(1)),defer函数不会执行 |
func main() {
defer fmt.Println("发生了 panic错误!")
log.Print("my log")
log.Printf("my log %d", 100)
name := "tom"
age := 20
log.Println(name, ",", age)
log.Panic("致命错误!")
// log.Fatal("致命错误!")
fmt.Println("end...")
}
结果示例(实际结果不是这样的哦,因为panic,fatal会影响程序的执行):
2021/11/29 16:45:59 my log
2021/11/29 16:45:59 my log 100
2021/11/29 16:45:59 tom , 20
2021/11/29 16:45:59 致命错误!
发生了 panic错误!
panic: 致命错误!
goroutine 1 [running]:
log.Panic({0xc000107f00, 0x3, 0xc000107f00})
C:/Program Files/Go/src/log/log.go:354 +0x65
main.main()
c:/Users/52406/Desktop/golangprojects/duoke360.com/pro01/demo.go:15 +0x19e
exit status 2
21.5.2 标准log配置
默认情况下log只会打印出时间,但是实际情况下我们可能还需要获取文件名,行号等信息,log包提供给我们定制的接口。 log包提供两个标准log配置的相关方法:
func Flags() int // 返回标准log输出配置
func SetFlags(flag int) // 设置标准log输出配置
1.flag参数
const (
// 控制输出日志信息的细节,不能控制输出的顺序和格式。
// 输出的日志在每一项后会有一个冒号分隔:例如2009/01/23 01:23:23.123123 /a/b/c/d.go:23: message
Ldate = 1 << iota // 日期:2009/01/23
Ltime // 时间:01:23:23
Lmicroseconds // 微秒级别的时间:01:23:23.123123(用于增强Ltime位)
Llongfile // 文件全路径名+行号: /a/b/c/d.go:23
Lshortfile // 文件名+行号:d.go:23(会覆盖掉Llongfile)
LUTC // 使用UTC时间
LstdFlags = Ldate | Ltime // 标准logger的初始值
)
2.标准日志配置示例
func main() {
i := log.Flags()
fmt.Printf("i: %v\\n", i)
log.SetFlags(log.Ldate | log.Ltime | log.Llongfile)
log.Print("my log")
}
// 输出结果:
i: 3
2021/11/29 16:49:53 c:/Users/52406/Desktop/golangprojects/duoke360.com/pro01/demo.go:12: my log
21.5.3 日志前缀配置
log包提供两个日志前缀配置的相关函数:
func Prefix() string // 返回日志的前缀配置
func SetPrefix(prefix string) // 设置日志前缀
1.日志前缀配置实例
package main
import (
"fmt"
"log"
)
func main() {
s := log.Prefix()
fmt.Printf("s: %v\\n", s)
log.SetPrefix("MyLog: ")
s = log.Prefix()
fmt.Printf("s: %v\\n", s)
log.Print("my log...")
}
2.输出结果:
s:
s: MyLog:
MyLog: 2021/11/29 16:51:55 my log...
21.5.4 日志输出位置配置
前面介绍的都是将日志输出到控制台上,golang的log包还支持将日志输出到文件中。log包提供了func SetOutput(w io.Writer)函数,将日志输出到文件中。
func main() {
f, err := os.OpenFile("a.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Panic("打开日志文件异常")
}
log.SetOutput(f)
log.Print("my log...")
}
**结果:**日志输出到当前目录下a.log文件中
2021/11/29 16:57:13 my log...
21.5.5 自定义logger
log包为我们提供了内置函数,让我们能自定义logger。从效果上来看,就是将标题3中的标准日志配置、日志前缀配置、日志输出位置配置整合到一个函数中,使日志配置不在那么繁琐。 log包中提供了func New(out io.Writer, prefix string, flag int) *Logger函数来实现自定义logger。
var logger *log.Logger
func init() {
logFile, err := os.OpenFile("a.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
log.Panic("打开日志文件异常")
}
logger = log.New(logFile, "success", log.Ldate | log.Ltime | log.Lshortfile)
}
func main() {
logger.Println("自定义logger")
}
21.6 标准库builtin
提供了一些类型声明、变量和常量声明,还有一些便利函数,这个包不需要导入,这些变量和函数就可以直接使用。
21.6.1 append
func append(slice []Type, elems ...Type) []Type
slice = append(slice, elem1, elem2) //直接在slice后面添加单个元素,添加元素类型可以和slice相同,也可以不同
slice = append(slice, anotherSlice...) //直接将另外一个slice添加到slice后面,但其本质还是将anotherSlice中的元素一个一个添加到slice中,和第一种方式类似.
// 示例:
func main() {
s1 := []int{1, 2, 3}
i := append(s1, 4)
fmt.Printf("i: %v\\n", i)
s2 := []int{7, 8, 9}
i2 := append(s1, s2...)
fmt.Printf("i2: %v\\n", i2)
}
21.6.2 len
返回,数组、切片、字符串、通道的长度
func main() {
s1 := "hello world"
i := len(s1)
fmt.Printf("i: %v\\n", i)
s2 := []int{1, 2, 3}
fmt.Printf("len(s2): %v\\n", len(s2))
}
21.6.3 print、println
打印输出到控制台。
func main() {
name := "tom"
age := 20
print(name, " ", age, "\\n")
fmt.Println("----------")
println(name, " ", age)
}
21.6.4 panic
抛出一个panic异常
func main() {
defer fmt.Println("panic 异常后执行...")
panic("panic 错误...")
fmt.Println("end...")
}
21.6.5 new和make
new和make区别:
1.make只能用来分配及初始化类型为slice,map,chan的数据;new可以分配任意类型的数据
2.new分配返回的是指针,即类型*T;make返回引用,即T;
3.new分配的空间被清零,make分配后,会进行初始化。
1.new
func testNew() {
b := new(bool)
fmt.Println(*b)
i := new(int)
fmt.Println(*i)
s := new(string)
fmt.Println(*s)
}
func main() {
testNew()
}
运行结果:
false
0
""
2.make
内建函数make(T, args)与new(T)的用途不一样。它只用来创建slice,map和channel,并且返回一个初始化的(而不是置零),类型为T的值(而不是*T)。之所以有所不同,是因为这三个类型的背后引用了使用前必须初始化的数据结构。例如,slice是一个三元描述符,包含一个指向数据(在数组中)的指针,长度,以及容量,在这些项被初始化之前,slice都是nil的。对于slice,map和channel,make初始化这些内部数据结构,并准备好可用的值。
make([]int, 10, 100)
分配一个有100个int的数组,然后创建一个长度为10,容量为100的slice结构,该slice引用包含前10个元素的数组。对应的,new([]int)返回一个指向新分配的,被置零的slice结构体的指针,即指向值为nil的slice的指针。
var p *[]int = new([]int) // allocates slice structure; *p == nil; rarely useful
var v []int = make([]int, 100) // the slice v now refers to a new array of 100 ints
// Unnecessarily complex:这种做法实在是很蛋疼
var p *[]int = new([]int)
*p = make([]int, 100, 100)
// Idiomatic:习惯的做法
v := make([]int, 100)
运行结果:
p: &[]
v: [0 0 0 0 0 0 0 0 0 0]
21.7 标准库bytes
bytes包提供了对字节切片进行读写操作的一系列函数,字节切片处理的函数比较多分为基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数和子切片处理函数等.
21.7.1 常用函数
func main() {
var i int = 1
var j byte = 2
j = byte(i)
fmt.Printf("j: %v\\n", j)
//Contains
b := []byte("duoke360.com") //字符串强转为byte切片
sublice1 := []byte("duoke360")
sublice2 := []byte("DuoKe360")
fmt.Println(bytes.Contains(b, sublice1)) //true
fmt.Println(bytes.Contains(b, sublice2)) //false
//Count
s := []byte("hellooooooooo")
sep1 := []byte("h")
sep2 := []byte("l")
sep3 := []byte("o")
fmt.Println(bytes.Count(s, sep1)) //1
fmt.Println(bytes.Count(s, sep2)) //2
fmt.Println(bytes.Count(s, sep3)) //9
//Repeat
b = []byte("hi")
fmt.Println(string(bytes.Repeat(b, 1))) //hi
fmt.Println(string(bytes.Repeat(b, 3))) //hihihi
//Replace
s = []byte("hello,world")
old := []byte("o")
news := []byte("ee")
fmt.Println(string(bytes.Replace(s, old, news, 0))) //hello,world
fmt.Println(string(bytes.Replace(s, old, news, 1))) //hellee,world
fmt.Println(string(bytes.Replace(s, old, news, 2))) //hellee,weerld
fmt.Println(string(bytes.Replace(s, old, news, -1))) //hellee,weerld
//Runes
s = []byte("你好世界")
r := bytes.Runes(s)
fmt.Println("转换前字符串的长度: ", len(s)) //12
fmt.Println("转换后字符串的长度: ", len(r)) //4
//Join
s2 := [][]byte{[]byte("你好"), []byte("世界")}
sep4 := []byte(",")
fmt.Println(string(bytes.Join(s2, sep4))) //你好,世界
sep5 := []byte("#")
fmt.Println(string(bytes.Join(s2, sep5))) //你好#世界
}
21.7.2 Buffer类型
缓冲区是具有读取和写入方法的可变大小的字节缓冲区。Buffer 的零值是准备使用的空缓冲区。
声明一个Buffer的四种方法:
var b bytes.Buffer // 直接定义一个Buffer变量,不用初始化,可以直接使用
b := new(bytes.Buffer) //使用New返回Buffer变量
b := bytes.NewBuffer(s []byte) //从一个[]byte切片,构造一个Buffer
b := bytes.NewBufferString(s string) //从一个string变量,构造一个Buffer
往Buffer中写入数据
b.Write(d []byte) //将切片d写入Buffer尾部
b.WriteString(s string) //将字符串s写入Buffer尾部
b.WriteByte(c byte) //将字符c写入Buffer尾部
b.WriteRune(r rune) //将一个rune类型的数据放到缓冲器的尾部
b.WriteTo(w io.Writer) //将Buffer中的内容输出到实现了io.Writer接口的可写入对象中
注:将文件中的内容写入Buffer,则使用ReadForm(i io.Reader)
从Buffer中读取数据到指定容器
c := make([]byte,8)
b.Read(c) //一次读取8个byte到c容器中,每次读取新的8个byte覆盖c中原来的内容
b.ReadByte() //读取第一个byte,b的第1个byte被拿掉,赋值给 a => a, _ := b.ReadByte()
b.ReadRune() //读取第一个rune,b的第1个rune被拿掉,赋值给 r => r, _ := b.ReadRune()
b.ReadBytes(delimiter byte) //需要一个 byte作为分隔符 ,读的时候从缓冲器里找第一个出现的分隔符(delim),找到后,把从缓冲器头部开始到分隔符之间的所有byte进行返回,作为byte类型的slice,返回后,缓冲器也会空掉一部分
b.ReadString(delimiter byte) // 需要一个byte作为分隔符,读的时候从缓冲器里找第一个出现的分隔符(delim),找到后,把从缓冲器头部开始到分隔符之间的所有byte进行返回, 作为字符串返回 ,返回后,缓冲器也会空掉一部分b.ReadForm(i io.Reader) // 从一个实现io.Reader接口的r,把r里的内容读到缓冲器里 ,n 返回读的数量
file, _ := os.Open(".text.txt")
buf := bytes.NewBufferString("Hello world")
buf.ReadFrom(file)
//将text.txt内容追加到缓冲器的尾部
fmt.Println(buf.String())
清空数据
b.Reset()
转换为字符串
b.String()
21.7.3 Reader类型
Reader实现了 io.Reader, io.ReaderAt, io.WriterTo, io.Seeker,io.ByteScanner, io.RuneScanner接口,Reader是只读的、可以seek。
func testReader() {
data := "123456789"
//通过[]byte创建Reader
re := bytes.NewReader([]byte(data))
//返回未读取部分的长度
fmt.Println("re len : ", re.Len())
//返回底层数据总长度
fmt.Println("re size : ", re.Size())
fmt.Println("------------")
buf := make([]byte, 2)
for {
//读取数据
n, err := re.Read(buf)
if err != nil {
break
}
fmt.Println(string(buf[:n]))
}
fmt.Println("------------")
//设置偏移量,因为上面的操作已经修改了读取位置等信息
re.Seek(0, 0)
for {
//一个字节一个字节的读
b, err := re.ReadByte()
if err != nil {
break
}
fmt.Println(string(b))
}
fmt.Println("------------")
re.Seek(0, 0)
off := int64(0)
for {
//指定偏移量读取
n, err := re.ReadAt(buf, off)
if err != nil {
break
}
off += int64(n)
fmt.Println(off, string(buf[:n]))
}
}
21.8 标准库errors
errors包实现了操作错误的函数。语言使用 error 类型来返回函数执行过程中遇到的错误,如果返回的 error 值为 nil,则表示未遇到错误,否则 error 会返回一个字符串,用于说明遇到了什么错误。
21.8.1 error 结构
type error interface {
Error() string
}
可以用任何类型去实现它(只要添加一个 Error() 方法即可),也就是说,error 可以是任何类型,这意味着,函数返回的 error 值实际可以包含任意信息,不一定是字符串。
error 不一定表示一个错误,它可以表示任何信息,比如 io 包中就用 error 类型的 io.EOF 表示数据读取结束,而不是遇到了什么错误。
errors 包实现了一个最简单的 error 类型,只包含一个字符串,它可以记录大多数情况下遇到的错误信息。errors 包的用法也很简单,只有一个 New 函数,用于生成一个最简单的 error 对象:
func New(text string) error
package main
import (
"errors"
"fmt"
)
func check(s string) error {
if s == "" {
return errors.New("字符串不能为空")
} else {
return nil
}
}
func main() {
check("hello")
err := check("")
fmt.Printf("err: %v\\n", err.Error())
}
21.8.2 自定义错误
// MyError is an error implementation that includes a time and message.
type MyError struct {
When time.Time
What string
}
func (e MyError) Error() string {
return fmt.Sprintf("%v: %v", e.When, e.What)
}
func oops() error {
return MyError{
time.Date(1989, 3, 15, 22, 30, 0, 0, time.UTC),
"the file system has gone away",
}
}
func main() {
if err := oops(); err != nil {
fmt.Println(err)
}
}
21.9 标准库中的sort包
21.9.1 内容以及使用
sort包提供了排序切片和用户自定义数据集以及相关功能的函数。
sort包主要针对[]int、[]float64、[]string、以及其他自定义切片的排序。
21.9.2 结构体
三种结构体的方法都是一样的,只是分别针对int切片、float64切片、strings切片这三种不同的类型。 然后三种结果都有五个公开方法
type IntSlice []int
type Float64Slice
type StringSlice
func (p xxxSlice) Len() int // 切片长度
func (p xxxSlice) Less(i, j int) bool
func (p xxxSlice) Swap(i, j int)
func (p xxxSlice) Search(x xxx) int
// 这个和后面那个功能一样
func (p xxxSlice) Sort()
21.9.3 函数
func Ints(a []int)
func IntsAreSorted(a []int) bool
func SearchInts(a []int, x int) int
func Float64s(a []float64)
func Float64sAreSorted(a []float64) bool
func SearchFloat64s(a []float64, x float64) int
func SearchFloat64s(a []flaot64, x float64) bool
func Strings(a []string)
func StringsAreSorted(a []string) bool
func SearchStrings(a []string, x string) int
func Sort(data Interface)
func Stable(data Interface)
func Reverse(data Interface) Interface
func ISSorted(data Interface) bool
func Search(n int, f func(int) bool) int
21.9.4 接口 type Interface
type Interface interface {
Len() int // Len方法返回集合中的元素个数
Less(i, j int) bool // i>j,该方法返回索引i的元素是否比索引j的元素小、
Swap(i, j int) // 交换i, j的值
}
// 实例:
type NewInts []uint
func (n NewInts) Len() int {
return len(n)
}
func (n NewInts) Less(i, j int) bool {
fmt.Println(i, j, n[i] < n[j], n)
return n[i] < n[j]
}
func (n NewInts) Swap(i, j int) {
n[i], n[j] = n[j], n[i]
}
func main() {
n := []uint{1,3,2}
sort.Sort(NewInts(n))
fmt.Println(n)
}
21.9.5 综合实例
[]float64:
f := []float64{1.1, 4.4, 5.5, 3.3, 2.2}
sort.Float64s(f)
fmt.Printf("f: %v\\n", f)
// f: [1.1 2.2 3.3 4.4 5.5]
[]int:
f := []int{3, 5, 1, 2, 4}
sort.Ints(f)
fmt.Printf("f: %v\\n", f)
// f: [1 2 3 4 5]
string:
//字符串排序,现比较高位,相同的再比较低位
// [] string
ls := sort.StringSlice{
"100",
"42",
"41",
"3",
"2",
}
fmt.Println(ls) //[100 42 41 3 2]
sort.Strings(ls)
fmt.Println(ls) //[100 2 3 41 42]
//字符串排序,现比较高位,相同的再比较低位
ls := sort.StringSlice{
"d",
"ac",
"c",
"ab",
"e",
}
fmt.Println(ls) //[d ac c ab e]
sort.Strings(ls)
fmt.Println(ls) //[ab ac c d e]
//汉字排序,依次比较byte大小
ls := sort.StringSlice{
"啊",
"博",
"次",
"得",
"饿",
"周",
}
fmt.Println(ls) //[啊 博 次 得 饿 周]
sort.Strings(ls)
fmt.Println(ls) //[博 周 啊 得 次 饿]
for _, v := range ls{
fmt.Println(v, []byte(v))
}
//博 [229 141 154]
//周 [229 145 168]
//啊 [229 149 138]
//得 [229 190 151]
//次 [230 172 161]
//饿 [233 165 191]
复杂结构:[][]int :
type testSlice [][]int
func (l testSlice) Len() int { return len(l) }
func (l testSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l testSlice) Less(i, j int) bool { return l[i][1] < l[j][1] }
func main() {
ls := testSlice{
{1,4},
{9,3},
{7,5},
}
fmt.Println(ls) //[[1 4] [9 3] [7 5]]
sort.Sort(ls)
fmt.Println(ls) //[[9 3] [1 4] [7 5]]
}
复杂结构体:[]map[string]int [{"k":0},{"k1":1},{"k2":2] :
type testSlice []map[string]float64
func (l testSlice) Len() int { return len(l) }
func (l testSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l testSlice) Less(i, j int) bool { return l[i]["a"] < l[j]["a"] } //按照"a"对应的值排序
func main() {
ls := testSlice{
{"a":4, "b":12},
{"a":3, "b":11},
{"a":5, "b":10},
}
fmt.Println(ls) //[map[a:4 b:12] map[a:3 b:11] map[a:5 b:10]]
sort.Sort(ls)
fmt.Println(ls) //[map[a:3 b:11] map[a:4 b:12] map[a:5 b:10]]
}
复杂结构体:[]struct :
type People struct {
Name string
Age int
}
type testSlice []People
func (l testSlice) Len() int { return len(l) }
func (l testSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l testSlice) Less(i, j int) bool { return l[i].Age < l[j].Age }
func main() {
ls := testSlice{
{Name:"n1", Age:12},
{Name:"n2", Age:11},
{Name:"n3", Age:10},
}
fmt.Println(ls) //[{n1 12} {n2 11} {n3 10}]
sort.Sort(ls)
fmt.Println(ls) //[{n3 10} {n2 11} {n1 12}]
}
21.10 标准库time
time包提供测量和显示时间的功能。
21.10.1 基本使用
打印显示出现在的时间,基本示例如下。 其中now为time.Time类型,Month 为time.Month类型
func test1() {
now := time.Now() //获取当前时间
// current time:2020-12-01 22:24:30.85736 +0800 CST m=+0.000096031
fmt.Printf("current time:%v\\n", now)
year := now.Year() //年
month := now.Month() //月
day := now.Day() //日
hour := now.Hour() //小时
minute := now.Minute() //分钟
second := now.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\\n", year, month, day, hour, minute, second)
fmt.Printf("%T,%T,%T,%T,%T,%T,%T\\n", now, year, month, day, hour, minute, second)
// time.Time,int,time.Month,int,int,int,int
}
21.10.2 时间戳
在编程中对于时间戳的应用也尤为广泛,例如在Web开发中做cookies有效期,接口加密,Redis中的key有效期等等,大部分都是使用到了时间戳
时间戳是自1970年1月1日(08:00:00GMT)至当前时间的总毫秒数。它也被称为Unix时间戳(UnixTimestamp)。
在GoLang中,获取时间戳的操作如下:
func test2() {
now := time.Now()
// 当前时间戳 TimeStamp type:int64, TimeStamp:1606832965
fmt.Printf("TimeStamp type:%T, TimeStamp:%v", now.Unix(), now.Unix())
}
除此之外还有纳秒时间戳,我们可以使用time.Now().UnixNano()来获取它
func test3() {
now := time.Now()
// 纳秒级时间戳TimeStamp type:int64, TimeStamp:1606833059999670000
fmt.Printf("TimeStamp type:%T, TimeStamp:%v\\n", now.UnixNano(), now.UnixNano())
}
1.时间戳转化为普通的时间格式
在go语言中可以time.Unix来直接将时间戳转化为当前时间格式,实现瞬间替换。
func timeStampToTime() {
timestamp := time.Now().Unix()
timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
fmt.Println(timeObj)
year := timeObj.Year() //年
month := timeObj.Month() //月
day := timeObj.Day() //日
hour := timeObj.Hour() //小时
minute := timeObj.Minute() //分钟
second := timeObj.Second() //秒
fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\\n", year, month, day, hour, minute, second)
}
21.10.3 操作时间
1.ADD
package main
import (
"fmt"
"time"
)
func add(h, m, s, mls, msc, ns time.Duration) {
now := time.Now()
fmt.Println(now.Add(time.Hour*h + time.Minute*m + time.Second*s + time.Millisecond*mls + time.Microsecond*msc + time.Nanosecond*ns))
}
func main() {
test4(3, 4, 5, 6, 7, 8)
}
注意在这里并不能增加年\月\日,仅能增加时分秒,也就是以下的才被允许
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
2.Sub
func sub() {
now := time.Now()
targetTime := now.Add(time.Hour)
// 目标时间与此时相比相差1h0m0s
fmt.Println(targetTime.Sub(now))
}
谁的sub谁为参照时间
3.Equal
func (t Time) Equal(u Time) bool
判断两个时间是否相同,会考虑时区的影响,因此不同时区标准的时间也可以正确比较。
4.Before
func (t Time) Before(u Time) bool
如果t代表的时间点在u之前,返回真;否则返回假。
5.After
func (t Time) After(u Time) bool
如果t代表的时间点在u之后,返回真;否则返回假。
6.定时器
使用time.Tick(时间间隔)来设置定时器,定时器的本质上是一个通道(channel)。
func tick() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i)//每秒都会执行的任务
}
}
21.10.4 时间格式化
时间类型有一个自带的方法Format进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S而是使用Go的诞生时间2006年1月2号15点04分(记忆口诀为2006 1 2 3 4)
func format() {
now := time.Now()
// 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
// 24小时制
fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
// 12小时制
fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))
fmt.Println(now.Format("2006/01/02"))
}
补充:如果想格式化为12小时方式,需指定
PM。
21.10.5 解析字符串格式的时间
now := time.Now()
fmt.Println(now)
// 加载时区
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println(err)
return
}
// 按照指定时区和指定格式解析字符串时间
timeObj, err := time.ParseInLocation("2006/01/02 15:04:05", "2019/08/04 14:15:20", loc)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(timeObj)
fmt.Println(timeObj.Sub(now))
21.11 标准库encoding/json
这个包可以实现json的编码和解码,就是将json字符串转换为struct,或者将struct转换为json。
21.11.1 两个核心函数
func Marshal(v interface{}) ([]byte, error)
将struct编码成json,可以接收任意类型
func Unmarshal(data []byte, v interface{}) error
将json转码成struct结构体
21.11.2 两个核心结构体
type Decoder struct {
// contains filtered or unexported fields
}
从输入流读取并解析json
type Encoder struct {
// contains filtered or unexported fields
}
写json到输出流
21.11.3 实例
1.结构体转换为json
type Person struct {
Name string
Age int
Email string
}
func Marshal() {
p := Person{
Name: "tom",
Age: 20,
Email: "tom@gmail.com",
}
b, _ := json.Marshal(p)
fmt.Printf("b: %v\\n", string(b))
}
2.json转换为结构体
func Unmarshal() {
b1 := []byte(`{"Name":"tom","Age":20,"Email":"tom@gmail.com"}`)
var m Person
json.Unmarshal(b1, &m)
fmt.Printf("m: %v\\n", m)
}
3.解析嵌套类型
// 解析嵌套类型
func test3() {
b := []byte(`{"Name":"tom","Age":20,"Email":"tom@gmail.com", "Parents":["tom", "kite"]}`)
var f interface{}
json.Unmarshal(b, &f)
fmt.Printf("f: %v\\n", f)
}
4.解析嵌套引用类型
func test4() {
type Person struct {
Name string
Age int
Email string
Parent []string
}
p := Person{
Name: "tom",
Age: 20,
Email: "tom@gmail.com",
Parent: []string{"big tom", "big kite"},
}
b, _ := json.Marshal(p)
fmt.Printf("b: %v\\n", string(b))
}
5.io流 Reader Writer 可以扩展到http websocket等场景
func test5() {
// dec := json.NewDecoder(os.Stdin)
// a.json : {"Name":"tom","Age":20,"Email":"tom@gmail.com", "Parents":["tom", "kite"]}
f, _ := os.Open("a.json")
dec := json.NewDecoder(f)
enc := json.NewEncoder(os.Stdout)
for {
var v map[string]interface{}
if err := dec.Decode(&v); err != nil {
log.Println(err)
return
}
fmt.Printf("v: %v\\n", v)
if err := enc.Encode(&v); err != nil {
log.Println(err)
}
}
/*
输入 {"Name":"tom","Age":20,"Email":"tom@gmail.com"}
输出
v: map[Age:20 Email:tom@gmail.com Name:tom]
{"Age":20,"Email":"tom@gmail.com","Name":"tom"}
*/
}
6.读写文件
func test6() {
f, _ := os.Open("a.json")
defer f.Close()
d := json.NewDecoder(f)
var v map[string]interface{}
d.Decode(&v)
fmt.Printf("v: %v\\n", v)
}
func test7() {
type Person struct {
Name string
Age int
Email string
Parent []string
}
p := Person{
Name: "tom",
Age: 20,
Email: "tom@gmail.com",
Parent: []string{"big tom", "big kite"},
}
f, _ := os.OpenFile("a.json", os.O_WRONLY, 0777)
defer f.Close()
e := json.NewEncoder(f)
e.Encode(p)
}
21.12 标准库encoding/xml
xml包实现xml解析
21.12.1 两个核心函数
func Marshal(v interface{}) ([]byte, error)
将struct编码成xml,可以接收任意类型
func Unmarshal(data []byte, v interface{}) error
将xml转码成struct结构体
21.12.2 两个核心结构体
type Decoder struct {
...
}
从输入流读取并解析xml
type Encoder struct {
// contains filtered or unexported fields
}
写xml到输出流
21.12.3 实例
1.结构体转换为xml
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name"`
Age int `xml:"age"`
Email string `xml:"email"`
}
func Marshal() {
p := Person{
Name: "tom",
Age: 20,
Email: "tom@gmail.com",
}
// b, _ := xml.Marshal(p)
// 有缩进格式
b, _ := xml.MarshalIndent(p, " ", " ")
fmt.Printf("%v\\n", string(b))
}
2.读写文件
func read() {
/*
<?xml version="1.0" encoding="UTF-8"?>
<person>
<name>tom</name>
<age>20</age>
<email>tom@gmail.com</email>
</person>
*/
b, _ := ioutil.ReadFile("a.xml")
var p Person
xml.Unmarshal(b, &p)
fmt.Printf("p: %v\\n", p)
}
func write() {
type Person struct {
XMLName xml.Name `xml:"person"`
Name string `xml:"name"`
Age int `xml:"age"`
Email string `xml:"email"`
}
p := Person{
Name: "tom",
Age: 20,
Email: "tom@gmail.com",
}
f, _ := os.OpenFile("a.xml", os.O_WRONLY, 0777)
defer f.Close()
e := xml.NewEncoder(f)
e.Encode(p)
}
21.13 标准库math
包含一些常量和一些有用的数学计算函数,例如:三角函数、随机数、绝对值、平方根等。
21.13.1 常量
fmt.Printf("float64的最大值是:%.f\\n", math.MaxFloat64)
fmt.Printf("float64的最小值是:%.f\\n", math.SmallestNonzeroFloat64)
fmt.Printf("float32的最大值是:%.f\\n", math.MaxFloat32)
fmt.Printf("float32的最小值是:%.f\\n", math.SmallestNonzeroFloat32)
fmt.Printf("Int8的最大值是:%d\\n", math.MaxInt8)
fmt.Printf("Int8的最小值是:%d\\n", math.MinInt8)
fmt.Printf("Uint8的最大值是:%d\\n", math.MaxUint8)
fmt.Printf("Int16的最大值是:%d\\n", math.MaxInt16)
fmt.Printf("Int16的最小值是:%d\\n", math.MinInt16)
fmt.Printf("Uint16的最大值是:%d\\n", math.MaxUint16)
fmt.Printf("Int32的最大值是:%d\\n", math.MaxInt32)
fmt.Printf("Int32的最小值是:%d\\n", math.MinInt32)
fmt.Printf("Uint32的最大值是:%d\\n", math.MaxUint32)
fmt.Printf("Int64的最大值是:%d\\n", math.MaxInt64)
fmt.Printf("Int64的最小值是:%d\\n", math.MinInt64)
fmt.Printf("圆周率默认为:%.200f\\n", math.Pi)
21.13.2 常用函数
func main() {
/*
取绝对值,函数签名如下:
func Abs(x float64) float64
*/
fmt.Printf("[-3.14]的绝对值为:[%.2f]\\n", math.Abs(-3.14))
/*
取x的y次方,函数签名如下:
func Pow(x, y float64) float64
*/
fmt.Printf("[2]的16次方为:[%.f]\\n", math.Pow(2, 16))
/*
取余数,函数签名如下:
func Pow10(n int) float64
*/
fmt.Printf("10的[3]次方为:[%.f]\\n", math.Pow10(3))
/*
取x的开平方,函数签名如下:
func Sqrt(x float64) float64
*/
fmt.Printf("[64]的开平方为:[%.f]\\n", math.Sqrt(64))
/*
取x的开立方,函数签名如下:
func Cbrt(x float64) float64
*/
fmt.Printf("[27]的开立方为:[%.f]\\n", math.Cbrt(27))
/*
向上取整,函数签名如下:
func Ceil(x float64) float64
*/
fmt.Printf("[3.14]向上取整为:[%.f]\\n", math.Ceil(3.14))
/*
向下取整,函数签名如下:
func Floor(x float64) float64
*/
fmt.Printf("[8.75]向下取整为:[%.f]\\n", math.Floor(8.75))
/*
取余数,函数签名如下:
func Floor(x float64) float64
*/
fmt.Printf("[10/3]的余数为:[%.f]\\n", math.Mod(10, 3))
/*
分别取整数和小数部分,函数签名如下:
func Modf(f float64) (int float64, frac float64)
*/
Integer, Decimal := math.Modf(3.14159265358979)
fmt.Printf("[3.14159265358979]的整数部分为:[%.f],小数部分为:[%.14f]\\n", Integer, Decimal)
}
21.13.3 随机数
func init() {
//以时间作为初始化种子
rand.Seed(time.Now().UnixNano())
}
func main() {
for i := 0; i < 10; i++ {
a := rand.Int()
fmt.Println(a)
}
fmt.Println("------------")
for i := 0; i < 10; i++ {
a := rand.Intn(100)
fmt.Println(a)
}
fmt.Println("------------")
for i := 0; i < 10; i++ {
a := rand.Float32()
fmt.Println(a)
}
}
22 Golang操作MySQL数据库
22.1 准备数据库和表
1.下载安装MySQL
https://dev.mysql.com/downloads/mysql/
2.创建并打开数据库
1.创建一个go_db数据库
create database go_db;
2.打开数据库
use go_db;
3.创建表
create table user_tbl (
id int primary key auto_increment,
username varchar(20) not null unique comment '用户名',
password varchar(100) not null comment '加密后的用户密码'
) comment='用户信息表';
1. 整体作用
create table 表名:创建一张数据表,表名叫 user_tbl(用户表)。
2. id integer primary key auto_increment
id:字段名,存储用户编号 integer:数据类型,整数
3. primary key:主键约束,保证每条数据的id唯一,用来唯一区分每一行用户
auto_increment:自增,插入数据时不用手动填id,数据库会自动按 1、2、3、4……递增赋值
4.username varchar(20)
username:字段名,用户名 varchar(20):可变长度字符串,最多存20个字符,用户名长度不超过20
5.password varchar(20)
password:字段名,密码 varchar(20):密码最多20个字符
6. 补充说明
not null,用户名、密码可以为空;
密码明文存储,真实业务需要加密(MD5、bcrypt);
用户名加唯一约束,不会出现重名用户;
Go代码连接MySQL后,就能对这张表做:新增用户、根据id查询、修改密码、删除用户等操作。
4.查看表结构
desc user_tbl;
5.添加模拟数据
insert into user_tbl (username, password) values ("Tom","Password@123");
insert into user_tbl (username, password) values ("Kite","Password@456");
22.2 安装配置mysql驱动

1.安装驱动
路径:https://pkg.go.dev/ — Packages — 搜索mysql — mysql (github.com/go-sql-driver/mysql)
go get -u github.com/go-sql-driver/mysql
PS D:\Go_Project> go get -u github.com/go-sql-driver/mysql
go: added filippo.io/edwards25519 v1.2.0
go: added github.com/go-sql-driver/mysql v1.10.0
2.初始化模块
go mod init m
3.执行go mod tidy
go mod tidy
4.导入驱动
package main
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
// See "Important settings" section.
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
}
22.3 获得数据库连接
1.导入包
package main
import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
2.获得连接
import (
"database/sql"
"fmt"
"time"
_ "github.com/go-sql-driver/mysql"
)
func main() {
db, err := sql.Open("mysql", "root:MySql9.7#Go2026@/go_db")
if err != nil {
panic(err)
}
db.SetConnMaxLifetime(time.Minute * 3) // 最大连接时长
db.SetMaxOpenConns(10) // 最大连接数
db.SetMaxIdleConns(10) // 空闲连接数
fmt.Printf("db: %v\n", db)
}
3.初始化连接
Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接。如果要检查数据源的名称是否真实有效,应该调用Ping方法。
返回的DB对象可以安全地被多个goroutine并发使用,并且维护其自己的空闲连接池。因此,Open函数应该仅被调用一次,很少需要关闭这个DB对象。
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
// 定义一个全局对象db
var db *sql.DB
func initDB() (err error) {
// MySQL 8.x
// dsn := "root:123456@tcp(127.0.0.1:3306)/go_db?charset=utf8mb4&parseTime=True"
// MySQL 9.x
dsn := "root:MySql9.7#Go2026@tcp(127.0.0.1:3306)/go_db?charset=utf8mb4&parseTime=true&loc=Local"
// 不会校验账号密码是否正确
// 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
db, err = sql.Open("mysql", dsn)
if err != nil {
return err
}
// 尝试与数据库建立连接(校验dsn是否正确)
err = db.Ping()
if err != nil {
return err
}
return nil
}
func main() {
err := initDB() // 调用输出化数据库的函数
if err != nil {
fmt.Printf("连接失败!,err:%v\n", err)
return
} else {
fmt.Printf("连接成功!")
}
}
22.4 查询操作
22.4.1 单行查询
单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。
// 定义一个结构体
type User struct {
id int
username string
password string
}
// 查询一条用户数据
func queryOneRow() {
sqlStr := "select id, username, password from user_tbl where id = ?"
var u User
// 确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
err := db.QueryRow(sqlStr, 2).Scan(&u.id, &u.username, &u.password)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
//fmt.Printf("id:%d username:%s password:%s\n", u.id, u.username, u.password)
fmt.Printf("u: %v\n", u)
}
22.4.2 查询多行
多行查询db.Query()执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。
// 定义一个结构体
type User struct {
id int
username string
password string
}
// 查询多条用户数据
func queryMultiRow() {
sqlStr := "select id, username, password from user_tbl where id > ?"
rows, err := db.Query(sqlStr, 0)
if err != nil {
fmt.Printf("query failed, err:%v\n", err)
return
}
defer rows.Close() // 关闭rows释放是有的数据库连接
// 循环读取结果中的数据
for rows.Next() {
var u User
err := rows.Scan(&u.id, &u.username, &u.password)
if err != nil {
fmt.Printf("scan failed, err:%v\n", err)
return
}
//fmt.Printf("id:%d user:%s password:%s\n", u.id, u.username, u.password)
fmt.Printf("u: %v\n", u)
}
// 额外判断循环结束后的错误
err = rows.Err()
if err != nil {
fmt.Printf("rows err: %v\n", err)
}
}
22.5 插入数据
插入、更新和删除操作都使用Exec方法。
// func (db *DB) Exec(query string, args ...interface{}) (Result, error)
// 方法一:
func insertData() {
sqlStr := "insert into user_tbl(username,password) values(?,?)"
ret, err := db.Exec(sqlStr, "王大锤", "Password@789")
if err != nil {
fmt.Printf("insert data failed, err:%v\n", err)
return
}
theID, err := ret.LastInsertId() // 新插入数据的id
if err != nil {
fmt.Printf("get lastinsert ID failed, err:%v\n", err)
return
}
fmt.Printf("insert success, the id is %d.\n", theID)
}
// 方法2:
func insertData2(username string, password string) {
sqlStr := "insert into user_tbl(username,password) values(?,?)"
ret, err := db.Exec(sqlStr, username, password)
if err != nil {
fmt.Printf("insert data failed, err:%v\n", err)
return
}
theID, err := ret.LastInsertId() // 新插入数据的id
if err != nil {
fmt.Printf("get lastinsert ID failed, err:%v\n", err)
return
}
fmt.Printf("insert success, the id is %d.\n", theID)
}
22.6 删除数据
插入、更新和删除操作都使用Exec方法。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
func delData() {
sqlStr := "delete from user_tbl where id=?"
ret, err := db.Exec(sqlStr, 1)
if err != nil {
fmt.Printf("删除失败, err: %v\n", err)
return
}
rows, err := ret.RowsAffected()
if err != nil {
fmt.Printf("删除行失败, err: %v\n", err)
return
}
fmt.Printf("删除成功,删除的行数: %d\n", rows)
}
22.7 更新数据
插入、更新和删除操作都使用Exec方法。
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
func updateData() {
sqlStr := "update user_tbl set username=?, password=? where id=?"
ret, err := db.Exec(sqlStr, "老王", "Password@000", "3")
if err != nil {
fmt.Printf("更新失败, err:%v\n", err)
return
}
rows, err := ret.RowsAffected()
if err != nil {
fmt.Printf("更新行失败, err:%v\n", err)
return
}
fmt.Printf("更新成功, 更新的行数: %d.\n", rows)
}