Golang基础教程


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语言的公司(项目)

公司 项目
Google Kubernetes
Facebook 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
print 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 也有基于架构的类型,例如:intuintuintptr

  类型的长度是根据运行程序所在的操作系统类型所决定的

   intuint 在 32 位操作系统上,均使用 32 位(4 个字节),在 64 位操作系统上使用 64 位(8 个字节)。

   uintptr 的长度被设定为足够存放一个指针即可。

  Go 语言中没有 float 类型。

  Go语言中只有 float32float64,没有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语言支持两种浮点型数:float32float64,这两种浮点型数据格式遵循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语言中的布尔类型有两个常量值:truefalse。布尔类型经常用在条件判断语句循环语句,也可用在逻辑表达式中。

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 嵌套语句: 在 ifelse if 语句中嵌入一个或多个 ifelse if 语句。

  4.switch 语句: switch 语句用于基于不同条件执行不同动作。

  5.select 语句: select 语句类似于 switch 语句,但select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

  6.for循环与for range循环:Go语言中的循环只有for循环,去除了whiledo 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. 左括号必须在ifelse的同一行
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关键字,去除了像其他语言中的whiledo 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循环可以通过breakgotoreturnpanic语句强制退出循环。

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语句可以结束forswitchselect的代码块。

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,existsreturn value,okreturn 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的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。

  mapsliceinterfacechannel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。

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函数类型,把summax赋值给它。

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}

注意:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。

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系列。

函数系列 作用
print 单纯打印日志
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

  newmake区别:

1.make只能用来分配及初始化类型为slicemapchan的数据;new可以分配任意类型的数据

2.new分配返回的是指针,即类型*Tmake返回引用,即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,数据库会自动按 1234……递增赋值

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)
}

文章作者: 罗宇
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 罗宇 !
  目录