a tour of go
golang-china/gopl-zh: :books: Go语言圣经中文版 (github.com) → Go语言圣经 - Go语言圣经 (gopl-zh.github.io)
【尚硅谷】Golang入门到实战教程丨一套精通GO语言_哔哩哔哩_bilibili
https://www.alipan.com/s/1YRzZZ8szDX 提取码: 1fw8
Go语言的特点 :
Go语言保证了既能到达静态编译语言的安全和性能,又达到了动态语言开发维护的高效率,使用一个表达式来形容 Go语言:Go = C + Python
,说明 Go语言既有C静态语言程序的运行速度,又能达到 Python动态语言的快速开发。
从C语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型调用参数传值,指针等等,也保留了和C语言一样的编译执行方式及弱化的指针。
引入包的概念,用于组织程序结构,Go语言的一个文件都要归属于一个包,而不能单独存在。
垃圾回收机制,内存自动回收,不需开发人员管理
天然并发
(1) 从语言层面支持并发,实现简单
(2) goroutine,轻量级线程,可实现大并发处理,高效利用多核
(3) 基于CPS并发模型(Communicating Sequential Processes)实现
吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel,可以实现不同的 goroute之间的相互通信。
函数可以返回多个值。
新的创新:切片 slice、延时执行 defer 等
什么是静态语言,什么是动态语言 静态语言 (Static languages)和动态语言 (Dynamic languages)是两种程序设计语言的类型,主要区别在于它们如何处理变量的类型和执行时间。
静态语言 :
静态语言要求在编译时确定所有变量的类型,即在程序运行之前。这种语言的优点是能够在编译阶段发现类型错误,从而提高程序的稳定性和效率。由于编译器可以优化生成的代码,所以通常静态语言的执行速度比动态语言更快。典型的静态语言包括C、C++、Java和Go。
动态语言 :
动态语言允许在运行时动态地更改变量的类型,不需要在代码中显式声明类型。这增加了编程的灵活性和简便性,使得编程更快速、更容易实现复杂功能。然而,这也可能导致运行时错误,因为错误只有在实际运行相应代码时才会被发现。动态语言的例子包括Python、Ruby和JavaScript。
总的来说,选择使用静态语言还是动态语言取决于项目的需求、团队的偏好以及特定情况下的性能要求。静态语言适用于需要高性能和稳定性的大型项目,而动态语言则适合快速开发和原型设计。
1 配置Go开发环境
安装配置SDK
SDK 的全称(S oftware D evelopment K it软件开发工具包)
SDK是提供给开发人员使用的,其中包含了对应开发语言的工具包
参考:VScode下配置Go语言开发环境【2023最新】_vscode go-CSDN博客
平台:window10
ide:vscode
go版本:1.22.4
1.1 下载并安装go
Go下载 - Go语言中文网 - Golang中文社区 (studygolang.com)
选择下载对应的版本,如下:
文件名
类型
操作系统(OS)
架构(Arch)
大小
SHA256 Checksum
……
……
……
……
……
……
go1.22.4.windows-amd64.msi
Installer
Windows
x86-64
60MB
3c21105d7b584759b6e266383b777caf6e87142d304a10b539dbc66ab482bb5f
……
……
……
……
……
……
运行刚才下载的按照程序。(我自定义安装在了D:\go\1.22.4
)
安装完成后,通过查看安装版本来检查安装情况,使用命令go version
:
1 2 C:\Users\GGW_2021>go version go version go1.22.4 windows/amd64
1.2 配置环境变量
需要配置什么内容:
环境变量
说明
GOROOT
指定SDK的安装路径 D:\go\1.22.4
Path
添加SDK的/bin目录【安装版直接配好了】
GOPATH
工作目录,将来我们的go项目的工作路径
未配置前 :运行命令go env
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 C:\Users\GGW_2021>go env set GO111MODULE= set GOARCH=amd64 set GOBIN= set GOCACHE=C:\Users\GGW_2021\AppData\Local\go-build set GOENV=C:\Users\GGW_2021\AppData\Roaming\go\env set GOEXE=.exe set GOEXPERIMENT= set GOFLAGS= set GOHOSTARCH=amd64 set GOHOSTOS=windows set GOINSECURE= set GOMODCACHE=C:\Users\GGW_2021\go\pkg\mod set GONOPROXY= set GONOSUMDB= set GOOS=windows set GOPATH=C:\Users\GGW_2021\go set GOPRIVATE= set GOPROXY=https://proxy.golang.org,direct set GOROOT=D:\go\1.22.4 set GOSUMDB=sum.golang.org set GOTMPDIR= set GOTOOLCHAIN=auto set GOTOOLDIR=D:\go\1.22.4\pkg\tool\windows_amd64 set GOVCS= set GOVERSION=go1.22.4 set GCCGO=gccgo set GOAMD64=v1 set AR=ar set CC=gcc set CXX=g++ set CGO_ENABLED=1 set GOMOD=NUL set GOWORK= set CGO_CFLAGS=-O2 -g set CGO_CPPFLAGS= set CGO_CXXFLAGS=-O2 -g set CGO_FFLAGS=-O2 -g set CGO_LDFLAGS=-O2 -g set PKG_CONFIG=pkg-config set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\GGW_2021\AppData\Local\ Temp\go-build2882896515=/tmp/go-build -gno-record-gcc-switches
配置环境变量 :
添加两个系统变量
变量名写 GOROOT
变量值写 Go 语言安装目录
变量名写 GOPATH
变量值写你自定义的目录
GOPATH 就是你以后 go 项目存放的路径,即工作目录。
把用户变量里的 GOPATH 也换成自定义的目录:
编辑用户变量里的 Path:
原本是 %USERPROFILE%\go\bin
,改成 %GOPATH%\bin
:
在 GOPATH 对应的目录下(我的是 D:\go\go_path_1.22.4
)新建三个文件夹:bin、pkg、src:
src :这个目录用于存放源代码(.go 文件)。按照 Go 的约定,所有的 Go 项目和库都应该放在这个目录下。这是因为 Go 的包导入机制依赖于这种目录结构来查找和引用其他项目或第三方库。src
目录下存放项目代码是一种早期的约定,目的是为了统一代码存放的位置和方式,便于工具链的管理和操作。但随着模块的引入,这一约定的必要性已经降低。
pkg :这个目录用来存放编译后的包文件(.a 文件),以便可以被其他项目引用,而不需要重新编译。
bin :编译后的可执行文件(二进制文件)会被放在这里。
一般的话可以在系统变量Path中看到 Go 的路径已经配置好了,我们不需要动,这是 Go 在安装时自动配置的。【不用动,之前安装程序自动配好的】之前用 go version
查看版本号也是因为这里的环境变量,如果以后出现命令未找到等问题,可以回到这里检查。
但是这里我把D:\go\1.22.4\bin
修改成了%GOROOT%\bin
输入 go env
可以检查一下,应该是和我们前面设置的一致:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 C:\Users\GGW_2021>go env set GO111MODULE= set GOARCH=amd64 set GOBIN= set GOCACHE=C:\Users\GGW_2021\AppData\Local\go-build set GOENV=C:\Users\GGW_2021\AppData\Roaming\go\env set GOEXE=.exe set GOEXPERIMENT= set GOFLAGS= set GOHOSTARCH=amd64 set GOHOSTOS=windows set GOINSECURE= set GOMODCACHE=D:\go\go_path_1.22.4\pkg\mod set GONOPROXY= set GONOSUMDB= set GOOS=windows set GOPATH=D:\go\go_path_1.22.4 set GOPRIVATE= set GOPROXY=https://proxy.golang.org,direct set GOROOT=D:\go\1.22.4 set GOSUMDB=sum.golang.org set GOTMPDIR= set GOTOOLCHAIN=auto set GOTOOLDIR=D:\go\1.22.4\pkg\tool\windows_amd64 set GOVCS= set GOVERSION=go1.22.4 set GCCGO=gccgo set GOAMD64=v1 set AR=ar set CC=gcc set CXX=g++ set CGO_ENABLED=1 set GOMOD=NUL set GOWORK= set CGO_CFLAGS=-O2 -g set CGO_CPPFLAGS= set CGO_CXXFLAGS=-O2 -g set CGO_FFLAGS=-O2 -g set CGO_LDFLAGS=-O2 -g set PKG_CONFIG=pkg-config set GOGCCFLAGS=-m64 -mthreads -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=C:\Users\GGW_2021\AppData\Local\ Temp\go-build3681508812=/tmp/go-build -gno-record-gcc-switches
可见上面的内容已经修改了【主要检查这两个】:
1 2 3 4 5 …… set GOPATH=D:\go\go_path_1.22.4 …… set GOROOT=D:\go\1.22.4 ……
1.3 安装vscode相关扩展
安装 Go 扩展【名字就叫Go】
更新 Go 工具:
ctrl + shift + P
搜索 Go: Install/Update Tools
全部选择好后,点确定后开始安装
超时问题:vscode 安装go环境无法安装gopls等插件,响应超时、失去连接等问题的简单解决方案_golang gopls无法连接到-CSDN博客
vscode 底部的输出端可以看到安装情况
可以看到有一些 exe 文件被安装到了我设置的 D:\go\go_path_1.22.4\bin
目录下
2 GO 概述
2.1 Go程序开发和基本结构说明
2.1.1 目录结构
项目写在GOPATH下的\src\go_code
下,我的是D:\go\go_path_1.22.4\src\go_code
尽管 Go 模块 已经成为推荐的依赖管理和项目结构方式,有些开发者或项目可能仍然在使用旧的项目结构习惯,特别是在较旧的代码库或团队中。这可能是为了保持与现有工作流程的兼容性。
为了学习,我们先用这个旧的项目结构习惯 。
我们在这个目录下在新建一个project1目录,并在project1下新建一个main.go,
1 2 3 D:\GO\GO_PATH_1.22.4\SRC\GO_CODE └─project1 main.go
2.1.2 go程序结构
main.go内容为:
1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("Hello, World!" ) }
(1) go 文件的后缀是 .go
(2) package main
:表示该 hello.go 文件所在的包是 main,在 go 中,每个文件都必须归属于一个包。
(3) import "fmt"
:表示引入一个包,包名 fmt,引入该包后,就可以使用 fmt 包的函数,如: fmt.Println
(4) func
是一个关键字,表示一个函数。main
是函数名,是一个主函数,即我们程序的入口。
2.1.3 编译运行
通过 go build
命令对该 go 文件进行编译,生成 .exe 文件;运行 main.exe 文件即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 PS D:\go\go_path_1.22.4\src\go_code\project1> dir 目录: D:\go\go_path_1.22.4\src\go_code\project1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2024-06-22 12:16 84 main.go PS D:\go\go_path_1.22.4\src\go_code\project1> go build main.go PS D:\go\go_path_1.22.4\src\go_code\project1> dir 目录: D:\go\go_path_1.22.4\src\go_code\project1 Mode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2024-06-22 12:24 1988096 main.exe -a---- 2024-06-22 12:16 84 main.go PS D:\go\go_path_1.22.4\src\go_code\project1> .\main.exe Hello, World! PS D:\go\go_path_1.22.4\src\go_code\project1>
通过 go run
命令可以直接运行 main.go 程序【类似执行一个脚本文件的形式】
1 2 3 PS D:\go\go_path_1.22.4\src\go_code\project1> go run main.go Hello, World! PS D:\go\go_path_1.22.4\src\go_code\project1>
两种执行流程的方式区别 :
1、如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有 go 开发环境的机器上,仍然可以运行
2、如果我们是直接 go run 源代码,那么如果要在另外一个机器上这么运行,也需要 go 开发环境,否则无法执行。
3、在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多。
2.1.4 注意事项
Go 应用程序的执行入口是 main()
函数。 这个是和其它编程语言(比如 java/c)类似。
Go 语言严格区分大小写。
Go 方法由一条条语句构成,每个语句后不需要分号 (Go 语言会在每行后自动加分号),这也体现出 Golang 的简洁性。
Go 编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个 ,否则报错。
go 语言定义的变量 或者 import 的包如果没有使用到,代码不能编译通过 。
2.2 转义字符
转义字符
描述
是否在 Go 和 C 中共有
\n
换行符
是
\r
回车符
是
\t
水平制表符(Tab)
是
\\
反斜杠
是
\"
双引号(在字符串中使用)
是
\'
单引号(在字符中使用)
是
\b
退格符
是
\f
换页符
是
\xhh
十六进制值表示的字符
是
\uhhhh
4位十六进制表示的 Unicode 字符
仅在 Go
\Uhhhhhhhh
8位十六进制表示的 Unicode 字符
仅在 Go
eg :
以下是在 Go 语言中使用 \xhh
、\uhhhh
和 \Uhhhhhhhh
转义序列的例子:
\xhh
:
用法:表示一个由两位十六进制数 hh
指定的字符。
示例代码:表示一个 ASCII 的 “A”,十六进制为 41。1 2 3 4 5 package mainimport "fmt" func main () { fmt.Println("\x41" ) }
\uhhhh
:
用法:表示一个 Unicode 码点为 hhhh
的字符,其中 hhhh
是四位十六进制数。
示例代码:表示一个中文字符 “中”,其 Unicode 码点为 4E2D。1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("\u4E2D" ) }
\Uhhhhhhhh
:
用法:表示一个 Unicode 码点为 hhhhhhhh
的字符,其中 hhhhhhhh
是八位十六进制数。
示例代码:表示一个表情符号 “😊”,其 Unicode 码点为 1F60A。1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("\U0001F60A" ) }
2.3 注释
行注释
块注释(多行注释)
注意块注释不能嵌套使用
2.4 规范的代码风格
注释 :
缩进和空白 :
VScode:使用一次 tab 操作,实现缩进,默认整体向右边移动,时候用 shift+tab 整体向左移
使用命令进行缩进
前者只是显示缩进后的结果,后者回写回。
运算符两边习惯性各加一个空格。比如:2 + 4 * 5
Go 设计者思想:一个问题尽量只有一个解决方法 。
所以统一了风格:
这种可以:
1 2 3 4 5 6 7 package mainimport "fmt" func main () { fmt.Println("hello world" ) }
这种不行 :
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { fmt.Println("hello world" ) }
一行最长不超过 80个字符,超过的请使用换行展示,尽量保持格式优雅。
2.5 官方
Golang 中文网 在线标准库文档: https://studygolang.com/pkgdoc
2.6 字面量
2.6.1 进制
Golang 和 C/C++ 在进制字面量表示上有一些相似之处,但也存在一些显著的不同。以下是 Go 和 C/C++ 在进制字面量表示方面的详细对比:
b2
二进制 :
Go :使用前缀 0b
或 0B
表示二进制字面量。
C/C++ :从 C++14 开始,使用前缀 0b
或 0B
表示二进制字面量。在 C 中不支持二进制字面量。
Go 示例 :
1 2 var bin = 0 b1101 fmt.Printf("%d\n" , bin)
C++ 示例 :
1 2 3 4 5 6 #include <iostream> int main () { int bin = 0b1101 ; std::cout << bin << std::endl; return 0 ; }
八进制 :
Go :使用前缀 0
表示八进制字面量。
C/C++ :使用前缀 0
表示八进制字面量。
Go 示例 :
1 2 var oct = 015 fmt.Printf("%d\n" , oct)
C/C++ 示例 :
1 2 3 4 5 6 #include <iostream> int main () { int oct = 015 ; std::cout << oct << std::endl; return 0 ; }
十进制 :
Go :直接使用数字表示十进制字面量。
C/C++ :直接使用数字表示十进制字面量。
Go 示例 :
1 2 var dec = 13 fmt.Printf("%d\n" , dec)
C/C++ 示例 :
1 2 3 4 5 6 #include <iostream> int main () { int dec = 13 ; std::cout << dec << std::endl; return 0 ; }
十六进制 :
Go :使用前缀 0x
或 0X
表示十六进制字面量。
C/C++ :使用前缀 0x
或 0X
表示十六进制字面量。
Go 示例 :
1 2 var hex = 0xD fmt.Printf("%d\n" , hex)
C/C++ 示例 :
1 2 3 4 5 6 #include <iostream> int main () { int hex = 0xD ; std::cout << hex << std::endl; return 0 ; }
Go 特有的表示方式 :
Go 还支持一些 C/C++ 中不常见或不支持的特性,例如带有下划线分隔符的数字字面量,以提高可读性。这在 Go 中是一种很实用的特性,可以用于所有进制的数字字面量。
Go 示例 :
1 2 var largeNumber = 1 _000_000 fmt.Printf("%d\n" , largeNumber)
2.7 代码换行
在Go语言中,代码的换行处理和一些其他语言(如Python)有所不同。Go使用一个称为自动分号插入 的机制来处理代码的换行,这是在Go的语言规范中明确定义的。这种机制影响了代码的编写风格,以及在某些情况下需要特别注意的换行规则。
自动分号插入规则
Go编译器会在特定情况下在源代码的行尾自动插入分号(;
),这是Go程序的语句终止符。根据Go语言规范,如果某一行的最后一个词法单元(token)是以下之一,编译器将在该行末自动插入分号:
一个标识符(如变量名)
一个整数、浮点数、虚数、字符或字符串字面量
关键字 break
、continue
、fallthrough
或 return
运算符和分隔符 ++
、--
、)
、]
示例与影响
这意味着,当你在写Go代码时,通常不需要在语句末尾添加分号,编译器会自动处理。然而,这种机制也可能导致一些不直观的问题,特别是在你想要在多行上分布一个表达式或语句时。
例如,以下代码会导致编译错误:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { var s = "hello" + " world" fmt.Println(s) }
上述代码中,"hello"
后面的换行导致编译器在该行自动插入分号,使得 + " world"
成为一个新的、独立的(且无效的)语句。
为了正确编写,你应该这样做:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { var s = "hello" + " world" fmt.Println(s) }
在这个修正后的例子中,+
出现在第一行末尾,避免了自动插入分号,允许表达式跨多行。
最佳实践
保持操作符在行末 :在多行表达式中,把操作符放在行末可以防止自动分号插入的问题。
使用括号 :在复杂表达式中使用括号不仅可以提高代码的可读性,也有助于避免因自动分号插入引起的错误。
通过理解和适应Go的这种换行处理,你可以更加有效地编写Go代码,并避免一些常见的陷阱。
3 变量
3.1 变量的声明
使用var
关键字 :这是最基本的变量声明方式,可以只声明不初始化,也可以在声明的同时初始化。
仅声明变量,不初始化(变量将初始化为默认值):1 2 3 var a int var b string var c bool
声明并初始化变量:1 2 3 var a int = 10 var b string = "hello" var c bool = true
类型推断 :使用var
关键字时,如果初始化了变量,可以省略类型,让编译器自动推断类型。
1 2 3 var a = 10 var b = "hello" var c = true
短变量声明 :在函数内部,可以使用:=
语法进行更简洁的变量声明和初始化。这种方式不能在函数外部使用。
1 2 3 a := 10 b := "hello" c := true
使用new
关键字 :通过new
关键字,可以创建一个指向类型零值的指针。
使用make
关键字 :用于内建的引用类型(如切片、映射和通道)的内存分配。make
只适用于这三种类型。
1 2 3 s := make ([]int , 10 ) m := make (map [string ]int ) ch := make (chan int )
每种方法有其适用场景,选择合适的声明方式可以让代码更清晰、更符合Go语言的习惯。
3.2 多变量声明
在Go语言中,可以同时声明多个变量,这样做可以使代码更简洁且易于维护。以下是几种常见的多变量声明方式:
使用var
关键字
你可以使用var
关键字来同时声明多个变量。这种方式可以用于全局变量的声明或者在函数内部。
使用短变量声明
在函数内部,你可以使用短变量声明:=
来同时声明并初始化多个变量。这种方法更加简洁,常用于局部变量的声明。
1 2 x, y := 100 , 200 name, age := "Alice" , 30
ps:
1 2 3 i := 10 j := 20 i, j = j, i
执行过程可以分解为以下步骤:
1) 右侧求值 :首先,表达式右侧的值被求值。这意味着 j
和 i
的当前值被取出并临时存储(在这个过程中,j
的值是 20
,i
的值是 10
)。
2) 元组赋值 :赋值操作从左到右进行。根据右侧的求值结果,j
的值(20
)被赋给 i
,i
的值(10
)被赋给 j
。
3) 结果 :i
的值变成了 20
,而 j
的值变成了 10
。
值的注意 的是:=
要求至少有一个新变量被声明,所以会有如下情况:
使用new
关键字
虽然new
关键字通常用于单个变量的声明,但你可以连续使用多次new
来声明多个指针变量。
1 2 p1, p2 := new (int ), new (float64 ) *p1, *p2 = 10 , 3.14
使用make
关键字
对于切片、映射和通道这些内建的引用类型,可以使用make
来同时声明并初始化。
1 2 3 s1, s2 := make ([]int , 0 ), make ([]int , 10 ) m1, m2 := make (map [string ]int ), make (map [string ]float64 ) ch1, ch2 := make (chan int ), make (chan string )
3.3 分组声明
在Go语言中,你提到的这种使用var
关键字的变量声明方式是一个分组声明,它允许你在一个括号内分组声明多个变量。这种方法不仅可以用来声明局部变量,也常用于声明全局变量,使代码更加整洁且易于维护。
分组声明的格式 :
使用var
分组声明的基本格式如下:
1 2 3 4 5 6 var ( variable1 = value1 variable2 = value2 variable3 = value3 )
这种格式可以在全局或局部作用域中使用,依据变量声明的位置而定。变量类型可以显式指定,也可以省略让Go自动推断。
例子 :
在你的例子中:
1 2 3 4 5 var ( n3 = 300 n4 = 900 name2 = "mary" )
这里,n3
和n4
被推断为int
类型,而name2
被推断为string
类型。这些变量可以是全局变量或局部变量,具体取决于声明它们的代码块的位置。
优点 :
使用分组声明变量的好处包括:
可读性提升 :将相关变量分组可以提高代码的可读性,特别是在声明多个全局变量时。
组织性强 :帮助程序员在逻辑上组织代码,尤其是在处理多个变量时,可以很容易地查看哪些变量是一组的。
简洁性 :避免重复使用多个var
关键字,代码看起来更简洁。
分组变量声明是Go语言中一种风格上的选择,广泛应用于各种Go程序中,有助于维护大型项目的清晰度和结构性。
3.4 数据类型的基本介绍
每一种数据都定义了明确的数据类型,在内存中分配了不同大小的内存空间。
如何在程序查看某个变量的字节大小和数据类型(使用较多)
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" "unsafe" )func main () { var num = 9.99 fmt.Printf("num的类型是%T, 占用的字节数是%d" , num, unsafe.Sizeof(num)) }
查看输出 1 num的类型是float64, 占用的字节数是8
3.5 整形
3.5.1 整形的类型
有符号整型(Signed Integers)
有符号整型可以表示正数、负数以及零。Go语言提供了以下几种有符号整型:
int8 :8位有符号整型,范围从-128到127。
int16 :16位有符号整型,范围从-32768到32767。
int32 :32位有符号整型,范围从-2147483648到2147483647。在Go中,int32
与rune
类型相同,常用于表示一个Unicode码点。
int64 :64位有符号整型,范围从-9223372036854775808到9223372036854775807。
此外,还有一个特殊的有符号整型:
int :其大小没有明确的位数,取决于执行程序的操作系统平台。通常在32位系统上是32位,在64位系统上是64位 。这是使用最广泛的整型,因为它在不同平台间提供了良好的兼容性。
无符号整型(Unsigned Integers)
无符号整型只能表示非负数(包括零)。Go语言提供了以下几种无符号整型:
uint8 :8位无符号整型,范围从0到255。
uint16 :16位无符号整型,范围从0到65535。
uint32 :32位无符号整型,范围从0到4294967295。
uint64 :64位无符号整型,范围从0到18446744073709551615。
和有符号整型一样,还有一个特殊的无符号整型:
uint :其大小同样取决于执行程序的操作系统平台。通常在32位系统上是32位,在64位系统上是64位 。它通常用于需要非负整数,且大小可能较大的情况。
特殊的整型类型
除了常规的有符号和无符号整型之外,Go语言还提供了几种特殊的整型类型:
uintptr :一个无符号整型,足以存储指针的位宽。这主要用于底层编程,如与C语言库交互或其他需要直接处理内存地址的场合。
byte :实际上是uint8
的别名,常用于处理原始数据,如文件流或网络数据包。
使用注意事项 :
选择正确的整型类型对于优化程序性能、内存使用和跨平台兼容性非常重要。通常,如果没有特定的大小需求,使用int
和uint
是最方便的,因为它们在不同平台上自动适应合适的大小。对于特定的应用,如处理大文件或需要精确控制数据结构大小的系统编程,选择固定大小的整型(如int64
或uint32
)可能更合适。
在编写程序时,还应注意整型溢出和隐式类型转换的问题,这些都可能导致意外的行为或错误。
3.5.2 整形的使用细节
3.6 浮点型
3.6.1 浮点型的类型
在Go语言中,浮点型数据用于表示有小数部分的数值。Go提供了两种基本的浮点类型,它们用于不同精度的数学计算:float32
和 float64
。
float32
float32
是一种单精度浮点类型,提供大约6-7位十进制精度。它在内存中占用32位(4字节),并遵循IEEE 754标准。由于其较小的大小和较快的处理速度,在处理大量浮点运算且对精度要求不是极高的场景下,使用float32
可以节省内存并提高性能。然而,它在进行复杂的数学运算或要求较高精度的应用中可能会引入较大的误差。
float64
float64
是一种双精度浮点类型,提供大约15-16位十进制精度。它在内存中占用64位(8字节),同样遵循IEEE 754标准。float64
是Go中使用最广泛的浮点类型,因为它提供了较高的精度,非常适合需要高精度计算的应用,如科学计算、金融分析等。除非有特别的性能或内存使用考虑,通常推荐使用float64
来避免精度问题。
使用浮点数的注意事项
精度问题 :由于浮点数的表示方式,某些值不能精确表示,例如大多数的小数。这可能导致精度损失,尤其是在进行一系列计算时累积的错误。
算术运算 :浮点数在进行加减乘除等基本算术运算时,可能会引入额外的误差,这一点在使用float32
时尤为明显。
比较运算 :直接比较两个浮点数是否相等通常是不可靠的,因为可能存在微小的差异。通常的做法是检查两个数的差的绝对值是否小于一个非常小的数(称为epsilon)。
零值 :浮点数的零值表示为0.0
,在Go中可以是正零+0.0
或负零-0.0
,这在某些数学函数中可能会有不同的行为。
示例代码 :
下面是一个Go程序的简单示例,展示了如何声明和使用浮点型变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "math" )func main () { var num1 float32 = 3.1415927 var num2 float64 = 3.141592653589793 fmt.Printf("num1 is %f and num2 is %f\n" , num1, num2) fmt.Printf("num1 with 7 decimal places: %.7f\n" , num1) fmt.Printf("num2 with 16 decimal places: %.16f\n" , num2) fmt.Printf("Sin of num2: %f\n" , math.Sin(num2)) }
总结
在Go中,选择float32
或float64
取决于应用的性能要求、内存效率以及对计算精度的需求。在大多数情况下,float64
由于其更高的精度而被推荐用于通用编程。对于特定的应用场景,如图形处理或大规模浮点运算,float32
可能是更合适的选择。
3.6.2 浮点型的使用细节
Golang 浮点类型有固定的范围和字段长度,不受具体 OS(操作系统)的影响。
Golang 的浮点型默认声明为 float64 类型 。
浮点型常量有两种表示形式
十进制数形式:如:5.12
、.512
(必须有小数点)
科学计数法形式:如:
5.1234e2
= 5.12*10的2次方
5.12E-2
= 5.12/10的2次方
通常情况下,应该使用 float64 ,因为它比 float32 更精确。[开发中,推荐使用 foat64 ]
3.7 字符型
3.7.1 基本介绍
在Go语言中,处理字符数据涉及两种主要的数据类型:byte
和 rune
。这两种类型分别用于处理ASCII字符和Unicode字符,非常适合在不同的应用场景中进行文本处理。
byte
byte
类型是 uint8
的别名,代表一个8位无符号整数。它通常用于处理ASCII字符,因为ASCII字符集中的每个字符都可以在8位(即1字节)中表示。在Go字符串中,字符串本质上是一个字节的切片(slice),所以处理ASCII或基于字节的数据时常常使用 byte
类型。
示例代码 :
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { var b byte = 'A' fmt.Printf("The byte is: %c\n" , b) }
在这个例子中,变量 b
被声明为 byte
类型,存储了ASCII字符 'A'
的字节表示。
一个用法 ,其他字母为var c byte = 'a' + byte(2)
, 这个字母为c。在Go中,当字符和数字进行运算时,实际上是在操作它们的数值。这里的计算结果是 97 + 2 = 99
。
rune
rune
类型是 int32
的别名,用于表示一个Unicode码点。这使得 rune
能够存储任何Unicode字符,包括那些复杂的、多字节的字符,如汉字或表情符号。在Go中,rune
能够处理来自世界上任何语言的字符,非常适合构建国际化和多语言的应用程序。
示例代码 :
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { var r rune = '中' fmt.Printf("The rune is: %c\n" , r) }
在这个例子中,r
被声明为 rune
类型,它可以存储和表示中文字符 ‘中’。
使用场景和注意事项
byte 适用于处理原始二进制数据、文本文件中的ASCII数据,或者任何需要按字节操作的场景。
rune 适用于处理Unicode文本,尤其是包含多字节字符的字符串。当你需要支持全球用户的文本(如多语言网站或应用程序)时,使用 rune
是必要的。
字符串和字符类型的操作
在Go中,字符串是不可变的字节序列,你可以通过索引访问字符串中的字节,但不能修改它们。如果需要修改字符串或进行复杂的字符处理,通常会涉及到将字符串转换为rune
切片,这样可以安全地处理多字节字符。
示例代码 :
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { s := "Hello, 世界" r := []rune (s) r[7 ] = '界' fmt.Println(string (r)) }
这个例子展示了如何将一个包含多语言字符的字符串转换为 rune
切片,修改它,然后再转换回字符串。
总结
在Go中,byte
和 rune
分别适用于不同的文本处理需求。选择正确的字符类型对于构建高效和可靠的文本处理功能至关重要。
3.7.2 字符型使用细节
字符常量是用单引号("
)括起来的单个字符。例如: var c1 byte ='a'
、var r rune = '中'
Go 中允许使用转义字符"\"来将其后的字符转变为特殊字符型常量。例如:var c3 char ='\n'
Go语言的字符使用UTF-8编码,如果想查询字符对应的 utf8 码值,可以访问http://www.mytju.com/classcode/tools/encode_utf8.asp
英文字母 1个字节;汉字 3 个字节
在 Go 中,字符的本质是一个整数,直接输出时,是该字符对应的 UTF-8编码的码值。
Go 语言的编码都统一成了 utf-8。非常的方便,很统一,再也没有编码乱码的困扰了
3.8 布尔型
在Go语言中,布尔型(Boolean)是一种基本数据类型,用于表示真或假的逻辑状态。布尔型在Go中的关键字是 bool
,它只有两个可能的值:true
和 false
。【占一个字节】
使用布尔型
布尔型常用于控制结构如条件语句(if
、else
)、循环(for
、while
在 Go 中使用 for
实现)以及控制程序逻辑流的任何地方。它也常常用于函数返回值,以表示操作的成功、错误检查或任何二分逻辑情况。
示例代码
下面是一些使用布尔型的基本示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { a := true b := false if a { fmt.Println("a is true" ) } if !b { fmt.Println("b is not true" ) } }
在这个例子中,变量 a
和 b
分别被赋予了布尔值 true
和 false
。接着使用 if
语句根据这些布尔值控制程序流。
布尔运算
Go语言支持标准的布尔逻辑运算,包括:
&&
(逻辑与):如果两个操作数都为 true
,结果才为 true
。
||
(逻辑或):如果两个操作数中至少有一个为 true
,结果就为 true
。
!
(逻辑非):如果操作数为 true
,结果为 false
;如果操作数为 false
,结果为 true
。
运算示例
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { fmt.Println(true && false ) fmt.Println(true || false ) fmt.Println(!true ) }
这段代码演示了三种基本的布尔逻辑运算。
3.9 string型
3.9.1 基本介绍
Go 语言中的 string
类型是一种用来处理文本的基本数据类型。它在 Go 中的实现和特性具有以下几个关键点:
不可变性
在 Go 中,字符串是不可变的。这意味着一旦一个字符串被创建,它所包含的内容就不能被改变。如果你需要修改字符串,Go 实际上是创建了一个新的字符串来存储修改后的内容。这种设计可以使字符串操作在多线程环境下自然地保持线程安全。
UTF-8 编码
Go 的字符串使用 UTF-8 编码。这使得字符串可以很容易地处理多种语言的文本,每个字符的编码长度可以从 1 到 4 个字节不等。UTF-8 编码支持全世界几乎所有的书写系统,因此在处理国际化应用程序时非常方便。
基本操作
Go 标准库提供了丰富的函数来操作字符串,这些函数包括在 strings
包中。这些操作包括但不限于搜索、替换、比较、截断、拼接和分割字符串。例如,strings.Contains
检查一个字符串是否包含另一个子串,strings.Split
将字符串按指定分隔符分割成多个子串。
字符和字节的访问
由于字符串是 UTF-8 编码的,直接索引字符串(例如 s[i]
)得到的是第 i
个字节而不一定是第 i
个字符 。如果需要按字符处理字符串,可以使用 range
循环,它会正确地按 UTF-8 字符边界迭代字符串:
1 2 3 for index, runeValue := range s { fmt.Printf("%#U starts at byte position %d\n" , runeValue, index) }
这里的 runeValue
是 rune
类型,表示单个 Unicode 字符。
字符串和字节切片的转换
字符串可以与字节切片([]byte
)相互转换。这允许程序直接修改或处理底层的字节数据。字符串转换为字节切片:
1 2 s := "hello" b := []byte (s)
字节切片转换回字符串:
性能考虑
字符串操作,特别是涉及拼接大量字符串的操作,可能会影响性能,因为每次拼接都可能生成新的字符串。在这种情况下,使用 strings.Builder
或 bytes.Buffer
可以更有效地构建字符串,因为它们在内部使用了更为高效的数据结构来减少内存分配和复制。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "strings" )func main () { s := "Hello, 世界" fmt.Println("Length:" , len (s)) fmt.Println("Number of runes:" , len ([]rune (s))) for i, r := range s { fmt.Printf("Char %d: %q\n" , i, r) } parts := strings.Split(s, ", " ) fmt.Println("Parts:" , parts) }
这些特性和操作使 Go 的 string
类型成为处理和操作文本数据的强大工具。
3.9.2 string的使用
语法 :
string的两种表示形式
(1) 双引号,会识别转义字符
1 s := "Hello, world\nHello, Go"
(2) 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
1 2 s := `Hello, world Hello, Go`
字符串连接
可以使用 +
操作符连接两个字符串,或者使用 +=
进行追加操作:
1 2 3 4 s1 := "Hello, " s2 := "world" s3 := s1 + s2 s1 += "Go"
字符串和字节切片
字符串可以转换为字节切片([]byte
),反之亦然。这在处理需要字节级操作的场景下非常有用,例如处理网络协议或文件系统操作:
1 2 3 str := "Hello, Go" bytes := []byte (str) str2 := string (bytes)
字符访问
通过索引访问字符串时,返回的是字节值,不是字符。要按字符访问和处理字符串,可以将字符串转换为 rune
切片或使用 range
循环遍历:
1 2 3 4 s := "你好,世界" for i, r := range s { fmt.Printf("Index %d Rune %q\n" , i, r) }
字符串的不可变性
字符串在 Go 中是不可变的。尝试改变字符串中的字符将导致编译错误:
要修改字符串,可以先将其转换为 []rune
或 []byte
,修改后再转换回来:
1 2 3 4 s := "hello" b := []byte (s) b[0 ] = 'H' s = string (b)
使用标准库
strings
包提供了一系列用于字符串处理的函数,如查找、替换、比较、截取等:
1 2 3 4 import "strings" fmt.Println(strings.Contains("test" , "es" )) fmt.Println(strings.ToUpper("test" ))
3.9.2 string使用的注意事项
1) Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员 。
rune 和 Unicode
rune
的定义 :在 Go 语言中,rune
是 int32
的别名。它用于表示一个 Unicode 码点,因此它是固定 32 位宽的。每个 rune
可以存储从 U+0000 到 U+10FFFF 的任何 Unicode 码点。
字符串和 UTF-8 :Go 中的字符串以 UTF-8 格式存储,这是一种变长编码方式,意味着每个 Unicode 字符可以占用 1 到 4 个字节。这样的存储方式支持广泛的字符集而不会浪费存储空间。
使用 range
遍历字符串 :当你使用 range
遍历一个字符串时,Go 自动处理 UTF-8 编码的复杂性。在每次迭代中,它解码 UTF-8 字节序列,返回当前字符的 Unicode 码点作为 rune
类型,以及这个字符在字符串中的起始字节位置。
rune
接收 Unicode 字符 :通过 range
循环,你可以把字符串中变长的每个字符(UTF-8 编码的)依次取出来,并使用 rune
类型来接收每个字符的完整码点。这使得字符处理变得简单,尤其是在需要字符级操作的场合,如文本分析或处理多语言文本。
示例代码
这里是一个演示如何使用 range
和 rune
来遍历字符串并处理每个字符的示例:
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { s := "Hello, 世界" for index, runeValue := range s { fmt.Printf("Character at byte index %d is %c (rune: %U)\n" , index, runeValue, runeValue) } }
这段代码有效地处理了包含英文和中文字符的字符串,正确地识别和输出了每个字符及其对应的 Unicode 码点。通过这种方式,Go 程序可以轻松地处理全球范围内的各种语言和字符集。
2) 当一行字符串太长时,需要使用到多行字符串,可以如下处理
对于需要在多行书写但实际输出为单行的字符串,你可以使用加号(+
)连接多个双引号界定的字符串字面量:
1 2 3 4 s := "这是一个例子 " + "的单行字符串," + "即使它在源代码中跨越了多行。" fmt.Println(s)
+
号要保留在上一行
3.10 基本数据类型的默认值
在 go 中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在 go 中,默认值又叫零值。
数据类型
默认值
整形
0
浮点型
0
string型
""
布尔型
false
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var a int var b float64 var c string var d bool fmt.Println(a) fmt.Println(b) fmt.Println(c) fmt.Println(d) }
查看输出
3.11 基本数据类型的相互转换
Golang 和 java/c 不同 ,Go 在不同类型的变量之间赋值时需要显式转换 。也就是说 Golang 中数据类型不能自动转换 。
基本语法
案例
1 2 var a int = 100 var b float64 = float64 (a)
注意事项
1)Go 中,数据类型的转换可以是从 表示范围小 到 表示范围大 ,也可以 范围大 到 范围小 。
2)被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化!
3)在转换中,比如将 int64 转成 int8【 -128 - 127 】 ,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。因此在转换时,需要考虑范围.
eg:
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var a int64 = 9999 var b int8 = int8 (a) fmt.Println(b) }
例子
例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { var a int32 = 12 var b int64 b = int64 (a) + 12 fmt.Println(b) }
为什么上面的 int64(变量a) 和 int(字面量12) 可以相加? 例子中:
1 2 3 4 var a int64 = 10 var b int64 b = a + 12
尽管 a
是 int64
类型,而字面量 12
默认是 int
类型,Go 编译器在处理算术运算时会根据上下文对整数字面量进行类型推断。这里的 12
被推断为 int64
类型,因此能够直接与 a
相加,而不会引发类型不匹配的编译错误。
这种情况主要是因为字面量(如数字常量)在 Go 中是有一种特殊的 “无类型” 状态,编译器可以根据需要将其视为任何具体的整数类型。因此,在这种情况下,12
自动适应了 int64
类型,使得表达式成立。
这种灵活性是为了代码的简洁性和易用性,但在涉及到明确的变量类型(特别是来自函数返回值或者复杂表达式的结果)时,Go 仍然要求显示的类型转换或确保匹配的类型操作。
例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var a int32 = 12 var b int8 var c int8 b = int8 (a) + 127 c = int8 (a) + 128 fmt.Println(b) fmt.Println(c) }
报错解释 对于 c
的赋值,int8(a) + 128
的结果是 140,但问题在于 128
本身不能被直接表示为一个 int8
类型的值。在 Go 中,整数常量如 128
会根据使用场景被视为具体类型。在这个表达式中,Go 编译器试图将 128
视作 int8
,这导致了编译错误,因为 128
超出了 int8
的最大范围。
3.12 基本数据类型和string的转换
3.12.1 基本数据类型 → string
方式一 :fmt.Sprintf("%参数", 表达式)
【推荐 】
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" func main () { var num1 int = 90 var num2 float64 = 23.456 var b bool = true var c byte = 'h' var str string str = fmt.Sprintf("%d" , num1) fmt.Printf("str type %T str=%q\n" , str, str) str = fmt.Sprintf("%f" , num2) fmt.Printf("str type %T str=%q\n" , str, str) str = fmt.Sprintf("%t" , b) fmt.Printf("str type %T str=%q\n" , str, str) str = fmt.Sprintf("%c" , c) fmt.Printf("str type %T str=%q\n" , str, str) }
查看输出 1 2 3 4 str type string str="90" str type string str="23.456000" str type string str="true" str type string str="h"
方式二 :使用strconv
包的函数
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "fmt" "strconv" )func main () { var a int = 99 var b float64 = 23.456 var c bool = true var str string str = strconv.FormatInt(int64 (a), 10 ) fmt.Printf("str type %T str=%q\n" , str, str) str = strconv.FormatFloat(b, 'f' , 10 , 64 ) fmt.Printf("str type %T str=%q\n" , str, str) str = strconv.FormatBool(c) fmt.Printf("str type %T str=%q\n" , str, str) var d int64 = 456 str = strconv.Itoa(int (d)) fmt.Printf("str type %T str=%q\n" , str, str) }
查看输出 1 2 3 4 str type string str="99" str type string str="23.4560000000" str type string str="true" str type string str="456"
3.12.2 string → 基本数据类型
方法 :使用strconv
包的函数
eg:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport ( "fmt" "strconv" )func main () { var str string = "true" var b bool b, _ = strconv.ParseBool(str) fmt.Printf("b type %T b=%v\n" , b, b) var str2 string = "123456" var n1 int64 var n2 int n1, _ = strconv.ParseInt(str2, 10 , 64 ) n2 = int (n1) fmt.Printf("n1 type %T n1=%v\n" , n1, n1) fmt.Printf("n2 type %T n2=%v\n" , n2, n2) var str3 string = "123.456" var f1 float64 f1, _ = strconv.ParseFloat(str3, 64 ) fmt.Printf("f1 type %T f1=%v\n" , f1, f1) }
查看输出 1 2 3 4 b type bool b=true n1 type int64 n1=123456 n2 type int n2=123456 f1 type float64 f1=123.456
注意事项
在将string类型转成基本数据类型时,要确保string类型能够转成有效的数据
eg: 如果把"hello"
转为整数,golang直接将其转为0【其他类型:float → 0
,bool → false
】
3.13 指针
和C/C++差不多
获取变量地址,用&
,和C/C++一样
指针类型,eg:var ptr *int = &num
解引用(获取指针类型所指向的值),使用*
,比如:var ptr *int
,使用*ptr
获取ptr
指向的值
eg:
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { var n1 int64 = 12 var n2 *int64 = &n1 fmt.Println(*n2) }
3.14 值类型和引用类型
在Go语言中,有分值类型和引用类型之分。具体来说:
值类型
值类型的变量直接包含它们的数据。赋值操作和函数传参时,会复制整个值。这意味着对一个变量的修改不会影响到其他变量。
常见的值类型有:
基本类型:如int、float、bool、string
数组
结构体(struct)
引用类型
引用类型的变量保存的是数据的地址(或指针)。赋值操作和函数传参时,复制的是地址,这意味着对一个变量的修改会影响到其他变量。
常见的引用类型有:
指针(pointer)
切片(slice)
映射(map)
通道(channel)
函数(function)
引用类型由 Go的垃圾回收器(Garbage Collector,简称GC)自动管理的。这意味着当这些类型的对象不再被任何部分的程序引用时,它们占用的内存资源将被自动回收。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func main () { a := 10 b := a b = 20 fmt.Println(a, b) arr1 := []int {1 , 2 , 3 } arr2 := arr1 arr2[0 ] = 100 fmt.Println(arr1, arr2) }
在这个例子中,int
是值类型,而slice
是引用类型。对于值类型的赋值,修改后的变量不会影响原变量;而对于引用类型的赋值,修改后的变量会影响原变量。
希望这可以解答你的问题。如果有更多具体的需求或问题,随时告诉我!
这里统一说明一下默认值 :
布尔类型是 false ,数值是 0 ,字符串是 “”。
数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
3.15 标识符命名规则
命名规则和C/C++一样 ……
其他 :
下划线_
本身在Go中是一个特殊的标识符,称为空标识符 。可以代表其他任何其他的标识符。但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用 。
3.16 系统保留关键字
在Go中,为了简化代码编译过程中对代码的解析,其定义的保留关键字 只有25个。如下:
序号
关键字
描述
1
break
跳出当前循环
2
default
switch 语句中的默认分支
3
func
定义函数和方法
4
interface
定义接口
5
select
用于通道操作的多路选择
6
case
switch 或 select 的一个分支
7
defer
延迟调用一个函数
8
go
开启一个新的 goroutine
9
map
定义 map 类型
10
struct
定义结构体
11
chan
定义通道类型
12
else
if 语句之后的分支
13
goto
跳转到指定的标签
14
package
定义包名
15
switch
定义选择语句
16
const
定义常量
17
fallthrough
在 switch 语句中强制执行下一个 case
18
if
定义条件语句
19
range
迭代 array、slice、map、string
20
type
定义类型或类型别名
21
continue
跳过当前循环的剩余部分,进入下一次循环迭代
22
for
定义循环语句
23
import
导入包
24
return
从函数返回
25
var
定义变量
3.17 系统的预定义标识符
Go 语言中的预定义标诈符包括基本的数据类型和内建函数。这些标识符为程序提供了基础的构建块和必需的工具。下面是一个带序号的表格,列出了这些预定义的标识符:
序号
预定义标识符
类型/用途
1
bool
基础数据类型
2
byte
基础数据类型(uint8
的别名)
3
complex64
基础数据类型
4
complex128
基础数据类型
5
error
接口类型
6
float32
基础数据类型
7
float64
基础数据类型
8
int
基础数据类型
9
int8
基础数据类型
10
int16
基础数据类型
11
int32
基础数据类型
12
int64
基础数据类型
13
rune
基础数据类型(int32
的别名)
14
string
基础数据类型
15
uint
基础数据类型
16
uint8
基础数据类型
17
uint16
基础数据类型
18
uint32
基础数据类型
19
uint64
基础数据类型
20
uintptr
基础数据类型
21
true
预定义常量
22
false
预定义常量
23
iota
特殊常量
24
nil
预定义常量
25
append
内建函数
26
cap
内建函数
27
close
内建函数
28
complex
内建函数
29
copy
内建函数
30
delete
内建函数
31
imag
内建函数
32
len
内建函数
33
make
内建函数
34
new
内建函数
35
panic
内建函数
36
print
内建函数
37
println
内建函数
38
real
内建函数
39
recover
内建函数
这些标识符被直接嵌入到语言中,可以在任何 Go 程序中使用而无需导入额外的包。
3.18 自定义数据类型
为了简化数据类型定义,Go 支持自定义数据类型
基本语法 :
值得一提的是,在go中,自定义类型名和原来的数据类型,被认为是两个不同的类型。
案例 1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { type myInt int var num1 myInt var num2 int num1 = 40 num2 = int (num1) fmt.Println("num1=" , num1, "num2=" , num2) }
案例2 :这个可以看完函数部分的内容再看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func getSum (n1 int , n2 int ) int { return n1 + n2 }type myFunType func (int , int ) int func myFun (funvar myFunType, n1 int , n2 int ) int { return funvar(n1, n2) }func main () { res := myFun(getSum, 10 , 20 ) fmt.Println(res) }
在 Go 语言中,使用 type
关键字定义的新类型,它不会从其底层类型继承任何方法或行为。这种类型声明主要是为了提供类型安全和增加代码的可读性。以下是几个关键点:
底层数据结构 :新类型会采用相同的内部数据结构作为底层类型。这意味着如果底层类型是 int
,那么新类型在内存中的存储方式和 int
完全相同。
类型独立 :尽管新类型和其底层类型在内存表示上相同,但在类型系统中它们是完全独立的。这样做的目的是为了增强类型安全,避免类型之间的隐式转换,减少潜在的错误。
显式转换 :可以在新类型和其底层类型之间进行显式类型转换。这是因为它们共享相同的数据表示,所以这种转换是安全的。
1 2 3 type MyInt int test1 := MyInt(10 ) test1 := MyInt(10.1 )
不继承方法 :如果底层类型有方法,这些方法不会自动应用到新类型上。如果你希望新类型具有相似的方法,需要显式地为新类型定义这些方法。
3.19 常量
在Go语言中,定义常量的语法非常类似于定义变量,不过是将 var
关键字换成 const
。常量一旦被赋值后,其值就不能被修改。
定义单个常量
你可以这样定义一个常量:
定义多个常量
如果你想一次性定义多个常量,可以使用括号将它们组合在一起,类似于定义多个变量:
1 2 3 4 5 const ( StatusOK = 200 StatusNotFound = 404 StatusInternalServerError = 500 )
常量和变量的主要区别
不可变性 :常量在定义后其值不能改变,而变量的值可以在程序运行期间改变。
编译时求值 :常量在编译时就需要被赋值一个常数表达式的结果,这个值在编译期就必须能确定。而变量可以在运行时被赋值和修改。
类型推断 :常量在Go中可以是显式类型的,也可以是隐式类型的(未指定类型)。如果未指定类型,常量会根据上下文自动适配所需类型。
常量的地址不可获取 :在 Go 语言中,你不能获取常量的地址,因为常量没有地址可言。
4 运算符
4.1 对比C/C++的运算符
Go 语言的运算符在很多方面与 C 和 C++ 相似,因为 Go 语言在设计时受到了 C 语言的强烈影响。然而,也有一些关键的区别和缺失的运算符,特别是 Go 在简化语言特性方面所做的一些决定。
相似之处
在 Go 和 C/C++ 中,许多基本的算术、比较和逻辑运算符是相同的,例如:
算术运算符:+
, -
, *
, /
, %
比较运算符:==
, !=
, <
, >
, <=
, >=
逻辑运算符:&&
, ||
, !
位运算符:&
, |
, ^
, <<
, >>
, &^
(&^
是 Go 特有的按位清除)
赋值运算符:=
, +=
, -=
, *=
, /=
, &=
, <<=
, >>=
, &=
, ^=
, |=
其他运算符:&
, *
Go 特有的运算符
&^
(按位清除):这是 Go 特有的运算符,用于将第二个操作数中设为 1 的位在第一个操作数中设为 0。
C/C++ 存在而 Go 中缺失的运算符
指针运算符:C/C++ 允许通过 ++
和 --
运算符直接操作指针,而 Go 禁止对指针执行算术运算。
三目运算符:C/C++ 中的 ?:
是一个常用的条件运算符,Go 没有这个运算符,需要使用完整的 if-else
语句替代。
自增和自减的前缀/后缀形式:在 Go 中,++
和 --
只能作为语句使用,而不是表达式。这意味着它们不能在赋值操作中使用,例如 a = b++
在 Go 中是非法的。
【 并且只有后缀形式,++i
不允许这么使用,只有i++
】
地址运算符:虽然 Go 和 C/C++ 都使用 &
来取地址,但 Go 在处理指针时更为严格和安全。
复合赋值运算符:Go 也支持复合赋值运算符,如 +=
、-=
、*=
等,这点与 C/C++ 类似。
4.2 运算符优先级
优先级
分类
运算符
结合性
14
后缀运算符
( )、[ ]、->
从左到右
13
单目运算符
!、*(指针)、& 、++、–、+(正号)、-(负号)
从右到左
12
乘法/除法/取余
*(乘号)、/、%
从左到右
11
加法/减法
+、-
从左到右
10
位移运算符
<<、>>
从左到右
9
关系运算符
<、<=、>、>=
从左到右
8
相等/不等
==、!=
从左到右
7
按位与
&
从左到右
6
按位异或
^
从左到右
5
按位或
|
从左到右
4
逻辑与
&&
从左到右
3
逻辑或
||
从左到右
2
赋值运算符
=、+=、-=、*=、/=、%=、>=、<<=、&=、^=、|=
从右到左
1
逗号运算符
,
从左到右
5 标准库学习
5.1 键盘输入
函数 fmt.Scanln()
或者 fmt.Scanf()
1 func Scanln (a …interface {}) (n int , err error )
在换行时才停止扫描 。最后一个条目后必须有换行或者到达结束位置。
1 func Scanf (format string , a …interface {}) (n int , err error )
Scanf从标准输入扫描文本。根据format参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。
例子
要求输入姓名、年龄、薪水,并打印
使用fmt.Scanln()
:
输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func main () { var ( name string age byte sal float32 ) fmt.Scanln(&name) fmt.Scanln(&age) fmt.Scanln(&sal) fmt.Printf("名字为%v, 年龄为%v, 薪水为%v\n" , name, age, sal) }
使用fmt.Scanf()
:
输入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var ( name string age byte sal float32 ) fmt.Scanf("%s %d %f" , &name, &age, &sal) fmt.Printf("名字为%v, 年龄为%v, 薪水为%v\n" , name, age, sal) }
Go `fmt.Scanf()` 和 C/C++ `scanf()` 的主要区别 严格的格式匹配 :
Go 的 fmt.Scanf()
对输入格式的要求更加严格 。它要求输入的格式必须严格匹配格式化字符串,否则会返回错误,并且不会填充所有变量。
C/C++ 的 scanf()
在遇到格式不匹配时会停止读取,但它会尝试尽可能多地填充变量。
返回值 :
fmt.Scanf()
返回两个值:成功扫描的项目数和可能的错误信息。你应该检查返回的错误以确保所有值都正确读取。
scanf()
返回成功读取的项目数,但不提供详细的错误信息。
类型支持 :
fmt.Scanf()
支持 Go 的特定类型,并且在某些情况下可能需要类型转换或特殊处理。
scanf()
直接支持 C/C++ 的基本类型。
指针传递 :
两者都需要传递变量的地址,但 Go 使用 &
符号,C/C++ 也是使用 &
符号。
5.2 字符串相关
统计字符串长度len
内建函数 len返回 v 的长度,这取决于具体类型:
数组:v中元素的数量
数组指针:*v中元素的数量(v为nil时panic)
切片、映射:v中元素的数量;若v为nil,len(v)即为零
字符串:v中字节的数量
通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零
由于golang的编码统一为utf-8,所以ascii字符占一个字节,汉字占三个字节 :
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { str := "hello哈喽" fmt.Println(len (str)) }
字符串遍历,处理中文问题[]rune(str)
先看一个错误的做法
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { str := "hello哈喽" for i := 0 ; i < len (str); i++ { fmt.Printf("字符=%c\n" , str[i]) } }
查看输出 1 2 3 4 5 6 7 8 9 10 11 字符=h 字符=e 字符=l 字符=l 字符=o 字符=å 字符= 字符= 字符=å 字符= 字符=½
正确的做法是:
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { str := "hello哈喽" res := []rune (str) for i := 0 ; i < len (res); i++ { fmt.Printf("字符=%c\n" , res[i]) } }
查看输出 1 2 3 4 5 6 7 字符=h 字符=e 字符=l 字符=l 字符=o 字符=哈 字符=喽
字符串转整数
1 func Atoi (s string ) (i int , err error )
整数转字符串
字符串转[]byte
1 2 str := "hello go" var bytes = []byte (str)
[byte]转字符串
1 str = string ([]byte {97 , 98 , 99 })
10 进制转 2, 8, 16 进制
1 func FormatInt (i int64 , base int ) string
返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母’a’到’z’表示大于10的数字。
例:
1 2 3 4 str := strconv.FormatInt(123 , 2 ) fmt.Printf("123对应的2进制是%v\n" , str) str = strconv.FormatInt(123 , 16 ) fmt.Printf("123对应的16进制是%v\n" , str)
查找子串是否在指定的字符串中
1 func Contains (s, substr string ) bool
判断字符串s是否包含子串substr。
例:
1 2 3 4 fmt.Println(strings.Contains("seafood" , "foo" )) \\ true fmt.Println(strings.Contains("seafood" , "bar" )) \\ false fmt.Println(strings.Contains("seafood" , "" )) \\ true fmt.Println(strings.Contains("" , "" )) \\ true
统计一个字符串有几个指定的子串
1 func Count (s, sep string ) int
返回字符串s中有几个不重复的sep子串。
例:
1 2 fmt.Println(strings.Count("cheese" , "e" )) fmt.Println(strings.Count("five" , "" ))
Output:
不区分大小写的字符串比较【==
是区分字母大小写的】
1 func EqualFold (s, t string ) bool
判断两个utf-8编码字符串(将unicode大写、小写、标题三种格式字符视为相同)是否相同。
例:
1 fmt.Println(strings.EqualFold("Go" , "go" ))
1 fmt.Println("Go" == 'go' )
返回子串在字符串第一次出现的 index 值
1 func Index (s, sep string ) int
子串sep在字符串s中第一次出现的位置,不存在则返回-1。
返回子串在字符串最后一次出现的 index 值
1 func LastIndex (s, sep string ) int
子串sep在字符串s中最后一次出现的位置,不存在则返回-1。
将指定的子串替换成另外一个子串
1 func Replace (s, old, new string , n int ) string
返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
例:
1 2 fmt.Println(strings.Replace("oink oink oink" , "k" , "ky" , 2 )) fmt.Println(strings.Replace("oink oink oink" , "oink" , "moo" , -1 ))
Output:
1 2 oinky oinky oink moo moo moo
按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
1 func Split (s, sep string ) []string
用去掉s中出现的sep的方式进行分割,会分割到结尾,并返回生成的所有片段组成的切片(每一个sep都会进行一次切割,即使两个sep相邻,也会进行两次切割)。如果sep为空字符,Split会将s切分成每一个unicode码值一个字符串。
例:
1 2 3 4 fmt.Printf("%q\n" , strings.Split("a,b,c" , "," )) fmt.Printf("%q\n" , strings.Split("a man a plan a canal panama" , "a " )) fmt.Printf("%q\n" , strings.Split(" xyz " , "" )) fmt.Printf("%q\n" , strings.Split("" , "Bernardo O'Higgins" ))
Output:
1 2 3 4 ["a" "b" "c"] ["" "man " "plan " "canal panama"] [" " "x" "y" "z" " "] [""]
将字符串的字母进行大小写的转换
1 func ToLower (s string ) string
返回将所有字母都转为对应的小写版本的拷贝。
1 func ToUpper (s string ) string
返回将所有字母都转为对应的大写版本的拷贝。
将字符串左右两边的空白去掉
1 func TrimSpace (s string ) string
返回将s前后端所有空白(unicode.IsSpace指定)都去掉的字符串。
例:
1 fmt.Println(strings.TrimSpace(" \t\n a lone gopher \n\t\r\n" ))
Output:
将字符串左右两边指定的字符去掉
1 func Trim (s string , cutset string ) string
返回将s前后端所有cutset包含的utf-8码值都去掉的字符串。
例:
1 fmt.Printf("[%q]" , strings.Trim(" !!! Achtung! Achtung! !!! " , "! " ))
Output:
将字符串左边指定的字符去掉
1 func TrimLeft (s string , cutset string ) string
返回将s前端所有cutset包含的utf-8码值都去掉的字符串。
将字符串右边指定的字符去掉
1 func TrimRight (s string , cutset string ) string
返回将s后端所有cutset包含的utf-8码值都去掉的字符串。
判断字符串是否以指定的字符串开头
1 func HasPrefix (s, prefix string ) bool
判断s是否有前缀字符串prefix。
判断字符串是否以指定的字符串结束
1 func HasSuffix (s, suffix string ) bool
判断s是否有后缀字符串suffix。
5.3 时间日期相关
时间和日期相关函数,需要导入 time 包
time.Time
类型,用于表示时间
1 2 3 4 5 6 7 8 9 10 11 package mainimport ( "fmt" "time" )func main () { now := time.Now() fmt.Printf("now = %v\nnow type = %T" , now, now) }
查看输出 1 2 now = 2024-07-13 02:08:49.9926704 +0800 CST m=+0.003055201 now type = time.Time
如何获取到其它的日期信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "time" )func main () { now := time.Now() fmt.Printf("now = %v\nnow type = %T\n" , now, now) fmt.Printf("年=%v\n" , now.Year()) fmt.Printf("月=%v\n" , now.Month()) fmt.Printf("月=%v\n" , int (now.Month())) fmt.Printf("日=%v\n" , now.Day()) fmt.Printf("时=%v\n" , now.Hour()) fmt.Printf("分=%v\n" , now.Minute()) fmt.Printf("秒=%v\n" , now.Second()) }
查看输出 1 2 3 4 5 6 7 8 9 now = 2024-07-13 02:18:47.0421625 +0800 CST m=+0.003064401 now type = time.Time 年=2024 月=July 月=7 日=13 时=2 分=18 秒=47
格式化日期时间
方式 1 : 就是使用 fmt.Printf
或者 fmt.SPrintf
方式 2 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "time" )func main () { now := time.Now() fmt.Printf("now = %v\nnow type = %T\n" , now, now) fmt.Println(now.Format("2006/01/02 15:04:05" )) fmt.Println(now.Format("2006-01-02 15:04:05" )) fmt.Println(now.Format("2006-01-02" )) fmt.Println(now.Format("15:04:05" )) }
查看输出 1 2 3 4 5 6 now = 2024-07-13 02:29:57.98494 +0800 CST m=+0.003053601 now type = time.Time 2024/07/13 02:29:57 2024-07-13 02:29:57 2024-07-13 02:29:57
这个时间一定要对 ,格式可以自己写
时间常量
Duration类型代表两个时间点之间经过的时间,以纳秒为单位。可表示的最长时间段大约290年。
1 2 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 )
常量的作用 : 在程序中可用于获取指定时间单位的时间,比如想得到 100 毫秒,就是100 * time.Millisecond
结合 Sleep 来使用一下时间常量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "time" )func main () { for i := 0 ; i <= 100 ; i++ { fmt.Println(i) time.Sleep(time.Second) } }
时间戳
1 func (t Time) Unix() int64
Unix将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位秒)。
1 func (t Time) UnixNano() int64
UnixNano将t表示为Unix时间,即从时间点January 1, 1970 UTC到时间点t所经过的时间(单位纳秒)。如果纳秒为单位的unix时间超出了int64能表示的范围,结果是未定义的。注意这就意味着Time零值调用UnixNano方法的话,结果是未定义的。
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" "time" )func main () { now := time.Now() fmt.Printf("now = %v\nnow type = %T\n" , now, now) fmt.Printf("Unix时间戳=%v Unixnano时间戳=%v\n" , now.Unix(), now.UnixNano()) }
查看输出 1 2 3 now = 2024-07-13 02:51:33.3834142 +0800 CST m=+0.002548801 now type = time.Time Unix时间戳=1720810293 Unixnano时间戳=1720810293383414200
统计程序执行时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "strconv" "time" )func test () { str := "" for i := 0 ; i < 100000 ; i++ { str += "hello" + strconv.Itoa(i) } }func main () { start := time.Now().Unix() test() end := time.Now().Unix() fmt.Println("函数执行了" , end - start, "秒" ) }
5.4 内置函数
Golang 设计者为了编程方便,提供了一些函数,这些函数可以直接使用,我们称为 Go 的内置函数。文档:https://studygolang.com/pkgdoc -> builtin
len
内建函数len返回 v 的长度,这取决于具体类型:
数组:v中元素的数量
数组指针:*v中元素的数量(v为nil时panic)
切片、映射:v中元素的数量;若v为nil,len(v)即为零
字符串:v中字节的数量
通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零
new
、
内建函数new分配内存。其第一个实参为类型,而非值。其返回值为指向该类型的新分配的零值 的指针。
例:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { n := new (int ) fmt.Printf("n的类型为%T\nn的值为%v\nn的地址为%v\nn指向的值为%v" , n, n, &n, *n) }
查看输出 1 2 3 4 n的类型为*int n的值为0xc00000a0a8 n的地址为0xc00006a028 n指向的值为0
make
1 func make (Type, size IntegerType) Type
内建函数make分配并初始化一个类型为切片、映射、或通道的对象。其第一个实参为类型,而非值。make的返回类型与其参数相同,而非指向它的指针。其具体结果取决于具体的类型:
切片:size指定了其长度。该切片的容量等于其长度。切片支持第二个整数实参可用来指定不同的容量;它必须不小于其长度,因此 make([]int, 0, 10) 会分配一个长度为0,容量为10的切片。
映射:初始分配的创建取决于size,但产生的映射长度为0。size可以省略,这种情况下就会分配一个小的起始大小。
通道:通道的缓存根据指定的缓存容量初始化。若 size为零或被省略,该信道即为无缓存的。
6 程序流程控制
6.1 分支控制
6.1.1 if……
单分支
语法:
ps :一定要有{}
双分支
语法:
1 2 3 4 5 if 条件表达式 { …… } else { …… }
ps:以下写法都是错的
1 2 3 4 5 6 if 条件表达式 { …… }else { …… }
多分支
语法:
1 2 3 4 5 6 7 8 9 if 条件表达式1 { …… } else if 条件表达式2 { …… } ……else { }
特殊之处
Go的 if
语句允许你在条件判断之前执行一个语句,该语句和条件判断由分号(;
)分隔。
这种在 if
语句中进行初始化的做法非常有用,因为它能帮助:
减少作用域污染 :在 if
语句中声明的变量仅在 if
和 else
块中可见,这有助于避免在更广泛的作用域中引入不必要的变量。
简化错误处理代码 :这种方法在处理可能失败的函数调用时尤其有用,使得代码更紧凑、易读。
重要细节
作用域 :初始化语句中声明的变量的作用域仅限于if
语句和相关的else if
和else
块。
初始化的限制 :if
语句中的初始化只能在第一个if
部分进行,不能在else if
或else
部分进行新的初始化。
逻辑清晰 :这种方式鼓励程序员在一个地方进行变量初始化,使得程序逻辑更加清晰和集中。
6.1.2 switch
语法 :
1 2 3 4 5 6 7 8 9 10 11 switch 表达式 { case 表达式1 , 表达式2 , ……: 语句块1 case 表达式3 , 表达式4 , ……: 语句块2 …… …… default : 语句块 }
例子:请编写一个程序,该程序可以接收一个字符,比如a,b,c……,输出字母对应的含义,a表示周一……
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport "fmt" func main () { var key byte fmt.Println("请输入一个字母:" ) fmt.Scanf("%c" , &key) switch key { case 'a' , 'h' : fmt.Println("这是周一" ) case 'b' : fmt.Println("这是周二" ) case 'c' : fmt.Println("这是周三" ) case 'd' : fmt.Println("这是周四" ) case 'e' : fmt.Println("这是周五" ) case 'f' : fmt.Println("这是周六" ) case 'g' : fmt.Println("这是周日" ) default : fmt.Println("没有意义" ) } }
switch穿透
如果在case语句块后面增加fallthrough
,这会继续执行下一个case。【穿一层】
1 2 3 4 5 6 7 8 9 10 11 12 switch key { case 'a' , 'h' : fmt.Println("这是周一" ) fallthrough case 'b' : fmt.Println("这是周二" ) case 'c' : fmt.Println("这是周三" ) case 'd' : fmt.Println("这是周四" ) case 'e' : ……
Type Switch
switch语句还可以被用于type-switch来判断某个interface变量中实际指向的变量类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" func main () { var x interface {} var y = 10.0 x = y switch i := x.(type ) { case nil : fmt.Printf("x的类型是: %T" , i) case int : fmt.Printf("x的类型是: int" ) case float64 : fmt.Printf("x的类型是: float64" ) case func (int ) float64 : fmt.Printf("x的类型是: func(int)" ) case bool , string : fmt.Printf("x的类型是: bool 或者 string" ) default : fmt.Printf("未知类型" ) } }
6.2 循环控制
for循环
语法
1 2 3 for 循环变量初始化; 循环条件; 循环变量迭代 { …… }
其他写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 for 循环条件 { …… }for { …… }for ;; { …… }
ps:Go语言中没有while
和do…while
的概念
练习 :打印九九乘法表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var num1 int8 var num2 int8 for num2 = 1 ; num2 <= 9 ; num2++ { for num1 = 1 ; num1 <= num2; num1++ { fmt.Print(num1, "*" , num2, "=" , num1*num2, "\t" ) } fmt.Print("\n" ) } }
查看输出 1 2 3 4 5 6 7 8 9 1*1=1 1*2=2 2*2=4 1*3=3 2*3=6 3*3=9 1*4=4 2*4=8 3*4=12 4*4=16 1*5=5 2*5=10 3*5=15 4*5=20 5*5=25 1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36 1*7=7 2*7=14 3*7=21 4*7=28 5*7=35 6*7=42 7*7=49 1*8=8 2*8=16 3*8=24 4*8=32 5*8=40 6*8=48 7*8=56 8*8=64 1*9=9 2*9=18 3*9=27 4*9=36 5*9=45 6*9=54 7*9=63 8*9=72 9*9=81
6.3 break, continue, goto, continue
break语句用于中断某个语句块的执行,用于中断for。
和C/C++差不多,但是go的switch会自动终止,不需要break了。
和C/C++差不多
不主张使用。
和C/C++差不多
7 函数、包和错误处理
7.1 函数
7.1.1 基本语法
Go函数的声明以关键字 func
开始,后跟函数名、参数列表和返回类型:
1 2 3 4 func 函数名(形参列表) (返回值列表) { …… return 返回值列表 }
与C/C++不同的是,go必须把声明和定义一次写好 。
7.1.2 参数
固定参数: 每个参数后都需要跟随类型。
可变参数: 使用省略号 ...
表示函数可以接收任意数量的参数,但可变参数必须是参数列表中的最后一个。【只能有一个,这个可变参数是一个slice切片,可以通过args[index]
访问到各个值】
1 2 3 4 5 6 7 func sum (args ...int ) int { total := 0 for _, num := range nums { total += num } return total }
当 ...
出现在函数调用时,它用于将一个切片展开为一系列独立的参数传递给可变参数函数。这种情况下,切片的元素将作为独立的参数传递给函数。
值的一提,go没有函数重载 。
7.1.3 返回值
单一返回值: 直接在函数定义中指定类型。
多返回值: Go函数可以返回多个值,常用于返回结果和错误状态。
1 2 3 4 5 6 func divide (a, b int ) (int , error ) { if b == 0 { return 0 , errors.New("division by zero" ) } return a / b, nil }
命名返回值: 可以给返回值命名,这样可以在函数内部直接使用这些名称,并通过 return
关键字返回。
1 2 3 4 5 6 7 8 9 10 11 12 func stats (numbers []int ) (min, max int ) { min, max = numbers[0 ], numbers[0 ] for _, num := range numbers { if num < min { min = num } if num > max { max = num } } return }
【和C/C++一样,有返回值的话必须要有return
,无返回值可以省略return
】
7.1.4 函数作为参数和返回值
函数在Go中可以作为参数传递,也可以作为返回值,这使得高阶函数(即操作其他函数的函数)成为可能:
1 2 3 4 5 6 7 8 9 10 func applyFunc (f func (int ) int , value int ) int { return f(value) }func double (x int ) int { return x * 2 } result := applyFunc(double, 5 ) fmt.Println(result)
Go的这些特性使其在错误处理、并发编程和功能抽象方面非常强大,也支持了函数式编程的某些模式。
7.1.5 init函数
每一个源文件都可以包含一个 init 函数 ,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用。
注意事项:
1) 如果一个文件同时包含全局变量定义 ,init 函数 和 main 函数 ,则执行的流程 全局变量定义->init函数->main 函数
2) init 函数最主要的作用,就是完成一些初始化的工作
3) 执行流程
文件结构:
1 2 3 4 5 6 project1 ├─main │ main.go │ └─utils utils.go
utils.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package utilsimport "fmt" var Name string = setName() func setName () string { fmt.Println("utils.go 变量定义执行" ) return "ggw" }func init () { fmt.Println("utils.go init()执行" ) }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "go_code/project1/utils" )var Age int = setAge() func setAge () int { fmt.Println("main.go 变量定义执行" ) return 22 }func init () { fmt.Println("main.go init()执行" ) }func main () { fmt.Println("main.go main()执行" ) fmt.Println("------\nutils.go 的Name为" , utils.Name, "main.go的Age为" , Age) }
查看输出 1 2 3 4 5 6 7 utils.go 变量定义执行 utils.go init()执行 main.go 变量定义执行 main.go init()执行 main.go main()执行 ------ utils.go 的Name为 ggw main.go的Age为 22
7.1.6 匿名函数
Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用。
用法一 :
在定义匿名函数时就直接调用,这种方式匿名函数只能调用一次。
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { res := func (n1, n2 int ) int { return n1 + n2 } (10 , 20 ) fmt.Println(res) }
用法二 :
将匿名函数赋给一个变量 (函数变量),再通过该变量来调用匿名函数
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" func main () { a := func (n1, n2 int ) int { return n1 + n2 } res := a(10 , 20 ) fmt.Println(res) }
用法三 :
如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport "fmt" var a = func (n1, n2 int ) int { return n1 + n2 }func main () { res := a(10 , 20 ) fmt.Println(res) }
7.1.7 闭包
基本介绍:闭包就是一个函数 和与其相关的引用环境 组合的一个整体 (实体)
下面介绍可能难理解,反正就是外面初始化环境,返回的函数在这个环境下执行 。
通过一个实例来说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func AddUpper () func (int ) int { var n int = 10 return func (x int ) int { n += x return n } }func main () { f := AddUpper() fmt.Println(f(1 )) fmt.Println(f(2 )) fmt.Println(f(3 )) }
对上面代码进行说明:
1、AddUpper 是一个函数,返回的数据类型是 fun (int) int
2、闭包的说明:返回的是一个匿名函数, 但是这个匿名函数引用到函数外的n ,因此这个匿名函数就和n 形成一个整体,构成闭包。
3、可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。
4、当我们反复的调用 f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
5、要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。
闭包的最佳实践
请编写一个程序,具体要求如下
编写一个函数 makeSuffix(suffix string)
可以接收一个文件后缀名(比如.jpg),并返回一个闭包
调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。
要求使用闭包的方式完成
strings.HasSuffix
, 该函数可以判断某个字符串是否有指定的后缀。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package mainimport ( "fmt" "strings" )func makeSuffix (suffix string ) func (string ) string { return func (filename string ) string { if !strings.HasSuffix(filename, suffix) { return filename + suffix } return filename } }func main () { addJpgSuffix := makeSuffix(".jpg" ) fmt.Println(addJpgSuffix("mypicture" )) fmt.Println(addJpgSuffix("holiday.jpg" )) fmt.Println(addJpgSuffix("family_photo" )) fmt.Println(addJpgSuffix("portrait.jpeg" )) addPngSuffix := makeSuffix(".png" ) fmt.Println(addPngSuffix("mypicture" )) }
7.1.8 注意事项和使用细节
Go 函数不支持函数重载。
在 Go 中,函数也是一种数据类型 ,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func getSum (n1 int , n2 int ) int { return n1 + n2 }func main () { a := getSum fmt.Printf("a的类型是%T,getSum的类型是%T\n" , a, getSum) res := a(10 , 20 ) fmt.Printf("res=%v" , res) }
查看输出 1 2 a的类型是func(int, int) int,getSum的类型是func(int, int) int res=30
形参没有被使用并不会报错。
7.1.9 defer
为什么需要defer?
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等) ,为了在函数执行完毕后,及时的释放资源 ,Go 的设计者提供 defer (延时机制)。
通过案例快速入门:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func sum (n1, n2 int ) int { defer fmt.Println("ok1 n1=" , n1) defer fmt.Println("ok2 n2=" , n2) res := n1 + n2 fmt.Println("ok3 res=" , res) return res }func main () { res := sum(10 , 20 ) fmt.Println("ok4 res=" , res) }
查看输出 1 2 3 4 ok3 res= 30 ok2 n2= 20 ok1 n1= 10 ok4 res= 30
细节
在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈 。请看一段代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" func sum (n1, n2 int ) int { defer fmt.Println("ok1 n1=" , n1) defer fmt.Println("ok2 n2=" , n2) n1++ n2++ res := n1 + n2 fmt.Println("ok3 res=" , res) return res }func main () { res := sum(10 , 20 ) fmt.Println("ok4 res=" , res) }
查看输出 1 2 3 4 ok3 res= 32 ok2 n2= 20 ok1 n1= 10 ok4 res= 32
defer的最佳实践
defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。看下模拟代码 。
1 2 3 4 5 6 func test () { file = openfile(文件名) defer file.close () }
1 2 3 4 5 6 func test () { connect = openDatabase(文件名) defer connect.close () }
说明:
1) 在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close() defer connect.Close()
2) 在 defer 后,可以继续使用创建资源.
3) 当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.
4) 这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。
7.2 包
背景1:需要在不同的文件中,去调用其他文件定义的函数,这种情况要怎么办。
背景2:程序员a和程序员b定义了同样的函数,这时候该怎么办。
答案是使用包
7.2.1 包的基本概念
包的本质实际上就是创建不同的文件夹,来存放程序文件 。
go 的每一个文件都是属于一个包的 ,也就是说 go 是以包的形式来管理文件和项目目录结构
1)区分相同名字的函数、变量等标识符
2)当程序文件很多时,可以很好的管理项目
3)控制函数、变量等访问范围,即作用域
1 2 3 4 5 import ( "包1" "包2" …… )
7.2.2 入门案例
文件结构
1 2 3 4 5 6 project1 ├─main │ main.go │ └─utils utils.go
utils.go
1 2 3 4 5 6 package utilsfunc Sum (a, b float64 ) float64 { return a + b }
main.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "go_code/project1/utils" )func main () { var a float64 = 20 var b float64 = 10 sum := utils.Sum(a, b) fmt.Println(sum) }
7.2.3 注意事项和使用细节
在给一个文件打包时,该包对应一个文件夹,比如这里的 utils
文件夹对应的包名就是 utils
,文件的包名通常 和文件所在的文件夹名一致,一般为小写字母。
所以也可以不保持一致,但是不会这么做,那么不保持一致的情况如下:
utils.go
1 2 3 4 5 6 package abc func Sum (a, b float64 ) float64 { return a + b }
main.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "go_code/project1/utils" )func main () { var a float64 = 20 var b float64 = 10 sum := abc.Sum(a, b) fmt.Println(sum) }
package
语句要放在文件第一行。
在 import
包时,路径从 $GOPATH
的 src
下开始,不用带 src
, 编译器会自动从 src
下开始引入
为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写 ,类似其它语言的 public,这样才能跨包访问。比如 utils.go 的。
对应的,首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat。
变量也是同样来控制访问 。函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效 。
在访问其它包函数,变量时,其语法是 包名.函数名
, 比如这里的 main.go 文件的。
如果包名较长,Go 支持给包取别名 , 注意细节:取别名后,原来的包名就不能使用了。
1 import ut "go_code/project1/utils"
1 2 3 4 import ( "fmt" ut "go_code/project1/utils" )
在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义。
如果你要编译成一个可执行程序文件,就需要将这个包声明为 main
, 即 package main
。这个就是一个语法规范,如果你是写一个库 ,包名可以自定义。
7.3 错误处理
1) 在默认情况下,当发生错误后(panic) , 程序就会退出(崩溃.)
2) 如果我们希望:当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后,给管理员一个提示(邮件,短信。。。)
3) 这里引出我们要将的错误处理机制
7.3.1 基本说明
1) Go 语言追求简洁优雅,所以,Go 语言不支持 传统的 try…catch…finally
这种处理。
2) Go 中引入的处理方式为:defer , panic , recover
3) 这几个异常的使用场景可以这么简单描述:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理
recover 在Go语言中,recover
是一个内置函数,用于拦截和处理函数执行过程中发生的恐慌(panic)。当一个函数发生恐慌时,它的正常执行流程会立即被中断,然后Go运行时开始逐层向上递归地执行每层函数调用的延迟(defer)语句。如果在这个过程中调用了 recover
,它会停止恐慌的传递过程,返回传递给 panic
调用的错误值,并恢复正常的执行流程。
panic后面介绍
例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func test () { defer func () { if err := recover (); err != nil { fmt.Println("err=" , err) } }() n1 := 10 n2 := 0 res := n1 / n2 fmt.Println("res=" , res) }func main () { test() fmt.Println("main执行test()之后的代码" ) }
查看输出 1 2 err= runtime error: integer divide by zero main执行test()之后的代码
7.3.2 自定义错误
Go 程序中,也支持自定义错误, 使用 errors.New
和 panic
内置函数。
1) errors.New("错误说明")
, 会返回一个 error
类型的值,表示一个错误
2) panic
内置函数 ,接收一个 interface{}
类型的值(也就是任何值了)作为参数。可以接收 error
类型的变量,输出错误信息 ,并退出程序 .
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport ( "errors" "fmt" )func readConf (name string ) (err error ) { if name == "init.conf" { return nil } else { return errors.New("读取文件错误……" ) } }func test () { err := readConf("xxx.conf" ) if err != nil { panic (err) } fmt.Println("test()继续执行" ) }func main () { test() }
查看输出 1 2 3 4 5 6 7 8 panic: 读取文件错误…… goroutine 1 [running]: main.test() d:/go/go_path_1.22.4/src/go_code/project1/main/main.go:24 +0x45 main.main() d:/go/go_path_1.22.4/src/go_code/project1/main/main.go:30 +0xf exit status 2
8 数组与切片
8.1 数组
数组可以存放多个同一类型 数据。数组也是一种数据类型,在 Go 中,数组是值类型 。
8.1.1 定义和内存布局
和C/C++一样,地址连续。
例:
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { var arr [5 ]int64 fmt.Printf("arr的地址\t%p\narr[0]的地址\t%p\narr[1]的地址\t%p\narr[2]的地址\t%p" , &arr, &arr[0 ], &arr[1 ], &arr[2 ]) }
查看输出 1 2 3 4 arr的地址 0xc00000e4b0 arr[0]的地址 0xc00000e4b0 arr[1]的地址 0xc00000e4b8 arr[2]的地址 0xc00000e4c0
从上面的例子可以看到,go的数组和C/C++的数组有所不同:
在Go语言中,当你声明一个数组,例如 var arr [5]int
,arr
实际上表示的是这个数组的整个数据结构,而不仅仅是一个指向数组首元素的指针。这与C语言中数组名被用作指向数组首元素的指针有所不同。尽管如此,在Go中,数组的名字在某些上下文中表现得类似于指针,尤其是在涉及内存地址时。
Go中数组的内存布局
在Go中,arr
指的是一个具体的数组结构,这个结构在内存中占据连续的空间,从 arr[0]
开始。这意味着数组 arr
的内存地址,即 &arr
,实际上就是其第一个元素 arr[0]
的地址,即 &arr[0]
。因此,输出 &arr
和 &arr[0]
通常会得到相同的内存地址值。
数组是值类型
在Go中,数组是值类型。当数组作为参数传递给函数时,会进行数组的完整复制,而不是像C语言中那样只传递指针。这意味着数组的大小和元素是该类型的固有部分。因此,在Go中,尽管 arr
和 arr[0]
的地址相同,这并不意味着 arr
是一个指针 。这只是因为数组的存储方式导致数组的首地址与其第一个元素的地址相同 。数组在Go中是一个独立的、完整的数据结构,这与C语言中的数组(通常用作指向其首元素的指针)有明显的区别。
8.1.2 数组的使用
1 2 3 4 5 6 7 8 var n11 [3 ]int = [3 ]int {1 , 2 , 3 }var n12 = [3 ]int {1 , 2 , 3 }var n21 = [...]int {8 , 9 , 10 }var n22 [3 ]int = [...]int {8 , 9 , 10 }var n31 [3 ]int = [3 ]int {1 :800 , 2 :900 , 0 :777 } var n32 = [...]int {1 :800 , 0 :900 , 2 :777 }
8.1.3 数组的遍历
方式一 :常规方式
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var nums = [3 ]int {1 , 2 , 3 } for i := 0 ; i < len (nums); i++ { fmt.Printf("%v " , nums[i]) } }
方式二 :for--range
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var nums = [3 ]int {1 , 2 , 3 } for index, value := range nums { fmt.Printf("nums[%v]=%v " , index, value) } }
1、第一个返回值index是数组的下标
2、第二个返回值是下标对应的值
8.1.4 注意事项和使用细节
数组是多个相同类型数据的组合 ,一个数组一旦声明/定义了,其长度是固定的,不能动态变化 。【和C/C++不同的是,go会报越界错误 】
var arr []int
这时 arr 就是一个 slice 切片,而不是数组。
数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
数组创建后,如果没有赋值,有默认值(零值)。【和C/C++区别】
使用数组的步骤 1. 声明数组并开辟空间 2. 给数组各个元素赋值(默认零值) 3. 使用数组。
Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响。【和C/C++区别】
如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
长度是数组类型的一部分,在传递函数参数时需要考虑数组的长度 ,看下面案例。
案例1:
1 2 3 4 5 6 7 8 9 package mainfunc test (arr []int ) { }func main () { var nums = [3 ]int {1 , 2 , 3 } test(nums) }
错误 :cannot use nums (variable of type [3]int) as []int value in argument to test
案例2:
1 2 3 4 5 6 7 8 9 package mainfunc test (arr [4]int ) { }func main () { var nums = [3 ]int {1 , 2 , 3 } test(nums) }
错误 :cannot use nums (variable of type [3]int) as [4]int value in argument to test
案例3:
1 2 3 4 5 6 7 8 9 package mainfunc test (arr [3]int ) { }func main () { var nums = [3 ]int {1 , 2 , 3 } test(nums) }
正确
在Go语言中,数组的大小必须在编译时是已知的,这意味着数组的长度必须是一个常量表达式 。
8.2 切片
先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数 是不确定的,请问怎么办?
解决方案:使用切片 。
8.2.1 切片的基本介绍
1) 切片的英文是 slice
2) 切片是数组的一个引用,因此切片是引用类型 ,在进行传递时,遵守引用传递的机制。
3) 切片的使用和数组类似 ,遍历切片、访问切片的元素和求切片长度 len(slice)
都一样。
4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组 。
5) 切片定义的基本语法:
快速入门案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var arr [5 ]int = [...]int {1 , 2 , 3 , 33 , 4 } slice := arr[1 :3 ] fmt.Println("arr=" , arr) fmt.Println("slice的元素是: " , slice) fmt.Println("slice的元素个数是: " , len (slice)) fmt.Println("slice的容量是: " , cap (slice)) }
查看输出 1 2 3 4 arr= [1 2 3 33 4] slice的元素是: [2 3] slice的元素个数是: 2 slice的容量是: 4
补充:
内建函数cap() :
内建函数cap返回 v 的容量,这取决于具体类型:
数组:v中元素的数量,与 len(v) 相同
数组指针:*v中元素的数量,与len(v) 相同
切片:切片的容量(底层数组的长度);若 v为nil,cap(v) 即为零
信道:按照元素的单元,相应信道缓存的容量;若v为nil,cap(v)即为零
8.2.2 内存布局
切片不存储任何数据本身,它们只是对底层数组的引用 。
切片的内存结构
切片在内存中的表示由三个主要部分组成:
指针 :指向切片所引用的数组的第一个元素的指针。这不一定是数组的物理起始地址,取决于切片是从数组的哪个部分开始的。
长度(length) :切片当前包含的元素个数。长度决定了可以安全访问切片的元素数目。
容量(capacity) :从切片的起始元素到底层数组末尾的元素个数。容量决定了切片可以增长到的最大长度,超过这个长度将导致重新分配底层数组,并复制现有元素。
例
想象有一个底层数组如下:
1 [0] [1] [2] [3] [4] [5] [6] [7] [8]
如果我们创建一个从索引2开始到索引5(不包括5)的切片,切片的结构将是:
指针 :指向索引2的位置。
长度 :3(因为包括索引2、3、4这三个元素)。
容量 :7(从索引2到数组的末尾索引8 )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { var arr [9 ]int = [...]int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } slice := arr[2 :5 ] fmt.Printf("arr[2]的地址为\t%p\n" , &arr[2 ]) fmt.Printf("slice[0]的地址为%p\n" , &slice[0 ]) fmt.Printf("slice的长度为 %v\n" , len (slice)) fmt.Printf("slice的容量为 %v\n" , cap (slice)) }
查看输出 1 2 3 4 arr[2]的地址为 0xc0000122e0 slice[0]的地址为0xc0000122e0 slice的长度为 3 slice的容量为 7
8.2.3 切片的使用
第一种方式 :定义一个切片,然后让切片去引用一个已经创建好的数组,比如前面的案例就是这样的。
1 2 var arr [9 ]int = [...]int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } slice := arr[2 :5 ]
第二种方式 :通过内建函数 make()
来创建切片
1 var 切片名 []type = make ([]type , len , [cap ])
type: 就是数据类型
len: 大小
cap:指定切片容量,可选, 如果你分配了 cap,则要求 cap>=len .
ps :如果没有给切片的各个元素赋值,那么就会使用默认值
第三种方式 :定义一个切片,直接就指定具体数组,使用原理类似 make 的方式
1 var slice []int = []int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }
8.2.4 切片的遍历
法一 :常规遍历
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var slice []int = []int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } for i := 0 ; i < len (slice); i++ { fmt.Printf("%d " , slice[i]) } }
法二 :for--range
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { var slice []int = []int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 } for _, v := range slice { fmt.Printf("%d " , v) } }
8.2.5 注意事项和使用细节
切片初始化时 var slice = arr[startIndex:endIndex]
说明:从 arr
数组下标为 startIndex
,取到下标为 endIndex
的元素(不含 arr[endIndex]
)。
切片初始化时,仍然不能越界。范围在 [0, len(arr))
之间,但是可以动态增长
简写:
var slice = arr[0:end]
可以简写 var slice = arr[:end]
var slice = arr[start:len(arr)]
可以简写 var slice = arr[start:]
var slice = arr[0:len(arr)]
可以简写: var slice = arr[:]
切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用。
1 2 var slice []int = []int {0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 }var slice1 []int = slice[1 :4 ]
用 append
内置函数,可以对切片进行**动态追加 **
1 func append (slice []Type, elems ...Type) []Type
内建函数append将元素追加到切片的末尾。若它有足够的容量,其目标就会重新切片以容纳新的元素。否则,就会分配一个新的基本数组。append返回更新后的切片,因此必须存储追加后的结果。【空间足够,不需要扩容O(1),空间不够,需要扩容,这时间复杂度为O(n)。平均时间复杂度O(1)】
1 2 slice = append (slice, elem1, elem2) slice = append (slice, anotherSlice...)
切片 append 操作的底层原理分析 :
1、切片 append 操作的本质就是对数组扩容
2、go 底层会创建一下新的数组 newArr(安装扩容后大小)
3、将 slice 原来包含的元素拷贝到新的数组 newArr
4、slice 重新引用到 newArr
5、注意 newArr 是在底层来维护的,程序员不可见.
1 func copy (dst, src []Type) int
内建函数copy将元素从来源切片复制到目标切片中,也能将字节从字符串复制到字节切片中。copy返回被复制的元素数量,它会是 len(src) 和 len(dst) 中较小的那个。来源和目标的底层内存可以重叠。【深拷贝】
例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" func main () { var slice []int = []int {0 , 1 , 2 } var slice1 []int = make ([]int , 5 ) var slice2 []int = make ([]int , 2 ) copy (slice1, slice) copy (slice2, slice) fmt.Println(slice) fmt.Println(slice1) fmt.Println(slice2) }
指针,slice,和 map 的零值都是 nil ,即还没有分配空间。
8.2.6 string和slice
string 底层是一个 byte 数组,因此 string 也可以进行切片处理
1 2 3 4 5 6 7 8 9 package mainimport "fmt" func main () { str := "hello world" slice := str[3 :] fmt.Println("slice=" , slice) }
string 是不可变的,也就说不能通过 str[0] = ‘z’ 方式来修改字符串
1 2 3 4 5 6 7 8 package mainimport "fmt" func main () { str := "hello world" str[0 ] = 'x' }
如果需要修改字符串,可以先将 string 转为 []byte / 或者 []rune,然后修改后转成 string
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { str := "hello world" arr1 := []byte (str) arr1[0 ] = 'x' str = string (arr1) fmt.Println(str) arr2 := []rune (str) arr2[0 ] = '哈' str = string (arr2) fmt.Println(str) }
8.2.7 练习题
编写一个函数 fbn(n int) ,要求完成能够将斐波那契的数列放到切片中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" func fbn (n int ) ([]uint64 ) { ans := make ([]uint64 , n) ans[0 ] = 1 ans[1 ] = 1 for i := 2 ; i < n; i++ { ans[i] = ans[i-2 ] + ans[i-1 ] } return ans }func main () { res := fbn(8 ) fmt.Println(res) }
8.3 二维数组
8.3.1 定义使用和内存布局
语法 :
初始化方法一:先声明再初始化
初始化方法二:声明的时候直接初始化
1 var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值, ……}, {初值, ……}, ……}
说明:方法二也有类似一维数组的写法
1 2 3 4 var 数组名 [大小][大小]类型 = [大小][大小]类型{{初值..},{初值..}}var 数组名 [大小][大小]类型 = [...][大小]类型{{初值..},{初值..}}var 数组名 = [大小][大小]类型{{初值..},{初值..}}var 数组名 = [...][大小]类型{{初值..},{初值..}}
8.3.2 遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" func main () { var arr = [2 ][3 ]int {{1 , 2 , 3 }, {4 , 5 , 6 }} for i := 0 ; i < len (arr); i++ { for j := 0 ; j < len (arr[i]); j++ { fmt.Printf("%d " , arr[i][j]); } fmt.Println() } for _, v := range arr { for _, v2 := range v { fmt.Printf("%d " , v2) } fmt.Println() } }
8.4 二维切片
在Go语言中定义和使用二维切片(切片的切片)涉及几个步骤,从基础的创建和初始化到灵活的操作和应用。这里,我们将逐步介绍如何定义、初始化、填充、操作和使用二维切片。
定义二维切片
二维切片本质上是一个切片,其中的每个元素本身也是一个切片。这种结构允许你创建类似于二维数组的数据结构,但与传统的二维数组不同的是,二维切片的每一行可以独立调整大小。
基本定义语法:
这行代码定义了一个名为 matrix
的二维切片,其中存储的元素类型为 int
。
初始化二维切片
二维切片在使用前需要初始化。因为二维切片实际上是切片的切片,所以你需要先初始化外层切片,然后初始化每一个内层切片。
使用 make
进行初始化:
1 2 3 4 5 6 rows := 3 cols := 4 matrix := make ([][]int , rows)for i := range matrix { matrix[i] = make ([]int , cols) }
这段代码创建了一个具有 3 行 4 列的二维切片。make([][]int, rows)
初始化外层切片,并设置其长度为 rows
。接着,通过循环为每个内层切片分配长度为 cols
的空间。
填充二维切片
定义并初始化二维切片后,接下来可以填充它。
示例:填充数据
1 2 3 4 5 for i := 0 ; i < rows; i++ { for j := 0 ; j < cols; j++ { matrix[i][j] = i + j } }
这里使用两层循环填充切片,使得 matrix[i][j]
的值为 i + j
。
访问和修改二维切片
访问和修改二维切片的元素非常直接。
示例:修改元素
示例:追加元素
如果需要在特定行追加新列或添加新行:
1 2 3 4 5 6 matrix[0 ] = append (matrix[0 ], 99 ) newRow := []int {10 , 20 , 30 , 40 } matrix = append (matrix, newRow)
遍历二维切片
遍历二维切片通常使用嵌套循环来完成。
示例:打印所有元素
1 2 3 4 5 6 for _, row := range matrix { for _, value := range row { fmt.Print(value, " " ) } fmt.Println() }
9 map
9.1 基本语法
声明 :
1 var map 变量名 map [keytype]valuetype
key 可以是什么类型
golang 中的 map 的 key 可以是很多种类型,比如 bool, 数字,string,指针,channel , 还可以是只包含前面几个类型的接口, 结构体, 数组
通常 key 为 int 、string
注意: slice, map 还有 function 不可以,因为这几个没法用 ==
号来判断
value 可以是什么类型
value 的类型和 key 基本一样,这里我就不再赘述了通常为: 数字(整数,浮点数), string, map, struct
声明的举例 :
1 2 3 4 var a map [string ]string var a map [string ]int var a map [int ]string var a map [string ]map [string ]string
注意:声明是不会分配内存的,初始化需要 make ,分配内存后才能赋值和使用。
1 func make (Type, size IntegerType) Type
映射:初始分配的创建取决于size,但产生的映射长度为0。size可以省略,这种情况下就会分配一个小的起始大小。
案例 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { var a map [string ]string a = make (map [string ]string , 10 ) a["no1" ] = "宋江" a["no2" ] = "吴用" a["no1" ] = "武松" fmt.Println(a) }
对上面代码的说明
1) map 在使用前一定要 make
2) map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
3) map 的 value 是可以相同的.
4) map 的 key-value 是无序
5) make 内置函数数目
9.2 使用
方式1 :先声明,再map
1 2 3 4 var a map [string ]string a = make (map [string ]string , 10 ) a["no1" ] = "宋江" a["no2" ] = "吴用"
方式2 :声明的时候直接map
1 2 3 a := make (map [string ]string , 10 ) a["no1" ] = "宋江" a["no2" ] = "吴用"
方式3 :声明的时候直接赋值
1 2 3 4 a := map [string ]string { "no1" : "宋江" , "no2" : "吴用" , }
9.3 增删改查
增加和更新
删除
delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在, 不操作,但是也不会报错。
细节说明
1、如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除
2、或者 map = make(…),make 一个新的,让原来的成为垃圾,被 gc 回收 。
查找
1 2 3 4 5 6 val, ok := a["no3" ]if ok { fmt.Printf("有no3 key, 值为%v\n" , val) } else { fmt.Printf("没有no3 key\n" , val) }
9.4 遍历
map 的遍历使用 for-range 的结构遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { a := map [string ]string { "no1" : "宋江" , "no2" : "吴用" , } for k, v := range a { fmt.Printf("key=%v val=%v\n" , k, v) } }
9.5 map切片
切片的数据类型如果是 map ,则我们称为 slice of map,map 切片 ,这样使用则 map 个数就可以动态变化了 。
案例:使用一个 map 来记录 monster 的信息 name 和 age, 也就是说一个 monster 对应一个 map,并且妖怪的个数可以动态的增加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport "fmt" func main () { var monsters []map [string ]string monsters = make ([]map [string ]string , 2 ) if monsters[0 ] == nil { monsters[0 ] = make (map [string ]string , 2 ) monsters[0 ]["name" ] = "牛魔王" monsters[0 ]["age" ] = "1000" } if monsters[1 ] == nil { monsters[1 ] = make (map [string ]string , 2 ) monsters[1 ]["name" ] = "玉兔精" monsters[1 ]["age" ] = "500" } newMonster := map [string ]string { "name" : "火云邪神" , "age" : "200" , } monsters = append (monsters, newMonster) fmt.Println(monsters) }
查看输出 1 [map[age:1000 name:牛魔王] map[age:500 name:玉兔精] map[age:200 name:火云邪神]]
9.6 map排序
1) golang 中没有一个专门的方法针对 map 的 key 进行排序
2) golang 中的 map 默认是无序的,注意也不是按照添加的顺序存放的,你每次遍历,得到的输出可能不一样. 【案例演示 1】
3) golang 中 map 的排序,是先将 key 进行排序,然后根据 key 值遍历输出即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport ( "fmt" "sort" )func main () { map1 := make (map [int ]int , 10 ) map1[10 ] = 100 map1[1 ] = 102 map1[3 ] = 101 map1[5 ] = 112 map1[2 ] = 1056 var keys []int for k, _ := range map1 { keys = append (keys, k) } sort.Ints(keys) for _, k := range keys { fmt.Printf("map[%v]=%v\n" , k, map1[k]) } }
查看输出 1 2 3 4 5 map[1]=102 map[2]=1056 map[3]=101 map[5]=112 map[10]=100
9.7 使用细节
map 是引用类型,遵守引用类型传递的机制,在一个函数接收 map,修改后,会直接修改原来的 map
map 的容量达到后,再想 map 增加元素,会自动扩容,并不会发生 panic,也就是说 map 能动态的增长键值对(key-value)
map 的 value 也经常使用 struct 类型 ,更适合管理复杂的数据(比前面 value 是一个 map 更好),比如 value 为 Student 结构体
使用len()
可以知道map有多少对键值对
10 面向对象
Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言 。所以我们说 Golang 支持面向对象编程特性 是比较准确的。
Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,你可以理解 Golang 是基于 struct 来实现 OOP 特性的。
Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等。
Golang 仍然有面向对象编程的继承,封装和多态 的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低 ,也非常灵活。后面同学们会充分体会到这个特点。也就是说在Golang 中面向接口编程 是非常重要的特性。
10.1 结构体
10.1.1 结构体入门和内存分布
通过实例 来入门:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" type Cat struct { Name string Age int Color string Hobby string }func main () { var cat1 Cat cat1.Name = "小白" cat1.Age = 2 cat1.Color = "白" cat1.Hobby = "吃<·)#))<" fmt.Println(cat1) fmt.Println("cat1的名字=" , cat1.Name) fmt.Println("cat1的年龄=" , cat1.Age) fmt.Println("cat1的颜色=" , cat1.Color) fmt.Println("cat1的爱好=" , cat1.Hobby) }
查看输出 1 2 3 4 5 {小白 2 白 吃<·)#))<} cat1的名字= 小白 cat1的年龄= 2 cat1的颜色= 白 cat1的爱好= 吃<·)#))<
内存布局 :
在Go语言中,结构体的内存布局是按照其字段在结构体定义中的顺序连续放置的,但具体的布局细节还受到字段对齐(alignment)规则的影响。这些规则是由编译器自动处理的,旨在优化内存访问速度和硬件效率。
结构体字段对齐的基本规则是:每个字段在内存中的起始位置应该是其类型大小的倍数。这是为了确保CPU访问内存时的效率,因为大多数硬件平台访问对齐的内存地址比非对齐的地址更快。【结构体的对齐值是其所有字段对齐值的最大者。这与C++中的处理方式相似,但在Go中,结构体的总大小必须是其最大字段对齐值的整数倍 ,这点与C++相同。】
假设我们有以下结构体:
1 2 3 4 5 6 type Example struct { A bool B int32 C int64 D string }
在64位的系统上,这个结构体的内存布局会受到对齐规则的影响,从而可能出现内存填充(padding):
A
作为 bool
类型,占用1字节,但后面可能需要填充3字节,以保证 B
作为 int32
能够从4字节边界开始。
B
正好对齐,占用4字节。
C
为 int64
,从下一个8字节边界开始。
D
在64位系统中是一个指针,占用8字节,并且自然对齐到8字节边界。
如果我们假设内存地址从0开始,该结构体可能的内存布局如下:
1 2 | A |pad|pad|pad| B | C | D | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
这里 pad
表示编译器插入的填充字节,以保证后续字段的对齐。
为了减少因对齐而造成的内存浪费,可以通过调整字段的顺序来优化结构体的内存布局。例如,将上述结构体的字段重新排序,使得较大的字段先声明:
1 2 3 4 5 6 type OptimizedExample struct { C int64 B int32 A bool D string }
这样排列可以减少或消除填充,因为较小的字段可以填补由较大字段留下的间隙。
如果你想查看Go中任何数据结构的实际内存布局,可以使用 unsafe
包的 Sizeof
、Alignof
和 Offsetof
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" "unsafe" )func main () { var x Example fmt.Println("Sizeof(Example):" , unsafe.Sizeof(x)) fmt.Println("Alignof(Example):" , unsafe.Alignof(x)) fmt.Println("Offsetof(B):" , unsafe.Offsetof(x.B)) fmt.Println("Offsetof(C):" , unsafe.Offsetof(x.C)) fmt.Println("Offsetof(D):" , unsafe.Offsetof(x.D)) }
这将打印结构体 Example
的大小、对齐要求和每个字段的偏移量,帮助你理解内存布局。
10.1.2 基本语法
语法 :
1 2 3 4 type 结构体名称 struct { field1 type field2 type }
说明 :
注意事项和细节说明
1) 字段的类型可以为:基本类型、数组或引用类型
2) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样。
3)不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型 。
10.1.3 创建和访问字段
使用 :
现在有这样一个结构体:
1 2 3 4 type Person struct { Name string Age int }
那么如何定义变量和访问字段呢
方式一 :直接声明
方式二 :声明并定义
1 var person Person = Person{}
1 var person Person = Person{"merry" , 20 }
方式三 :使用new分配结构体指针
1 var person *Person = new (Person)
指针类型怎么访问字段?
标准方式:
这种方式类似C/C++,但是为了程序员使用方便,可以如下这么写:
go的底层会对这句代码进行处理,会给p加上*
,变回之前的方式。go设计者认为这种方式更符合程序员的使用习惯。
方式四 :用&
1 var person *Person = &Person{}
1 var person *Person = &Person{"merry" , 20 }
10.1.4 创建struct实例时指定字段值
方式一
1 2 3 4 5 6 var stu1 Student = Student{"tom" , 10 } stu2 := Student{"tom" , 10 } stu3 := Student{ Name: "tom" , Age: 10 , }
方式二
1 var stu4 *Student = &Student{"tom" , 10 }
10.1.5 注意事项和使用细节
结构体的所有字段在内存中是连续 的
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段 (名字、个数和类型,字段顺序也要一样)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type Teacher struct { Name string Age int }type Student struct { Name string Age int }func main () { teacher1 := Teacher{"xxx" , 30 } student1 := Student(teacher1) fmt.Println(student1) }
结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" type Student struct { Name string Age int }type Teacher Studentfunc main () { teacher1 := Teacher{"xxx" , 30 } student1 := Student(teacher1) fmt.Println(student1) }
struct 的每个字段上,可以写上一个 tag , 该 tag 可以通过反射机制 获取,常见的使用场景就是序列化 和反序列化。【后面会再讲】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "encoding/json" "fmt" )type Student struct { Name string `json:"name"` Age int `json:"age"` }func main () { stu := Student{"xxx" , 15 } jsonStr, err := json.Marshal(stu) if err != nil { fmt.Println("json处理错误" , err) } else { fmt.Println("jsonStr: " , string (jsonStr)) } }
10.2 方法
在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外( 年龄,姓名…),Person 结构体还有一些行为比如:可以说话、跑步…,通过学习,还可以做算术题。这时就要用方法才能完成。
Golang 中的方法是作用在指定的数据类型上的 (即:和指定的数据类型绑定),因此自定义类型, 都可以有方法 ,而不仅仅是 struct 。
10.2.1 方法的声明和调用
语法 :
1 2 3 4 func (recevier type ) methodName(参数列表) (返回值列表){ 方法体 return 返回值 }
1) 参数列表:表示方法输入
2) recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3) receiver type : type 可以是结构体,也可以其它的自定义类型
4) receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5) 返回值列表:表示返回的值,可以多个
6) 方法主体:表示为了实现某一功能代码块
7) return 语句不是必须的。
例:
1 2 3 4 5 6 type A struct { Num int }func (a A) test() { fmt.Println(a.Num) }
说明
1) func (a A) test() {}
表示 A 结构体有一方法,方法名为 test
2) (a A)
体现 test
方法是和 A
类型绑定的
举例说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type Person struct { Name string }func (p Person) test() { fmt.Println("test()调用了,Name=" , p.Name) }func main () { var p Person p.Name = "tom" p.test() }
对上面的总结
1) test 方法和 Person 类型绑定
2) test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3) func (p Person) test() {},p 表示哪个 Person 变量调用,这个 p 就是它的副本, 这点和函数传参非常相似。【所以在test()中修改成员变量是不行的,p也被当做变量传入函数中】
10.2.2 注意事项和使用细节
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type Person struct { Name string }func (p *Person) test() { p.Name = "xxx" }func main () { var p Person p.Name = "tom" p.test() fmt.Println(p.Name) }
【为了提高效率,通常用的也是指针,可以减少拷贝 】
Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法。
方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
如果一个类型实现了 String() 这个方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" type Person struct { Name string }func (p *Person) String() string { return "绑定的String(),name=" + p.Name }func main () { var p Person p.Name = "tom" fmt.Println(p) fmt.Println(&p) }
查看输出 1 2 {tom} 绑定的String(),name=tom
10.3 工厂模式
Golang 的结构体没有构造函数 ,通常可以使用工厂模式来解决这个问题。
先来开一个需求:
一个结构体的声明是这样的:
1 2 3 4 type student struct { Name string Score float64 }
因为这里student是小写的,在别的包用不了,怎么办?
使用工厂模式实现跨包创建结构体实例(变量):
直接看例子就行:
文件结构:
1 2 3 4 5 6 factory ├─main │ main.go │ └─model student.go
student.go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package modeltype student struct { Name string Score float64 }func NewStudent (Name string , Score float64 ) *student { return &student{ Name: Name, Score: Score, } }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport ( "fmt" "go_code/factory/model" )func main () { stu := model.NewStudent("tom" , 88.6 ) fmt.Println(stu) }
这样问题就得到了解决
10.4 编译器逃逸分析
在10.3工厂模式 小节,我们编写了如下代码:
1 2 3 4 5 6 func NewStudent (Name string , Score float64 ) *student { return &student{ Name: Name, Score: Score, } }
让人感到疑惑的是,在函数内部创造的结构体,居然还能被函数外的指针引用,按道理说函数结束,这个结构体的空间会被释放才对,然后达不到这个函数想要的效果才对,那是什么原因呢?
在这个函数中,一个新的 student
结构体实例被创建并通过函数返回。由于返回的是对该结构体实例的引用(指针),该实例的生命周期可能会超出 NewStudent
函数的作用域。因此,编译器可能会判断这个结构体实例需要在堆上分配,从而使得其在函数执行完毕后仍然可用。
什么是逃逸?
在Go中,逃逸 指的是变量在其定义作用域之外被引用的情况。逃逸分析是编译器在编译阶段进行的一种优化,用来确定变量应该存储在堆还是栈上。
栈上分配 :如果一个变量的生命周期仅限于其定义的函数内部,它可以在栈上分配。栈上分配和释放通常非常快速,且管理起来比较简单。
堆上分配 :如果一个变量在函数外部被引用,或者其引用方式使得其生命周期不能由编译器确定,它将被分配在堆上。堆上的内存管理由Go的垃圾回收器负责,可能会有更高的性能开销。
工厂模式中的逃逸情况
这里创建了一个 student
的新实例,并返回一个指向它的指针。这种方式通常会导致逃逸到堆上,因为:
返回指针 :函数返回了一个指向 student
实例的指针。由于函数返回后指针依然有效,这意味着 student
实例的生命周期至少与这个返回的指针一样长。
未知的引用情况 :返回的指针可能被赋给任何外部变量或者用于其他函数调用,编译器无法确保其只在局部使用,因此必须保守处理,分配在堆上。
如何查看逃逸分析结果
可以通过以下命令查看你的Go程序的逃逸分析报告,这有助于理解特定的变量为何需要在堆上分配:
1 go build -gcflags='-m' your_program.go
这条命令会在构建时输出编译器的逃逸分析详情,显示哪些变量逃逸到堆上以及原因。
总结
逃逸分析是Go编译器的一个重要特性,它帮助程序在保证性能的同时,自动管理内存。通过了解逃逸分析,开发者可以更好地理解内存的使用情况,有时还可以通过调整代码设计来优化性能和内存使用。在你的案例中,NewStudent
函数中的 student
实例很可能会因为逃逸到堆上而被垃圾回收器管理。
10.5 封装
封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。
好处:
1) 隐藏实现细节
2) 提可以对数据进行验证 ,保证安全合理
如何体现封装
1) 对结构体中的属性进行封装
2) 通过方法 ,包 实现封装
封装的实现步骤
1) 将结构体、字段(属性)的首字母小写 (不能导出了,其它包不能使用,类似 private)
2) 给结构体所在包提供一个工厂模式的函数,首字母大写。类似一个构造函数
3) 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值
1 2 3 4 func (var 结构体类型名) SetXxx(参数列表) (返回值列表) { var .字段 = 参数 }
4) 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值
1 2 3 func (var 结构体类型名) GetXxx() { return var .age; }
特别说明 :在 Golang 开发中并没有特别强调封装,这点并不像 Java,所以提醒学过java 的朋友,不用总是用 java 的语法特性来看待 Golang, Golang 本身对面向对象的特性做了简化的.
案例
请写一个程序(person.go),不能随便查看人的年龄,工资等隐私,并对输入的年龄进行合理的验证。
文件结构:
1 2 3 4 5 6 encapsulate ├─main │ main.go │ └─model person.go
person.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package modelimport "fmt" type person struct { name string age int sal float64 }func NewPerson () *person { return &person{} }func (p *person) SetName(name string ) { p.name = name }func (p *person) GetName() string { return p.name }func (p *person) SetAge(age int ) { if age > 0 && age < 150 { p.age = age } else { fmt.Println("年龄范围不正确" ) } }func (p *person) GetAge() int { return p.age }func (p *person) SetSal(sal float64 ) { if sal >= 300 && sal <= 30000 { p.sal = sal } else { fmt.Println("薪水设置范围不正确" ) } }func (p *person) GetSal() float64 { return p.sal }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "fmt" "go_code/encapsulate/model" )func main () { p := model.NewPerson() p.SetName("tom" ) p.SetAge(22 ) p.SetSal(28500 ) fmt.Println(p.GetName(), "age=" , p.GetAge(), "sal=" , p.GetSal()) }
10.6 继承
继承可以解决代码复用,让我们的编程更加靠近人类思维。
当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
其它的结构体不需要重新定义这些属性(字段)和方法。
示意图:
10.6.1 单继承
嵌入匿名结构体的基本语法
1 2 3 4 5 6 7 8 9 type Goods struct { Name string Price int }type Book struct { Goods Writer string }
当我们对结构体嵌入了匿名结构体使用方法会发生变化 :book.Goods.Goods的方法
案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package mainimport ( "fmt" )type Student struct { Name string Age int Score int }func (stu *Student) ShowInfo() { fmt.Printf("学生名=%v 年龄=%v 成绩=%v\n" , stu.Name, stu.Age, stu.Score) }func (stu *Student) SetScore(score int ) { stu.Score = score }func (stu *Student) GetSum(n1 int , n2 int ) int { return n1 + n2 }type Pupil struct { Student }func (p *Pupil) testing() { fmt.Println("小学生正在考试中....." ) }type Graduate struct { Student }func (p *Graduate) testing() { fmt.Println("大学生正在考试中....." ) }func main () { pupil := &Pupil{} pupil.Student.Name = "tom~" pupil.Student.Age = 8 pupil.testing() pupil.Student.SetScore(70 ) pupil.Student.ShowInfo() fmt.Println("res=" , pupil.Student.GetSum(1 , 2 )) graduate := &Graduate{} graduate.Student.Name = "mary~" graduate.Student.Age = 28 graduate.testing() graduate.Student.SetScore(90 ) graduate.Student.ShowInfo() fmt.Println("res=" , graduate.Student.GetSum(10 , 20 )) }
10.6.2 继承的深入讨论
1) 结构体可以使用嵌套匿名结构体所有的字段和方法 ,即:首字母大写或者小写的字段、方法, 都可以使用。
2) 匿名结构体字段访问可以简化
1 2 3 pupil.Student.SetScore(70 ) pupil.SetScore(70 )
(1) 当我们直接通过 Pupil 访问字段或方法时,其执行流程如下比如 pupil.SetScore(70)
(2) 编译器会先看 Pupil 对应的类型有没有 SetScore()方法, 如果有,则直接调用 Pupil 类型的 SetScore()方法
(3) 如果没有就去看 Pupil 中嵌入的匿名结构体 Student 有没有声明 SetScore()方法,如果有就调用,如果没有继续查找…如果都找不到就报错.
3) 当结构体 和匿名结构体 有相同的字段或者方法时,编译器采用就近访问原则访问 ,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分。
4) 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法 (同时结构体本身没有同名的字段和方法 ),在访问时,就必须明确指定匿名结构体名字,否则编译报错。
1 2 3 4 5 6 7 8 9 10 11 12 type A struct { Name string }type B struct { Name string }type C struct { A B }
5) 如果一个 struct 嵌套了一个有名结构体,这种模式就是**组合 **,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字
6) 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的 值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package mainimport "fmt" type Goods struct { Name string Price float64 }type Brand struct { Name string Adderss string }type Tv1 struct { Goods Brand }type Tv2 struct { *Goods *Brand }func main () { tv1 := Tv1{ Goods{ "电视机001" , 5000.1 , }, Brand{ "TCL" , "深圳" , }, } fmt.Println(tv1) tv2 := Tv2{ &Goods{ "电视机001" , 5000.1 , }, &Brand{ "TCL" , "深圳" , }, } fmt.Println(tv2) }
7) 结构体的匿名字段是基本数据类型,如何访问?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" type A struct { int }func main () { a := A{10 } fmt.Println(a) a.int = 60 fmt.Println(a) }
1) 如果一个结构体有 int 类型的匿名字段,就不能第二个。
2) 如果需要有多个 int 的字段,则必须给 int 字段指定名字
10.6.3 多重继承
如果一个 struct 嵌套了多个匿名结构体 ,那么该结构体可以直接访问嵌套的匿名结构体的字段和方法,从而实现了多重继承 。
前面已经有案例了,我们直接拿过来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package mainimport "fmt" type Goods struct { Name string Price float64 }type Brand struct { Name string Adderss string }type Tv1 struct { Goods Brand }type Tv2 struct { *Goods *Brand }func main () { tv1 := Tv1{ Goods{ "电视机001" , 5000.1 , }, Brand{ "TCL" , "深圳" , }, } fmt.Println(tv1) tv2 := Tv2{ &Goods{ "电视机001" , 5000.1 , }, &Brand{ "TCL" , "深圳" , }, } fmt.Println(tv2) }
1、如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
2、为了保证代码的简洁性,建议大家尽量不使用多重继承 。【就是问题会复杂化】
10.7 接口
10.7.1 基本介绍
通过一个 案例 来入门:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 package mainimport "fmt" type Usb interface { Start() Stop() }type Phone struct { }func (p Phone) Start() { fmt.Println("手机开始工作……" ) }func (p Phone) Stop() { fmt.Println("手机停止工作……" ) }type Camera struct { }func (p Camera) Start() { fmt.Println("相机开始工作……" ) }func (p Camera) Stop() { fmt.Println("相机停止工作……" ) }type Computer struct { }func (c Computer) Working(usb Usb) { usb.Start() usb.Stop() }func main () { computer := Computer{} phone := Phone{} camera := Camera{} computer.Working(phone) computer.Working(camera) }
查看输出 1 2 3 4 手机开始工作…… 手机停止工作…… 相机开始工作…… 相机停止工作……
10.7.2 语法
概念 :
interface 类型可以定义一组方法,但是这些不需要实现。并且 interface 不能包含任何变量 。到某个自定义类型(比如结构体 Phone)要使用的时候,在根据具体情况把这些方法写出来(实现)。
基本语法 :
定义接口:
1 2 3 4 5 type 接口名 interface { method1(参数列表) 返回值列表 method2(参数列表) 返回值列表 …… }
实现接口所有方法:
1 2 3 4 5 6 7 8 9 func (t 自定义类型) method1(参数列表) 返回值列表 { }func (t 自定义类型) method2(参数列表) 返回值列表 { } ……
说明 :
1) 接口里的所有方法都没有方法体 ,即接口的方法都是没有实现的方法。接口体现了程序设计的多态 和高内聚低偶合 的思想。
2) Golang 中的接口,不需要显式的实现 。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现这个接口。因此,Golang 中没有 implement 这样的关键字。
10.7.3 应用场景
对初学者讲,理解接口的概念不算太难,难的是不知道什么时候使用接口,下面例举几个应用场景:
1、现在美国要制造轰炸机,武装直升机,专家只需把飞机需要的功能/规格定下来即可,然后让别的人具体实现就可。
2、现在有一个项目经理,管理三个程序员,开发一个软件,为了控制和管理软件项目经理可以定义一些接口,然后由程序员具体实现。
10.7.4 注意事项和使用细节
1) 接口本身不能创建实例 ,但是可以指向一个实现了该接口的自定义类型的变量 (实例)
这么写报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package maintype AInterface interface { Say() }func main () { var a AInterface a.Say() }
正确使用应该是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" type AInterface interface { Say() }type Student struct { Name string }func (stu Student) Say() { fmt.Println("stu Say()……" ) }func main () { stu := Student{} var a AInterface = stu a.Say() }
2) 接口中所有的方法都没有方法体,即都是没有实现的方法。
3) 在 Golang 中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口。
4) 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
5) 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type AInterface interface { Say() }type integer int func (i integer) Say() { fmt.Println("integer Say i=" , i) }func main () { var i integer = 10 var a AInterface = i a.Say() }
6) 一个自定义类型可以实现多个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type AInterface interface { Say() }type BInterface interface { Hello() }type integer int func (i integer) Say() { fmt.Println("integer Say i=" , i) }func (i integer) Hello() { fmt.Println("integer Hello i=" , i) }
7) Golang 接口中不能有任何变量
8) 一个接口(比如 A 接口)可以继承多个别的接口(比如 B,C 接口),这时如果要实现 A 接口,也必须将 B,C 接口的方法也全部实现。【但是继承不能有相同的方法名】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package maintype BInterface interface { test01() }type CInterface interface { test02() }type AInterface interface { BInterface CInterface test03() }type Stu struct { }func (stu Stu) test01() { }func (stu Stu) test02() { }func (stu Stu) test03() { }func main () { var stu Stu var a AInterface = stu a.test01() }
9) interface 类型默认是一个指针(引用类型),如果没有对 interface 初始化就使用,那么会输出 nil
10) 空接口 interface{} 没有任何方法,所以所有类型都实现了空接口 , 即我们可以把任何一个变量赋给空接口 。
空接口两种写法:
1、正常定义一个空接口使用
2、如下这么写:interface{}
10.7.5 练习
题目1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type AInterface interface { Test01() Test02() }type BInterface interface { Test01() Test03() }type CInterface interface { AInterface BInterface }func main () { }
上面代码有问题,因为CInterface继承的两个AInterface和BInterface都有Test01(),会直接编译错误。
为什么呢?相当于CInterface中有两个Test01(),报重复定义错误。
题目2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" type Usb interface { Say() }type Phone struct { }func (p *Phone) Say() { fmt.Println("Say()" ) }func main () { var phone Phone = Phone{} var u Usb = &phone u.Say() }
10.7.6 接口编程的最佳实践
资料:
1 func Sort (data Interface)
Sort排序data。它调用1次data.Len确定长度,调用O(n*log(n))次data.Less和data.Swap。本函数不能保证排序的稳定性(即不保证相等元素的相对次序不变)。
1 2 3 4 5 6 7 8 type Interface interface { Len() int Less(i, j int ) bool Swap(i, j int ) }
一个满足sort.Interface接口的(集合)类型可以被本包的函数进行排序。方法要求集合中的元素可以被整数索引。
实现对Hero结构体切片的排序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package mainimport ( "fmt" "math/rand" "sort" )type Hero struct { Name string Age int }type HeroSlice []Herofunc (hs HeroSlice) Len() int { return len (hs) }func (hs HeroSlice) Less(i, j int ) bool { return hs[i].Name < hs[j].Name }func (hs HeroSlice) Swap(i, j int ) { hs[i], hs[j] = hs[j], hs[i] }func main () { var heros HeroSlice for i := 0 ; i < 10 ; i++ { hero := Hero{ Name : fmt.Sprintf("hero%d" , rand.Intn(100 )), Age : rand.Intn(100 ), } heros = append (heros, hero) } for _, v := range heros { fmt.Println(v) } sort.Sort(heros) fmt.Println("--------------------" ) for _, v := range heros { fmt.Println(v) } }
10.7.7 接口 VS 继承
实现接口可以看作是对 继承 的一直补充
例:
接口和继承解决的解决的问题不同
继承的价值主要在于:解决代码的复用性 和可维护性 。
接口的价值主要在于:设计 ,设计好各种规范(方法),让其它自定义类型去实现这些方法。
接口比继承更加灵活,比较松散。
接口比继承更加灵活,继承是满足 is - a 的关系,而接口只需满足 like - a 的关系
接口在一定程度上实现代码 解耦
10.8 多态
变量(实例)具有多种形态。面向对象的第三大特征,在 Go 语言,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现。这时接口变量就呈现不同的形态。
案例:上一节的 10.7.1 基本介绍 的“USB”例子
接口体现多态的两种方式
多态参数
在前面的 Usb 接口案例,(usb Usb)
,即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态。
多态数组
演示一个案例:给 Usb 数组中,存放 Phone 结构体 和 Camera 结构体变量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package mainimport "fmt" type Usb interface { Start() Stop() }type Phone struct { name string }func (p Phone) Start() { fmt.Println("手机开始工作……" ) }func (p Phone) Stop() { fmt.Println("手机停止工作……" ) }type Camera struct { name string }func (p Camera) Start() { fmt.Println("相机开始工作……" ) }func (p Camera) Stop() { fmt.Println("相机停止工作……" ) }func main () { var usbArr [3 ]Usb usbArr[0 ] = Phone{"vivo" } usbArr[1 ] = Phone{"小米" } usbArr[2 ] = Camera{"Sony" } fmt.Println(usbArr) }
10.9 类型断言
10.9.1 基本介绍
由一个具体的需求,引出了类型断言
先看下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" type Point struct { x int y int }func main () { var a interface {} var point Point = Point{1 , 2 } a = point var b Point b = a fmt.Println(b) }
正确的做法:
1 2 3 var b Point b = a.(Point) fmt.Println(b)
基本介绍
类型断言:由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言, 具体的如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { var x interface {} var b2 float32 = 1.1 x = b2 y := x.(float32 ) fmt.Printf("y的类型是%T, 值为%v" , y, y) }
说明:在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.
如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" func main () { var x interface {} var b2 float32 = 1.1 x = b2 if y, ok := x.(float32 ); ok { fmt.Println("convert success" ) fmt.Printf("y的类型为%T, 值为%v" , y, y) } else { fmt.Println("convert fail" ) } fmt.Println("继续执行……" ) }
10.9.2 类型断言的最佳实践
最佳实践1 :
在前面的 Usb 接口案例做改进:
给 Phone 结构体增加一个特有的方法 call(), 当 Usb 接口接收的是Phone 变量时,还需要调用 call 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 package mainimport "fmt" type Usb interface { Start() Stop() }type Phone struct { name string }func (p Phone) Start() { fmt.Println("手机开始工作……" ) }func (p Phone) Stop() { fmt.Println("手机停止工作……" ) }func (p Phone) Call() { fmt.Println("手机打电话……" ) }type Camera struct { name string }func (p Camera) Start() { fmt.Println("相机开始工作……" ) }func (p Camera) Stop() { fmt.Println("相机停止工作……" ) }type Computer struct { }func (computer Computer) Working(usb Usb) { usb.Start() if phone, ok := usb.(Phone); ok { phone.Call() } usb.Stop() }func main () { var usbArr [3 ]Usb usbArr[0 ] = Phone{"vivo" } usbArr[1 ] = Phone{"小米" } usbArr[2 ] = Camera{"Sony" } var computer Computer for _, v := range usbArr { computer.Working(v) fmt.Println("--------------------" ) } }
查看输出 1 2 3 4 5 6 7 8 9 10 11 手机开始工作…… 手机打电话…… 手机停止工作…… -------------------- 手机开始工作…… 手机打电话…… 手机停止工作…… -------------------- 相机开始工作…… 相机停止工作…… --------------------
最佳实践2
写一函数,循环判断传入参数的类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package mainimport "fmt" func TypeJudege (items ...interface {}) { for index, x := range items { switch x.(type ) { case bool : fmt.Printf("第%v个参数是bool类型,值是%v\n" , index, x) case float32 : fmt.Printf("第%v个参数是float32类型,值是%v\n" , index, x) case float64 : fmt.Printf("第%v个参数是float64类型,值是%v\n" , index, x) case int , int32 , int64 : fmt.Printf("第%v个参数是整型,值是%v\n" , index, x) case string : fmt.Printf("第%v个参数是string类型,值是%v\n" , index, x) default : fmt.Printf("第%v个参数的类型不确定\n" , index) } } }func main () { var n1 float32 = 1.1 var n2 float64 = 1.2 var n3 int = 10 var name string = "xxx" TypeJudege(n1, n2, n3, name) }
查看输出 1 2 3 4 第0个参数是float32类型,值是1.1 第1个参数是float64类型,值是1.2 第2个参数是整型,值是10 第3个参数是string类型,值是xxx
11 文件操作(doing)
11.1 文件的基本介绍
输入流和输出流 :
流:数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径
os.File
封装所有文件相关操作,File
是一个结构体
type File
11.2 打开和关闭文件
打开函数 :
1 func Open (name string ) (file *File, err error )
Open打开一个文件用于读取。如果操作成功,返回的文件对象的方法可用于读取数据;对应的文件描述符具有O_RDONLY模式。如果出错,错误底层类型是*PathError。
关闭方法 :
1 func (f *File) Close() error
Close关闭文件f,使文件不能用于读写。它返回可能出现的错误。
案例演示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" "os" )func main () { file, err := os.Open("sample.txt" ) if err != nil { fmt.Println("open file error, err=" , err) } defer func () { err := file.Close() if err != nil { fmt.Println("close file error, err=" , err) } }() fmt.Printf("open file success, file=%v" , file) }
11.3 读文件
读取文件的内容 并显示在终端(带缓冲区 的方式),使用 os.Open
, file.Close
, bufio.NewReader()
,reader.ReadString
函数和方法.
1 func NewReader (rd io.Reader) *Reader
NewReader创建一个具有默认大小缓冲、从r读取的*Reader。
1 func (b *Reader) ReadString(delim byte ) (line string , err error )
ReadString读取直到第一次遇到delim字节,返回一个包含已读取的数据和delim字节的字符串。如果ReadString方法在读取到delim之前遇到了错误,它会返回在错误之前读取的数据以及该错误(一般是io.EOF)。当且仅当ReadString方法返回的切片不以delim结尾时,会返回一个非nil的错误。
例子 :
查看代码 sample.txt的内容
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "bufio" "fmt" "io" "os" )func main () { file, err := os.Open("sample.txt" ) if err != nil { fmt.Println("open file error, err=" , err) } defer func () { err := file.Close() if err != nil { fmt.Println("close file error, err=" , err) } }() reader := bufio.NewReader(file) for { str, err := reader.ReadString('\n' ) if err == io.EOF { break } else if err != nil { fmt.Println("read file error, err=" , err) } fmt.Print(str) } fmt.Println("end" ) }
输出:
读取文件的内容并显示在终端(使用 ioutil
一次将整个文件读入到内存中),这种方式适用于文件不大的情况 。相关方法和函数(ioutil.ReadFile
)
1 func ReadFile (filename string ) ([]byte , error )
ReadFile 从filename指定的文件中读取数据并返回文件的内容。成功的调用返回的err为nil而非EOF。因为本函数定义为读取整个文件,它不会将读取返回的EOF视为应报告的错误。
查看例子 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "io/ioutil" )func main () { content, err := ioutil.ReadFile("sample.txt" ) if err != nil { fmt.Println("Read file error, err=" , err) } fmt.Println(string (content)) }
11.4 写文件
1 func OpenFile (name string , flag int , perm FileMode) (file *File, err error )
OpenFile是一个更一般性的文件打开函数,大多数调用者都应用Open或Create代替本函数。它会使用指定的选项(如O_RDONLY等)、指定的模式(如0666等)打开指定名称的文件。如果操作成功,返回的文件对象可用于I/O。如果出错,错误底层类型是*PathError。
第二个参数:
1 2 3 4 5 6 7 8 9 10 const ( O_RDONLY int = syscall.O_RDONLY O_WRONLY int = syscall.O_WRONLY O_RDWR int = syscall.O_RDWR O_APPEND int = syscall.O_APPEND O_CREATE int = syscall.O_CREAT O_EXCL int = syscall.O_EXCL O_SYNC int = syscall.O_SYNC O_TRUNC int = syscall.O_TRUNC )
第三个参数:(只针对Unix系统)
1 2 3 4 5 type FileMode uint32 r->4 w->2 x->1
案例1 :
创建一个新文件,写入内容 5 句 “hello, Gardon”
1 func NewWriter (w io.Writer) *Writer
NewWriter创建一个具有默认大小缓冲、写入w的*Writer。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package mainimport ( "bufio" "fmt" "os" )func main () { filePath := "sample.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666 ) if err != nil { fmt.Println("open file err:" , err) } defer func () { if err := file.Close(); err != nil { fmt.Println("close file err:" , err) } }() writer := bufio.NewWriter(file) for i := 0 ; i < 10 ; i++ { _, err := writer.WriteString("hello world\n" ) if err != nil { fmt.Println("write err:" , err) } } err = writer.Flush() if err != nil { fmt.Println("flush err:" , err) } }
查看文件内容 1 2 3 4 5 6 7 8 9 10 11 hello world hello world hello world hello world hello world hello world hello world hello world hello world hello world
打开一个存在的文件(刚才创建的那个)中,将原来的内容覆盖成新的内容 5 句 “hello shopee”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "bufio" "fmt" "os" )func main () { filePath := "sample.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666 ) if err != nil { fmt.Println("open file err:" , err) } defer func () { if err := file.Close(); err != nil { fmt.Println("close file err:" , err) } }() writer := bufio.NewWriter(file) for i := 0 ; i < 5 ; i++ { _, err := writer.WriteString("hello shopee\n" ) if err != nil { fmt.Println("write err:" , err) } } err = writer.Flush() if err != nil { fmt.Println("flush err:" , err) } }
查看文件内容 1 2 3 4 5 6 hello shopee hello shopee hello shopee hello shopee hello shopee
打开一个存在的文件,在原来的内容追加内容 ‘become gopher’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "bufio" "fmt" "os" )func main () { filePath := "sample.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666 ) if err != nil { fmt.Println("open file err:" , err) } defer func () { if err := file.Close(); err != nil { fmt.Println("close file err:" , err) } }() writer := bufio.NewWriter(file) for i := 0 ; i < 5 ; i++ { _, err := writer.WriteString("become gopher\n" ) if err != nil { fmt.Println("write err:" , err) } } err = writer.Flush() if err != nil { fmt.Println("flush err:" , err) } }
查看文件内容 1 2 3 4 5 6 7 8 9 10 11 hello shopee hello shopee hello shopee hello shopee hello shopee become gopher become gopher become gopher become gopher become gopher
案例2 :
编程一个程序,将一个文件的内容,写入到另外一个文件。注:这两个文件已经存在了. 说明:使用 ioutil.ReadFile
/ ioutil.WriteFile
完成写文件的任务.
另一个函数已经在上面介绍过了。
1 func WriteFile (filename string , data []byte , perm os.FileMode) error
函数向filename指定的文件中写入数据。如果文件不存在将按给出的权限创建文件,否则在写入数据之前清空文件。
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport ( "fmt" "io/ioutil" )func main () { filePathRead := "sample.txt" filePathWrite := "sample-copy.txt" data, err := ioutil.ReadFile(filePathRead) if err != nil { fmt.Println("read file err:" , err) } err = ioutil.WriteFile(filePathWrite, data, 0644 ) if err != nil { fmt.Println("write file err:" , err) } }
案例3 :
判断文件是否存在
golang判断文件或者文件夹是否存在的方法是为使用os.Stat()
函数的返回错误值进行判断:
1 如果返回错误是nil
,说明文件或文件夹存在
2 如果返回的错误类型使用os.IsNotExist()
判断为true,说明文件/文件夹不存在
3 如果返回为其他类型,则不确定是否存在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport ( "fmt" "os" )func main () { _, err := os.Stat("sample.txt" ) if err == nil { fmt.Println("文件存在" ) } else if os.IsNotExist(err) { fmt.Println("文件不存在" ) } else { fmt.Println("不确定" ) } }
11.5 文件编程应用实例
拷贝文件
说明:将一张 图片/电影/mp3 拷贝到另外一个文件。
函数:
1 func Copy (dst Writer, src Reader) (written int64 , err error )
将src的数据拷贝到dst,直到在src上到达EOF或发生错误。返回拷贝的字节数和遇到的第一个错误。
对成功的调用,返回值err为nil而非EOF,因为Copy定义为从src读取直到EOF,它不会将读取到EOF视为应报告的错误。如果src实现了WriterTo接口,本函数会调用src.WriteTo(dst)进行拷贝;否则如果dst实现了ReaderFrom接口,本函数会调用dst.ReadFrom(src)进行拷贝。
查看代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package mainimport ( "bufio" "fmt" "io" "os" )func CopyFile (dst, src string ) (int64 , error ) { srcFile, err := os.Open(src) if err != nil { fmt.Printf("open file %s failed, err:%v\n" , src, err) return 0 , err } defer func () { if err := srcFile.Close(); err != nil { fmt.Printf("close file %s failed, err:%v\n" , src, err) } }() reader := bufio.NewReader(srcFile) dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE, 0666 ) if err != nil { fmt.Printf("open file %s failed, err:%v\n" , dst, err) return 0 , err } defer func () { if err := dstFile.Close(); err != nil { fmt.Printf("close file %s failed, err:%v\n" , dst, err) } }() writer := bufio.NewWriter(dstFile) return io.Copy(writer, reader) }func main () { srcFIle := "sample.jpg" dstFIle := "sample-copy.jpg" _, err := CopyFile(dstFIle, srcFIle) if err != nil { fmt.Printf("copy file %s failed, err:%v\n" , srcFIle, err) } fmt.Printf("copy file %s success\n" , dstFIle) }
11.6 命令行参数
我们希望能够获取到命令行输入的各种参数,该如何处理? 命令行参数
11.6.1 基本介绍
os.Args
是string的一个切片,用来存储所有命令行参数。
例子 :
请编写一段代码,可以获取命令行各个参数
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" "os" )func main () { fmt.Println("命令行参数有" , len (os.Args)) for i, arg := range os.Args { fmt.Printf("arg[%d]=%s\n" , i, arg) } }
查看参数例子和输出 1 2 3 4 5 6 guowei.gong@GJVXWHGXQ1 demo-command-args % go run main.go args1 sss bbb 命令行参数有 4 arg[0]=/var/folders/1h/3_fcjhlj7t30rbb68dnhqzx40000gp/T/go-build2035444824/b001/exe/main arg[1]=args1 arg[2]=sss arg[3]=bbb
arg[0] :这里显示的是 Go 编译器为了执行程序而临时生成的可执行文件的路径。当你使用 go run
命令时,Go 编译器首先编译程序为一个临时的二进制文件,然后立即执行这个二进制文件。这就是为什么 arg[0]
不直接是 main.go
,而是一个指向临时编译结果的路径。
11.6.2 flag包用来解析命令行参数
前面的方式是比较原生的方式,对解析参数不是特别的方便,特别是带有指定参数形式的命令行。
例如:mysql -u root -p 这种指定形式的,可以使用flag包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 package mainimport ( "flag" "fmt" )func main () { var ( username string password string hostname string port int ) flag.StringVar(&username, "u" , "" , "用户名默认为空" ) flag.StringVar(&password, "p" , "" , "密码默认为空" ) flag.StringVar(&hostname, "h" , "localhost" , "主机名,默认为localhost" ) flag.IntVar(&port, "port" , 3306 , "端口号默认为3306" ) flag.Parse() fmt.Printf("username: %s\npassword: %s\nhostname: %s\nport: %d\n" , username, password, hostname, port) }
查看参数例子和输出 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 guowei.gong@GJVXWHGXQ1 demo-command-args % go run main.go -u root -p 123456 username: root password: 123456 hostname: localhost port: 3306 guowei.gong@GJVXWHGXQ1 demo-command-args % go run main.go -help Usage of /var/folders/1h/3_fcjhlj7t30rbb68dnhqzx40000gp/T/go-build51329585/b001/exe/main: -h string 主机名,默认为localhost (default "localhost") -p string 密码默认为空 -port int 端口号默认为3306 (default 3306) -u string 用户名默认为空
11.7 json
http://www.json.cn 网站可以验证一个 json 格式的数据是否正确。尤其是在我们编写比较复杂的json 格式数据时,很有用。
11.7.1 json序列化
介绍
json 序列化是指,将有 key-value 结构的数据类型(比如结构体、map、切片)序列化成 json 字符串的操作。
func Marshal 1 func Marshal (v interface {}) ([]byte , error )
Marshal函数返回v的json编码。
Marshal函数会递归的处理值。如果一个值实现了Marshaler接口切非nil指针,会调用其MarshalJSON方法来生成json编码。nil指针异常并不是严格必需的,但会模拟与UnmarshalJSON的行为类似的必需的异常。
否则,Marshal函数使用下面的基于类型的默认编码格式:
布尔类型编码为json布尔类型。
浮点数、整数和Number类型的值编码为json数字类型。
字符串编码为json字符串。角括号"<“和”>“会转义为”\u003c"和"\u003e"以避免某些浏览器吧json输出错误理解为HTML。基于同样的原因,“&“转义为”\u0026”。
数组和切片类型的值编码为json数组,但[]byte编码为base64编码字符串,nil切片编码为null。
结构体的值编码为json对象。每一个导出字段变成该对象的一个成员,除非:
1 2 - 字段的标签是"-" - 字段是空值,而其标签指定了omitempty选项
空值是false、0、“”、nil指针、nil接口、长度为0的数组、切片、映射。对象默认键字符串是结构体的字段名,但可以在结构体字段的标签里指定。结构体标签值里的"json"键为键名,后跟可选的逗号和选项,举例如下:
1 2 3 4 5 6 7 8 Field int `json:"-"` Field int `json:"myName"` Field int `json:"myName,omitempty"` Field int `json:",omitempty"`
"string"选项标记一个字段在编码json时应编码为字符串。它只适用于字符串、浮点数、整数类型的字段。这个额外水平的编码选项有时候会用于和javascript程序交互:
1 Int64String int64 `json:",string"`
如果键名是只含有unicode字符、数字、美元符号、百分号、连字符、下划线和斜杠的非空字符串,将使用它代替字段名。
匿名的结构体字段一般序列化为他们内部的导出字段就好像位于外层结构体中一样。如果一个匿名结构体字段的标签给其提供了键名,则会使用键名代替字段名,而不视为匿名。
Go结构体字段的可视性规则用于供json决定那个字段应该序列化或反序列化时是经过修正了的。如果同一层次有多个(匿名)字段且该层次是最小嵌套的(嵌套层次则使用默认go规则),会应用如下额外规则:
1)json标签为"-"的匿名字段强行忽略,不作考虑;
2)json标签提供了键名的匿名字段,视为非匿名字段;
3)其余字段中如果只有一个匿名字段,则使用该字段;
4)其余字段中如果有多个匿名字段,但压平后不会出现冲突,所有匿名字段压平;
5)其余字段中如果有多个匿名字段,但压平后出现冲突,全部忽略,不产生错误。
对匿名结构体字段的管理是从go1.1开始的,在之前的版本,匿名字段会直接忽略掉。
映射类型的值编码为json对象。映射的键必须是字符串,对象的键直接使用映射的键。
指针类型的值编码为其指向的值(的json编码)。nil指针编码为null。
接口类型的值编码为接口内保持的具体类型的值(的json编码)。nil接口编码为null。
通道、复数、函数类型的值不能编码进json。尝试编码它们会导致Marshal函数返回UnsupportedTypeError。
Json不能表示循环的数据结构,将一个循环的结构提供给Marshal函数会导致无休止的循环。
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type ColorGroup struct { ID int Name string Colors []string } group := ColorGroup{ ID: 1 , Name: "Reds" , Colors: []string {"Crimson" , "Red" , "Ruby" , "Maroon" }, } b, err := json.Marshal(group)if err != nil { fmt.Println("error:" , err) } os.Stdout.Write(b)
Output:
1 {"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}
应用案例
这里我们介绍一下结构体、map 和切片的序列化,其它数据类型的序列化类似。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 package mainimport ( "encoding/json" "fmt" )type Monster struct { Name string Age int Birthday string Sal float64 Skill string }func testStruct () { monster := Monster{ Name: "牛魔王" , Age: 500 , Birthday: "2011-11-11" , Sal: 8000.0 , Skill: "牛魔拳" , } data, err := json.Marshal(&monster) if err != nil { fmt.Printf("序列号错误 err=%v\n" , err) } fmt.Printf("monster 序列化后=%v\n" , string (data)) }func testMap () { var a map [string ]interface {} a = make (map [string ]interface {}) a["name" ] = "红孩儿" a["age" ] = 30 a["address" ] = "洪崖洞" data, err := json.Marshal(a) if err != nil { fmt.Printf("序列化错误 err=%v\n" , err) } fmt.Printf("a map 序列化后=%v\n" , string (data)) }func testSlice () { var slice []map [string ]interface {} var m1 map [string ]interface {} m1 = make (map [string ]interface {}) m1["name" ] = "jack" m1["age" ] = "7" m1["address" ] = "北京" slice = append (slice, m1) var m2 map [string ]interface {} m2 = make (map [string ]interface {}) m2["name" ] = "tom" m2["age" ] = "20" m2["address" ] = [2 ]string {"墨西哥" , "夏威夷" } slice = append (slice, m2) data, err := json.Marshal(slice) if err != nil { fmt.Printf("序列化错误 err=%v\n" , err) } fmt.Printf("slice 序列化后=%v\n" , string (data)) }func testFloat64 () { var num1 float64 = 2345.67 data, err := json.Marshal(num1) if err != nil { fmt.Printf("序列化错误 err=%v\n" , err) } fmt.Printf("num1 序列化后=%v\n" , string (data)) }func main () { fmt.Println("| struct |----------------------" ) testStruct() fmt.Println("| map |----------------------" ) testMap() fmt.Println("| slice |----------------------" ) testSlice() fmt.Println("| 基本数据类型 |----------------------" ) testFloat64() }
查看输出 1 2 3 4 5 6 7 8 | struct |---------------------- monster 序列化后={"Name":"牛魔王","Age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"} | map |---------------------- a map 序列化后={"address":"洪崖洞","age":30,"name":"红孩儿"} | slice |---------------------- slice 序列化后=[{"address":"北京","age":"7","name":"jack"},{"address":["墨西哥","夏威夷"],"age":"20","name":"tom"}] | 基本数据类型 |---------------------- num1 序列化后=2345.67
注意事项 :
对于结构体的序列化,如果我们希望序列化后的 key 的名字,又我们自己重新制定,那么可以给 struct 指定一个 tag 标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "encoding/json" "fmt" )type Monster struct { Name string `json:"monster_name"` Age int `json:"monster_age"` Birthday string Sal float64 Skill string }func testStruct () { monster := Monster{ Name: "牛魔王" , Age: 500 , Birthday: "2011-11-11" , Sal: 8000.0 , Skill: "牛魔拳" , } data, err := json.Marshal(&monster) if err != nil { fmt.Printf("序列号错误 err=%v\n" , err) } fmt.Printf("monster 序列化后=%v\n" , string (data)) }func main () { testStruct() }
查看输出 1 monster 序列化后={"monster_name":"牛魔王","monster_age":500,"Birthday":"2011-11-11","Sal":8000,"Skill":"牛魔拳"}
11.7.2 json反序列化
基本介绍 :
json 反序列化是指,将 json 字符串反序列化成对应的数据类型(比如结构体、map、切片)的操作。
func Unmarshal 1 func Unmarshal (data []byte , v interface {}) error
Unmarshal函数解析json编码的数据并将结果存入v指向的值。
Unmarshal和Marshal做相反的操作,必要时申请映射、切片或指针,有如下的附加规则:
要将json数据解码写入一个指针,Unmarshal函数首先处理json数据是json字面值null的情况。此时,函数将指针设为nil;否则,函数将json数据解码写入指针指向的值;如果指针本身是nil,函数会先申请一个值并使指针指向它。
要将json数据解码写入一个结构体,函数会匹配输入对象的键和Marshal使用的键(结构体字段名或者它的标签指定的键名),优先选择精确的匹配,但也接受大小写不敏感的匹配。
要将json数据解码写入一个接口类型值,函数会将数据解码为如下类型写入接口:
1 2 3 4 5 6 Bool 对应JSON布尔类型float64 对应JSON数字类型string 对应JSON字符串类型 []interface {} 对应JSON数组map [string ]interface {} 对应JSON对象nil 对应JSON的null
如果一个JSON值不匹配给出的目标类型,或者如果一个json数字写入目标类型时溢出,Unmarshal函数会跳过该字段并尽量完成其余的解码操作。如果没有出现更加严重的错误,本函数会返回一个描述第一个此类错误的详细信息的UnmarshalTypeError。
JSON的null值解码为go的接口、指针、切片时会将它们设为nil,因为null在json里一般表示“不存在”。 解码json的null值到其他go类型时,不会造成任何改变,也不会产生错误。
当解码字符串时,不合法的utf-8或utf-16代理(字符)对不视为错误,而是将非法字符替换为unicode字符U+FFFD。
Example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var jsonBlob = []byte (`[ {"Name": "Platypus", "Order": "Monotremata"}, {"Name": "Quoll", "Order": "Dasyuromorphia"} ]` )type Animal struct { Name string Order string }var animals []Animal err := json.Unmarshal(jsonBlob, &animals)if err != nil { fmt.Println("error:" , err) } fmt.Printf("%+v" , animals)
Output:
1 [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
应用案例
这里我们介绍一下将 json 字符串反序列化成结构体、map 和切片.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package mainimport ( "encoding/json" "fmt" )type Monster struct { Name string Age int Birthday string Sal float64 Skill string }func unmarshalStruct () { str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}" var monster Monster err := json.Unmarshal([]byte (str), &monster) if err != nil { fmt.Printf("unmarshal err=%v\n" , err) } fmt.Printf("反序列化后 monster=%v monster.Name=%v \n" , monster, monster.Name) }func unmarshalMap () { str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}" var a map [string ]interface {} err := json.Unmarshal([]byte (str), &a) if err != nil { fmt.Printf("unmarshal err=%v\n" , err) } fmt.Printf("反序列化后 a=%v\n" , a) }func unmarshalSlice () { str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," + "{\"address\":[\"墨西哥\",\"夏威夷\"],\"age\":\"20\",\"name\":\"tom\"}]" var slice []map [string ]interface {} err := json.Unmarshal([]byte (str), &slice) if err != nil { fmt.Printf("unmarshal err=%v\n" , err) } fmt.Printf("反序列化后 slice=%v\n" , slice) }func main () { unmarshalStruct() unmarshalMap() unmarshalSlice() }
查看输出 1 2 3 反序列化后 monster={牛魔王 500 2011-11-11 8000 牛魔拳} monster.Name=牛魔王 反序列化后 a=map[address:洪崖洞 age:30 name:红孩儿] 反序列化后 slice=[map[address:北京 age:7 name:jack] map[address:[墨西哥 夏威夷] age:20 name:tom]]
注意事项
1) 在反序列化一个json 字符串时,要确保反序列化后的数据类型 和原来序列化前的数据类型 一致。
2) 如果 json 字符串是通过程序获取到的,则不需要再对 “转义处理"。
案例代码中是这么写的:
1 str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"
这是由于这边直接写字符串才需要加转义,实际获取的时候已经转义好了。
12 单元测试
传统方法的缺点分析
1) 不方便, 我们需要在 main 函数中去调用,这样就需要去修改 main 函数,如果现在项目正在运行,就可能去停止项目。
2) 不利于管理,因为当我们测试多个函数或者多个模块时,都需要写在 main 函数,不利于我们管理和清晰我们思路
这样我们就引出了单元测试。 testing 测试框架 可以很好解决问题。
基本介绍
Go 语言中自带有一个轻量级的测试框架 testing 和自带的 go test 命令来实现单元测试和性能测试,testing 框架和其他语言中的测试框架类似,可以基于这个框架写针对相应函数的测试用例,也可以基于该框架写相应的压力测试用例。通过单元测试,可以解决如下问题
① 确保每个函数是可运行,并且运行结果是正确的
② 确保写出来的代码性能是好的,
③ 单元测试能及时的发现程序设计或实现的逻辑错误,使问题及早暴露,便于问题的定位解决, 而性能测试的重点在于发现程序设计上的一些问题,让程序能够在高并发的情况下还能保持稳定。
快速入门
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package mainimport "fmt" func partition (arr []int , left, right int ) int { pivotVal := arr[left] for left < right { for left < right && pivotVal <= arr[right] { right-- } arr[left] = arr[right] for left < right && pivotVal >= arr[left] { left++ } arr[right] = arr[left] } arr[left] = pivotVal return left }func quicksort (arr []int , left, right int ) { if left < right { pivot := partition(arr, left, right) quicksort(arr, left, pivot-1 ) quicksort(arr, pivot+1 , right) } }func main () { arr := []int {15 , 22 , 55 , 34 , 25 , 16 , 7 , 58 , 79 , 10 } quicksort(arr, 0 , len (arr)-1 ) fmt.Println(arr) }
同目录下main_test.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "testing" )func TestQuickSort (t *testing.T) { arr := []int {15 , 22 , 55 , 34 , 25 , 16 , 7 , 58 , 79 , 10 } correctArr := []int {7 , 10 , 15 , 16 , 22 , 25 , 34 , 55 , 58 , 79 } quicksort(arr, 0 , len (arr)-1 ) fmt.Println(arr) for i := 0 ; i < len (arr); i++ { if arr[i] != correctArr[i] { t.Fatal("not correct" ) } } t.Logf("corrrct" ) }
运行命令:
1 2 3 4 5 6 7 guowei.gong@GJVXWHGXQ1 demo-test % go test -v === RUN TestQuickSort [7 10 15 16 22 25 34 55 58 79] main_test.go:19: corrrct --- PASS: TestQuickSort (0.00s) PASS ok learning/demo-test 0.259s
规则总结
测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go, cal不是固定的。
测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper。Test后面必须紧跟一个大写字母。
TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T
一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
运行测试用例指令
go test
[如果运行正确,无日志,错误时,会输出日志]
go test -v
[运行正确或是错误,都输出日志]
当出现错误时,可以使用 t.Fatalf
来格式化输出错误信息,并退出程序
t.Logf
方法可以输出相应的日志
测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].
PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
测试单个文件,一定要带上被测试的原文件,eg: go test -v cal_test.go cal.go
测试单个方法go test -v -test.run TestAddUpper
13 goroutine和channel
13.1 goroutine基本介绍
需求:要求统计 1-9000000000 的数字中,哪些是素数?
分析思路:
1) 传统的方法,就是使用一个循环,循环的判断各个数是不是素数。【很慢】
2) 使用并发或者并行的方式,将统计素数的任务分配给多个 goroutine 去完成,这时就会使用到goroutine.【速度提高】
main
函数的执行是在一个进程中开始的。当你运行一个 Go 程序时,操作系统为该程序创建一个新的进程,main
函数是这个进程的入口点。显然,如果没有goroutine,那么程序就只有一个单线程在执行。
Go 语言中的并发模型使得多个 Goroutine (M)可以被调度到较少的操作系统线程(N)上运行。
13.2 MPG模型
https://juejin.cn/post/6987360989150707720
go使用的是MPG模型,意思是通过一个全局的调度器来实现goroutine协程的调度,来达到通过分配平均使用CPU资源。
go的调度器有3个重要的结构,M(OS线程)、P(协程调度器),G(goroutine协程)
M(OS线程):是操作系统的线程,一个程序可以模拟出多个线程。
P(逻辑处理器or协程调度器):这个一个专门调度goroutine协程的逻辑处理器,或者称为协程调度器都可以。
G(goroutine协程):goroutine协程。
用户空间线程和内核空间线程之间的映射关系有:N:1、1:1和M:N
N:1,多个(N)用户线程始终在一个内核线程上跑,context上下文切换确实很快,但是无法真正的利用多核。
1:1,一个用户线程就只在一个内核线程上跑,这时可以利用多核,但是上下文switch很慢。
M:N,多个goroutine在多个内核线程上跑,这个看似可以集齐上面两者的优势,但是无疑增加了调度的难度。
而go使用的就是M:N这种映射关系。
实现原理
当go启动一个进程的时候,会默认创建一个线程,这个线程会有一个逻辑处理器,通过具体的逻辑处理器处理goroutine协程。如下图所示,我们假设当前线程为M2,逻辑处理器为P0,有4个协程G1、G2、G3、G4需要执行,当前正在执行G1,其他正在等待。
假如当G1有执行文件阻塞的操作,这个时候逻辑处理器P0会将G1分离处理,同时与线程M2分离,如下图:
这个时候原有的逻辑处理器P0下面还有很多协程需要执行,于是会生成一个新的线程M3来继续进行当前逻辑处理器P0的处理,此时顺序执行G2。而原有的线程M2和G1则等待文件阻塞操作的完成。
此时M3正常执行goroutine,过了一段时间,原有的G1阻塞操作完成,等待被继续执行,而此时G2也执行完成,G1会被重新分配回到逻辑处理器P0进行执行。
而这个时候,线程M2并不会立即销毁,而是等待被下次利用。这样一个简单的goroutine协程调度流程就完成了。下面来一张完整图:
但是这个只是针对系统文件的I/O操作的情况。如果是针对网络I/O的情况,稍微有一点不一样。
涉及网络I/O操作的时候,会使用网络轮询器来进行操作,对这个不太了解,有想深入了解的同学可以查阅相关资料。
但是原理是一样的,都是通过将阻塞的G放到其他线程处理,也放一张完整图:
至此,一个单一的MPG模型就完成了,但是如果实现上面的M:N模型呢?
那就是多线程操作了。上面的列子只是说明一个单一的线程在执行goroutie调度。go启动的时候默认是启动4个线程M,4个线程M都都是一个MPG。图示如下:
当我们要分配很多goroutine协程的时候,会被平均分配到各个线程M上,这样就实现了并行处理操作。
GOMAXPROCS 和默认线程数 :
GOMAXPROCS :这是一个环境变量,也可以在 Go 程序中通过 runtime.GOMAXPROCS
函数设置。它决定了有多少处理器 P 可以同时执行用户级代码。默认情况下,Go 运行时设置这个值为可用的 CPU 核心数。
操作系统线程 :Go 运行时会为每个 P 创建至少一个操作系统线程(M),但并不意味着所有线程都会在同一时间处于活动状态。如果 Goroutine 没有执行需要并行处理的任务,或者 Goroutine 被阻塞,多余的线程可能会处于休眠状态。
13.3 案例引出channel
需求 :现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到 map 中。最后显示出来。要求使用 goroutine 完成。
分析思路:
1) 使用 goroutine 来完成,效率高,但是会出现并发/并行安全问题.
2) 这里就提出了不同 goroutine 如何通信的问题
不同goroutine 之间如何通讯
1) 全局变量的互斥锁
2) 使用管道 channel 来解决
用全局变量的互斥锁来解决需求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "fmt" "sync" "time" )var ( myMap = make (map [int ]int ) lock sync.Mutex )func test (n int ) { res := 1 for i := 1 ; i <= n; i++ { res *= i } lock.Lock() myMap[n] = res lock.Unlock() }func main () { for i := 1 ; i <= 10 ; i++ { go test(i) } time.Sleep(time.Second * 10 ) lock.Lock() for i, v := range myMap { fmt.Printf("map[%d]=%d\n" , i, v) } lock.Unlock() }
查看输出 1 2 3 4 5 6 7 8 9 10 map[2]=2 map[6]=720 map[8]=40320 map[1]=1 map[5]=120 map[7]=5040 map[9]=362880 map[10]=3628800 map[3]=6 map[4]=24
使用channel解决需求(下面介绍)
为什么需要channel
1) 前面使用全局变量加锁同步来解决 goroutine 的通讯,但不完美
2) 主线程在等待所有 goroutine 全部完成的时间很难确定,我们这里设置 10 秒,仅仅是估算。
3) 如果主线程休眠时间长了,会加长等待时间,如果等待时间短了,可能还有 goroutine 处于工作状态,这时也会随主线程的退出而销毁
4) 通过全局变量加锁同步来实现通讯,也并不利用多个协程对全局变量的读写操作。
5) 上面种种分析都在呼唤一个新的通讯机制-channel
13.4 channel的基本介绍
channle 本质就是一个生产者消费者模式
数据是先进先出【FIFO : first in first out】
线程安全,多 goroutine 访问时,不需要加锁,就是说 channel 本身就是线程安全的
hannel 有类型的,一个 string 的 channel 只能存放 string 类型数据。
定义/声明 channel
eg:
1 2 3 4 var intChan chan int var mapChan chan map [int ]string var perChan chan Personvar perChan2 chan *Person
说明:
管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项
channel 中只能存放指定的数据类型
channle 的数据放满后,就不能再放入了【容量在初始化的时候固定了】
如果从 channel 取出数据后,可以继续放入
在没有使用协程的情况下,如果 channel 数据取完了,再取,就会报 dead lock
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package mainimport "fmt" func main () { var intChan chan int intChan = make (chan int , 3 ) fmt.Printf("intChan 的值=%v\nintChan 本身的地址=%p\n" , intChan, &intChan) intChan <- 1 num1 := 10 intChan <- num1 intChan <- 50 fmt.Println("----------------------------" ) fmt.Println("----------------------------" ) var num2 int = <-intChan fmt.Printf("num2=%v \n" , num2) fmt.Printf("channel len= %v cap=%v \n" , len (intChan), cap (intChan)) fmt.Println("----------------------------" ) num3 := <-intChan num4 := <-intChan fmt.Printf("num3=%v num4=%v num5=%v\n" , num3, num4 ) }
查看输出 1 2 3 4 5 6 7 8 intChan 的值=0x1400010a000 intChan 本身的地址=0x14000050020 ---------------------------- ---------------------------- num2=1 channel len= 2 cap=3 ---------------------------- num3=10 num4=50
13.5 channel的遍历与关闭
channel关闭
使用内置函数close 可以关闭 channel, 当 channel 关闭后,就不能再向 channel 写数据了,但是仍然可以从该 channel 读取数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { intChan := make (chan int , 3 ) intChan <- 1 intChan <- 2 close (intChan) fmt.Println(<-intChan) n1 := <-intChan fmt.Println(n1) }
channel遍历
channel 支持 for–range 的方式进行遍历,请注意两个细节
1) 在遍历时,如果 channel 没有关闭,则回出现 deadlock 的错误。
2) 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" func main () { intChan := make (chan int , 100 ) for i := 0 ; i < 100 ; i++ { intChan <- i } close (intChan) for v := range intChan { fmt.Println(v) } }
13.6 channe解决案例中的问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package mainimport ( "fmt" "sync" )var ( myMap = make (map [int ]int ) lock sync.Mutex )func test (n int , exitChan chan bool ) { res := 1 for i := 1 ; i <= n; i++ { res *= i } lock.Lock() myMap[n] = res lock.Unlock() exitChan <- true }func main () { exitChan := make (chan bool , 10 ) for i := 1 ; i <= 10 ; i++ { go test(i, exitChan) } for i := 1 ; i <= 10 ; i++ { <-exitChan } close (exitChan) for i, v := range myMap { fmt.Printf("map[%d]=%d\n" , i, v) } }
13.7 channel 使用细节和注意事项
channel 可以声明为只读,或者只写性质 【案例演示】
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var chan1 chan <- int chan1 = make (chan int , 3 ) chan1 <- 1 var chan2 <-chan int chan2 = make (chan int , 3 ) fmt.Println(chan1, chan2) }
1 定义函数参数列表的时候设置只读只写,这样来控制函数的访问权限,避免误操作
使用 select 可以解决从管道取数据的阻塞问题
在 select
中,如果有多个 case
同时满足,Go 运行时会随机选择一个执行,这就带来了非确定性,这种随机性使得使用 select
可以平衡多个通道的处理,避免因某个通道的频繁活动而导致其他通道饿死(即长时间无法处理)。default
语句提供了一个无任何通道准备好时的操作,这在避免阻塞中非常有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package mainimport ( "fmt" "time" )func main () { intChan := make (chan int , 10 ) for i := 0 ; i < 10 ; i++ { intChan <- i } stringChan := make (chan string , 5 ) for i := 0 ; i < 5 ; i++ { stringChan <- "hello" + fmt.Sprintf("%d" , i) } for { select { case v := <-intChan: fmt.Printf("从 intChan 读取的数据%d\n" , v) time.Sleep(time.Second) case v := <-stringChan: fmt.Printf("从 stringChan 读取的数据%s\n" , v) time.Sleep(time.Second) default : fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n" ) time.Sleep(time.Second) return } } }
goroutine 中使用 recover,解决协程中出现 panic,导致程序崩溃问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package mainimport ( "fmt" "time" )func sayHello () { for i := 0 ; i < 10 ; i++ { time.Sleep(time.Second) fmt.Println("hello,world" ) } }func test () { defer func () { if err := recover (); err != nil { fmt.Println("test() 发生错误" , err) } }() var myMap map [int ]string myMap[0 ] = "golang" }func main () { go sayHello() go test() for i := 0 ; i < 10 ; i++ { fmt.Println("main() ok=" , i) time.Sleep(time.Second) } }
14 反射
14.1 反射的基本介绍
基本介绍
1) 反射可以在运行时动态获取变量的各种信息 , 比如变量的类型(type),类别(kind)
2) 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段 、方法 )
3) 通过反射,可以修改变量的值,可以调用关联的方法。
4) 使用反射,需要 import (“reflect”)
反射的应用场景
1) 不知道接口调用哪个函数,根据传入的参数在运行时确定调用的具体接口,这种需要对函数或方法反射。例如一下这种桥接模式:
1 func bridge (funcPtr interface {}, args... interface {})
第一个参数funcPtr以接口的形式传入函数指针,函数参数args以可变参数的形式传入,bridge函数中可用反射来动态执行funcPtr函数。
2) 对结构体序列时,如果结构体有指定的tag,也会使用待反射生成对应的字符串。
1 2 3 4 5 6 7 type Monster struct { Name string `json:"monster_name"` Age int `json:"monster_age"` Birthday string Sal float64 Skill string }
14.2 反射的重要函数和概念
reflect.TypeOf(变量名)
,获取变量的类型,返回reflect.Type
类型
reflect.ValueOf(变量名)
,获取变量的值,返回reflect.Value
类型,reflect.Value
时一个结构体类型,可以通过reflect.Value
获取到关于变量的很多信息。
类型/操作
方法
说明
获取类型
Type()
返回变量的 reflect.Type
获取数值
Int()
, Float()
等
直接获取基础类型的数值
设置值
SetXXX()
系列
如果变量是可设置的,可以修改其值
访问结构体字段
Field(i int)
访问结构体的字段
调用方法
Method(i int)
获取并调用结构体的方法
获取长度
Len()
获取数组、切片、字符串的长度
访问元素
Index(i int)
访问数组、切片、字符串的指定位置元素
映射的键值对
MapKeys()
, MapIndex(key reflect.Value)
获取映射的所有键和对应的值
转换为接口类型
Interface()
将 reflect.Value
转换回 interface{}
类型,以便进行常规类型操作
变量、interface{}
和 reflect.Value
是可以相互转换的,这点在实际开发中,会经常使用到。示意图:
14.3 反射的快速入门
演示对(基本数据类型、interface{}、reflect.Value)进行反射的基本操作
演示对(结构体类型、interface{}、reflect.Value)进行反射的基本操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 package mainimport ( "fmt" "reflect" )func reflectTest01 (b interface {}) { rTyp := reflect.TypeOf(b) fmt.Println("rType=" , rTyp) rVal := reflect.ValueOf(b) n2 := 2 + rVal.Int() fmt.Println("n2=" , n2) fmt.Printf("rVal=%v rVal type=%T\n" , rVal, rVal) iV := rVal.Interface() num2 := iV.(int ) fmt.Println("num2=" , num2) }type Student struct { Name string Age int }func reflectTest02 (b interface {}) { rTyp := reflect.TypeOf(b) fmt.Println("rType=" , rTyp) rVal := reflect.ValueOf(b) iV := rVal.Interface() fmt.Printf("iv=%v iv type=%T \n" , iV, iV) stu, ok := iV.(Student) if ok { fmt.Printf("stu.Name=%v\n" , stu.Name) } }func main () { var num = 100 reflectTest01(num) fmt.Println("----------------------------" ) stu := Student{Name: "tom" , Age: 20 } reflectTest02(stu) }
查看输出 1 2 3 4 5 6 7 8 rType= int n2= 102 rVal=100 rVal type=reflect.Value num2= 100 ---------------------------- rType= main.Student iv={tom 20} iv type=main.Student stu.Name=tom
14.4 反正的注意事项和使用细节
reflect.Value.Kind
,获取变量的类别,返回的是一个常量
Kind代表Type类型值表示的具体分类。零值表示非法分类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const ( Invalid Kind = iota Bool Int Int8 Int16 Int32 Int64 Uint Uint8 Uint16 Uint32 Uint64 Uintptr Float32 Float64 Complex64 Complex128 Array Chan Func Interface Map Ptr Slice String Struct UnsafePointer )
Type 和 Kind 的区别
Type 是类型, Kind 是类别, Type 和 Kind 可能是相同的 ,也可能是不同的 .
比如: var num int = 10, num 的 Type 是 int , Kind 也是 int
比如: var stu Student, stu 的 Type 是 pkg1.Student , Kind 是 struct
使用反射的方式来获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value(x).Int()
,而不能使用其他的,否则报panic。
通过反射的来修改变量, 注意当使用 SetXxx
方法来设置需要通过对应的指针类型来完成, 这样才能改变传入的变量的值, 同时需要使用到 reflect.Value.Elem()
方法
1 func (v Value) Elem() Value
Elem返回v持有的接口保管的值的Value封装,或者v持有的指针指向的值的Value封装。如果v的Kind不是Interface或Ptr会panic;如果v持有的值为nil,会返回Value零值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "reflect" )func test (b interface {}) { val := reflect.ValueOf(b) fmt.Printf("val type: %T\n" , val) val.Elem().SetInt(110 ) fmt.Printf("val: %T\n" , val) }func main () { n := 100 test(&n) fmt.Println("n:" , n) }
查看输出 1 2 3 val type: reflect.Value val: reflect.Value n: 110
14.5 反射的最佳实践
使用反射来遍历结构体的字段 ,调用结构体的方法 ,并获取结构体标签 的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 package mainimport ( "fmt" "reflect" )type Monster struct { Name string `json:"name"` Age int `json:"monster_age"` Score float32 `json:"成绩"` Sex string }func (s Monster) GetSum(n1, n2 int ) int { return n1 + n2 }func (s Monster) Set(name string , age int , score float32 , sex string ) { s.Name = name s.Age = age s.Sex = sex }func (s Monster) Print() { fmt.Println("---start~ " ) fmt.Println(s) fmt.Println("---end~ " ) }func TestStruct (a interface {}) { typ := reflect.TypeOf(a) val := reflect.ValueOf(a) kd := val.Kind() if kd != reflect.Struct { fmt.Println("expect struct" ) return } num := val.NumField() fmt.Printf("struct has %d fields\n" , num) for i := 0 ; i < num; i++ { fmt.Printf("Field %d: 值为=%v\n" , i, val.Field(i)) tagVal := typ.Field(i).Tag.Get("json" ) if tagVal != "" { fmt.Printf("Field %d: tag 为=%v\n" , i, tagVal) } } numOfMethod := val.NumMethod() fmt.Printf("struct has %d methods\n" , numOfMethod) val.Method(1 ).Call(nil ) var params []reflect.Value params = append (params, reflect.ValueOf(10 )) params = append (params, reflect.ValueOf(40 )) res := val.Method(0 ).Call(params) fmt.Println("res=" , res[0 ].Int()) }func main () { var a Monster = Monster{ Name: "黄鼠狼精" , Age: 400 , Score: 30.8 , } TestStruct(a) }
查看输出 1 2 3 4 5 6 7 8 9 10 11 12 13 struct has 4 fields Field 0: 值为=黄鼠狼精 Field 0: tag 为=name Field 1: 值为=400 Field 1: tag 为=monster_age Field 2: 值为=30.8 Field 2: tag 为=成绩 Field 3: 值为= struct has 3 methods ---start~ {黄鼠狼精 400 30.8 } ---end~ res= 50
15 TCP编程
Golang 的主要设计目标之一就是面向大规模后端服务程序,网络通信这块是服务端 程序必不可少也是至关重要的一部分。
网络编程 有两种:
1) TCP socket 编程,是网络编程的主流。之所以叫 Tcp socket 编程,是因为底层是基于 Tcp/ip 协议的. 比如: QQ 聊天 [示意图]
2) b/s 结构的 http 编程,我们使用浏览器去访问服务器时,使用的就是 http 协议,而 http 底层依旧是用 tcp socket 实现的。[示意图] 比如: 京东商城 【这属于 go web 开发范畴 】
tcp socket 编程的客户端和服务器端 :
16 context
go语言的context
16.1 基本介绍
在 Go 语言中,Context 是一个非常重要的概念,它用于在不同的 goroutine 之间传递请求域的相关数据,并且可以用来控制 goroutine 的生命周期和取消操作。
1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key any) any }
Deadline() 方法用于获取 Context 的截止时间,
Done() 方法用于返回一个只读的 channel,用于通知当前 Context 是否已经被取消,
Err() 方法用于获取 Context 取消的原因,
Value() 方法用于获取 Context 中保存的键值对数据。
看一些官方包的源码的时候,很多函数的第一个参数都是 ctx context.Context
Context是干什么的
我们都知道 go的协程写起来是非常轻松的
并且在协程里面还能继续开协程
颇有一种一生二,二生三,三生万物的感觉了
这么多协程,协程与协程之间的通信如何解决呢
这个协程我不想要了,我如何关闭它,我们之前没有讲过
那么context来了,就是解决如何关闭协程这个问题的
除了关闭协程,他还能用于传输数据,做到协程与协程之间的桥梁,所以我们叫他 上下文
16.2 数据传递
Context 中保存的键值对数据是线程安全的,因为是只读的,它们可能会在多个 goroutine 中同时访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport ( "context" "fmt" )func main () { ctx := context.Background() ctx = context.WithValue(ctx, "name" , "guowei.gong" ) GetUserName(ctx) }func GetUserName (ctx context.Context) { fmt.Println(ctx.Value("name" )) }
慎用 context.WithValue
:虽然 context.WithValue
可以用来在 Context
中存储键值对,但它并不适合作为常规的存储方式,因为它主要用于携带与请求生命周期相关的数据。过度使用 context.WithValue
可能会导致代码难以理解和维护。一般建议用于少量的、与请求强相关的元数据传递。(元数据是关于数据本身的描述性信息,能够帮助我们更好地管理、理解、搜索和交换数据。)
16.3 取消协程 WithCancel
sync.WaitGroup 以下是 sync.WaitGroup
的主要使用步骤和相关方法的说明:
声明和初始化 :
1 var wait = sync.WaitGroup{}
这行代码声明并初始化了一个 sync.WaitGroup
实例,命名为 wait
。你可以使用 wait
来跟踪和等待多个 goroutine 的完成。
添加等待计数 :
Add(n int)
方法用于设置需要等待的 goroutine 数量。在你启动每个 goroutine 之前,调用 Add(1)
增加等待计数。n
表示需要等待的任务数量。
启动 goroutine 并执行任务 :
1 2 3 4 go func () { defer wait.Done() }()
每个 goroutine 在启动时需要在其执行的代码块中使用 defer wait.Done()
,以确保该 goroutine 完成后调用 Done()
方法来减少等待计数。
Done()
方法减少一个等待计数,表示有一个 goroutine 已经完成。
等待所有任务完成 :
Wait()
方法会阻塞调用它的 goroutine(通常是主 goroutine),直到等待计数变为 0,也就是所有注册的 goroutine 都已完成为止。
一个很常见的案例,我有一个获取ip的协程,但是这是一个耗时操作,用户随时可能会取消
如果用户取消了,那么之前那个获取协程的函数就要停止
怎么办呢?
原例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "fmt" "sync" "time" )var wait = sync.WaitGroup{}func main () { t1 := time.Now() wait.Add(1 ) go func () { defer wait.Done() ip, err := GetIp() fmt.Println(ip, err) }() wait.Wait() fmt.Println("执行完成" , time.Since(t1)) }func GetIp () (ip string , err error ) { time.Sleep(3 * time.Second) ip = "192.168.200.1" return }
增加取消后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "context" "fmt" "sync" "time" )var wait = sync.WaitGroup{}func main () { t1 := time.Now() wait.Add(2 ) ctx, cancel := context.WithCancel(context.Background()) go func () { ip, err := GetIp(ctx) fmt.Println(ip, err) }() go func () { time.Sleep(2 * time.Second) cancel() wait.Done() }() wait.Wait() fmt.Println("执行完成" , time.Since(t1)) }func GetIp (ctx context.Context) (ip string , err error ) { go func () { select { case <-ctx.Done(): fmt.Println("取消" , ctx.Err().Error()) err = ctx.Err() wait.Done() return } }() time.Sleep(3 * time.Second) ip = "192.168.200.1" wait.Done() return }
16.4 截止时间WithDeadline
除了使用 WithCancel() 方法进行取消操作之外,Context 还可以被用来设置截止时间,以便在超时的情况下取消请求
还是上面那个案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "context" "fmt" "time" )func main () { ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(5 *time.Second)) go GetIp(ctx) time.Sleep(5 * time.Second) time.Sleep(1 * time.Second) }func GetIp (ctx context.Context) { fmt.Println("获取ip中" ) select { case <-ctx.Done(): fmt.Println("请求超时或被取消" , ctx.Err()) } }
16.5 超时时间WithTimeout
用法大差不差,也是可以手动取消的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "context" "fmt" "time" )func main () { ctx, _ := context.WithTimeout(context.Background(), 5 *time.Second) go GetIp(ctx) time.Sleep(5 * time.Second) time.Sleep(1 * time.Second) }func GetIp (ctx context.Context) { fmt.Println("获取ip中" ) select { case <-ctx.Done(): fmt.Println("请求超时或被取消" , ctx.Err()) } }
和WithDeadline的主要区别
时间表示 :
WithTimeout
使用的是相对时间,即从调用时刻开始计时的持续时间(如“5秒后”)。
WithDeadline
使用的是绝对时间,即特定的截止时间点(如“2024年8月13日下午3点”)。
灵活性 :
WithTimeout
更适合那些从当前时刻开始的定时操作。
WithDeadline
更适合那些需要在某个具体时间点之前完成的操作。
内部实现 :
WithTimeout
实际上是基于 WithDeadline
实现的。WithTimeout(parent, timeout)
实际上等价于 WithDeadline(parent, time.Now().Add(timeout))