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动态语言的快速开发。

  1. 从C语言中继承了很多理念,包括表达式语法,控制结构,基础数据类型调用参数传值,指针等等,也保留了和C语言一样的编译执行方式及弱化的指针。
  2. 引入包的概念,用于组织程序结构,Go语言的一个文件都要归属于一个包,而不能单独存在。
  3. 垃圾回收机制,内存自动回收,不需开发人员管理
  4. 天然并发
    (1) 从语言层面支持并发,实现简单
    (2) goroutine,轻量级线程,可实现大并发处理,高效利用多核
    (3) 基于CPS并发模型(Communicating Sequential Processes)实现
  5. 吸收了管道通信机制,形成 Go 语言特有的管道 channel 通过管道 channel,可以实现不同的 goroute之间的相互通信。
  6. 函数可以返回多个值。
  7. 新的创新:切片 slice、延时执行 defer 等
什么是静态语言,什么是动态语言

静态语言(Static languages)和动态语言(Dynamic languages)是两种程序设计语言的类型,主要区别在于它们如何处理变量的类型和执行时间。

静态语言

静态语言要求在编译时确定所有变量的类型,即在程序运行之前。这种语言的优点是能够在编译阶段发现类型错误,从而提高程序的稳定性和效率。由于编译器可以优化生成的代码,所以通常静态语言的执行速度比动态语言更快。典型的静态语言包括C、C++、Java和Go。

动态语言

动态语言允许在运行时动态地更改变量的类型,不需要在代码中显式声明类型。这增加了编程的灵活性和简便性,使得编程更快速、更容易实现复杂功能。然而,这也可能导致运行时错误,因为错误只有在实际运行相应代码时才会被发现。动态语言的例子包括Python、Ruby和JavaScript。

总的来说,选择使用静态语言还是动态语言取决于项目的需求、团队的偏好以及特定情况下的性能要求。静态语言适用于需要高性能和稳定性的大型项目,而动态语言则适合快速开发和原型设计。




1 配置Go开发环境

安装配置SDK
SDK 的全称(Software Development Kit软件开发工具包)
SDK是提供给开发人员使用的,其中包含了对应开发语言的工具包

参考:VScode下配置Go语言开发环境【2023最新】_vscode go-CSDN博客

平台:window10
ide:vscode
go版本:1.22.4

1.1 下载并安装go

Go下载 - Go语言中文网 - Golang中文社区 (studygolang.com)

  1. 选择下载对应的版本,如下:
文件名 类型 操作系统(OS) 架构(Arch) 大小 SHA256 Checksum
…… …… …… …… …… ……
go1.22.4.windows-amd64.msi Installer Windows x86-64 60MB 3c21105d7b584759b6e266383b777caf6e87142d304a10b539dbc66ab482bb5f
…… …… …… …… …… ……
  1. 运行刚才下载的按照程序。(我自定义安装在了D:\go\1.22.4

  2. 安装完成后,通过查看安装版本来检查安装情况,使用命令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


配置环境变量

  1. 添加两个系统变量

变量名写 GOROOT
变量值写 Go 语言安装目录

变量名写 GOPATH
变量值写你自定义的目录

GOPATH 就是你以后 go 项目存放的路径,即工作目录。

  1. 把用户变量里的 GOPATH 也换成自定义的目录:

  1. 编辑用户变量里的 Path:
    原本是 %USERPROFILE%\go\bin,改成 %GOPATH%\bin

  1. 在 GOPATH 对应的目录下(我的是 D:\go\go_path_1.22.4)新建三个文件夹:bin、pkg、src:

src:这个目录用于存放源代码(.go 文件)。按照 Go 的约定,所有的 Go 项目和库都应该放在这个目录下。这是因为 Go 的包导入机制依赖于这种目录结构来查找和引用其他项目或第三方库。src 目录下存放项目代码是一种早期的约定,目的是为了统一代码存放的位置和方式,便于工具链的管理和操作。但随着模块的引入,这一约定的必要性已经降低。

pkg:这个目录用来存放编译后的包文件(.a 文件),以便可以被其他项目引用,而不需要重新编译。

bin:编译后的可执行文件(二进制文件)会被放在这里。

  1. 一般的话可以在系统变量Path中看到 Go 的路径已经配置好了,我们不需要动,这是 Go 在安装时自动配置的。【不用动,之前安装程序自动配好的】之前用 go version 查看版本号也是因为这里的环境变量,如果以后出现命令未找到等问题,可以回到这里检查。

但是这里我把D:\go\1.22.4\bin修改成了%GOROOT%\bin

  1. 输入 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相关扩展

  1. 安装 Go 扩展【名字就叫Go】

  2. 更新 Go 工具:

    1. ctrl + shift + P 搜索 Go: Install/Update Tools
    2. 全部选择好后,点确定后开始安装
      超时问题:vscode 安装go环境无法安装gopls等插件,响应超时、失去连接等问题的简单解决方案_golang gopls无法连接到-CSDN博客
    3. vscode 底部的输出端可以看到安装情况
    4. 可以看到有一些 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 main

import "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 编译运行

  1. 通过 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>
  1. 通过 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 转义序列的例子:

  1. \xhh

    • 用法:表示一个由两位十六进制数 hh 指定的字符。
    • 示例代码:表示一个 ASCII 的 “A”,十六进制为 41。
      1
      2
      3
      4
      5
      package main
      import "fmt"
      func main() {
      fmt.Println("\x41") // 输出 A
      }
  2. \uhhhh

    • 用法:表示一个 Unicode 码点为 hhhh 的字符,其中 hhhh 是四位十六进制数。
    • 示例代码:表示一个中文字符 “中”,其 Unicode 码点为 4E2D。
      1
      2
      3
      4
      5
      6
      7
      package main

      import "fmt"

      func main() {
      fmt.Println("\u4E2D") // 输出 中
      }
  3. \Uhhhhhhhh

    • 用法:表示一个 Unicode 码点为 hhhhhhhh 的字符,其中 hhhhhhhh 是八位十六进制数。
    • 示例代码:表示一个表情符号 “😊”,其 Unicode 码点为 1F60A。
      1
      2
      3
      4
      5
      6
      7
      package main

      import "fmt"

      func main() {
      fmt.Println("\U0001F60A") // 输出 😊
      }




2.3 注释

  1. 行注释
1
// 行注释内容
  1. 块注释(多行注释)
1
2
3
/*
块注释内容
*/

注意块注释不能嵌套使用





2.4 规范的代码风格

注释

  • Go 官方推荐使用行注释来注释整个方法和语句。

缩进和空白

  • VScode:使用一次 tab 操作,实现缩进,默认整体向右边移动,时候用 shift+tab 整体向左移

  • 使用命令进行缩进

    1
    gofmt main.go
    1
    gofmt -w main.go

    前者只是显示缩进后的结果,后者回写回。

  • 运算符两边习惯性各加一个空格。比如:2 + 4 * 5

  • Go 设计者思想:一个问题尽量只有一个解决方法
    所以统一了风格:
    这种可以:

    1
    2
    3
    4
    5
    6
    7
    package main

    import "fmt"

    func main() {
    fmt.Println("hello world")
    }

    这种不行

    1
    2
    3
    4
    5
    6
    7
    8
    package main

    import "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:使用前缀 0b0B 表示二进制字面量。
  • C/C++:从 C++14 开始,使用前缀 0b0B 表示二进制字面量。在 C 中不支持二进制字面量。

Go 示例

1
2
var bin = 0b1101
fmt.Printf("%d\n", bin) // 输出 13

C++ 示例

1
2
3
4
5
6
#include <iostream>
int main() {
int bin = 0b1101;
std::cout << bin << std::endl; // 输出 13
return 0;
}

八进制

  • Go:使用前缀 0 表示八进制字面量。
  • C/C++:使用前缀 0 表示八进制字面量。

Go 示例

1
2
var oct = 015
fmt.Printf("%d\n", oct) // 输出 13

C/C++ 示例

1
2
3
4
5
6
#include <iostream>
int main() {
int oct = 015;
std::cout << oct << std::endl; // 输出 13
return 0;
}

十进制:

  • Go:直接使用数字表示十进制字面量。
  • C/C++:直接使用数字表示十进制字面量。

Go 示例

1
2
var dec = 13
fmt.Printf("%d\n", dec) // 输出 13

C/C++ 示例

1
2
3
4
5
6
#include <iostream>
int main() {
int dec = 13;
std::cout << dec << std::endl; // 输出 13
return 0;
}

十六进制:

  • Go:使用前缀 0x0X 表示十六进制字面量。
  • C/C++:使用前缀 0x0X 表示十六进制字面量。

Go 示例

1
2
var hex = 0xD
fmt.Printf("%d\n", hex) // 输出 13

C/C++ 示例

1
2
3
4
5
6
#include <iostream>
int main() {
int hex = 0xD;
std::cout << hex << std::endl; // 输出 13
return 0;
}

Go 特有的表示方式:

Go 还支持一些 C/C++ 中不常见或不支持的特性,例如带有下划线分隔符的数字字面量,以提高可读性。这在 Go 中是一种很实用的特性,可以用于所有进制的数字字面量。

Go 示例

1
2
var largeNumber = 1_000_000
fmt.Printf("%d\n", largeNumber) // 输出 1000000




2.7 代码换行

在Go语言中,代码的换行处理和一些其他语言(如Python)有所不同。Go使用一个称为自动分号插入的机制来处理代码的换行,这是在Go的语言规范中明确定义的。这种机制影响了代码的编写风格,以及在某些情况下需要特别注意的换行规则。


自动分号插入规则

Go编译器会在特定情况下在源代码的行尾自动插入分号(;),这是Go程序的语句终止符。根据Go语言规范,如果某一行的最后一个词法单元(token)是以下之一,编译器将在该行末自动插入分号:

  • 一个标识符(如变量名)
  • 一个整数、浮点数、虚数、字符或字符串字面量
  • 关键字 breakcontinuefallthroughreturn
  • 运算符和分隔符 ++--)]

示例与影响

这意味着,当你在写Go代码时,通常不需要在语句末尾添加分号,编译器会自动处理。然而,这种机制也可能导致一些不直观的问题,特别是在你想要在多行上分布一个表达式或语句时。

例如,以下代码会导致编译错误:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var s = "hello"
+ " world"
fmt.Println(s)
}

上述代码中,"hello" 后面的换行导致编译器在该行自动插入分号,使得 + " world" 成为一个新的、独立的(且无效的)语句。

为了正确编写,你应该这样做:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var s = "hello" +
" world"
fmt.Println(s)
}

在这个修正后的例子中,+ 出现在第一行末尾,避免了自动插入分号,允许表达式跨多行。


最佳实践

  • 保持操作符在行末:在多行表达式中,把操作符放在行末可以防止自动分号插入的问题。
  • 使用括号:在复杂表达式中使用括号不仅可以提高代码的可读性,也有助于避免因自动分号插入引起的错误。

通过理解和适应Go的这种换行处理,你可以更加有效地编写Go代码,并避免一些常见的陷阱。





3 变量

3.1 变量的声明

  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

  1. 类型推断:使用var关键字时,如果初始化了变量,可以省略类型,让编译器自动推断类型。
1
2
3
var a = 10       // int
var b = "hello" // string
var c = true // bool

  1. 短变量声明:在函数内部,可以使用:=语法进行更简洁的变量声明和初始化。这种方式不能在函数外部使用。
1
2
3
a := 10
b := "hello"
c := true

  1. 使用new关键字:通过new关键字,可以创建一个指向类型零值的指针。
1
p := new(int)   // *int类型,指向一个值为0的int

  1. 使用make关键字:用于内建的引用类型(如切片、映射和通道)的内存分配。make只适用于这三种类型。
1
2
3
s := make([]int, 10)  // 创建一个长度为10的int切片
m := make(map[string]int) // 创建一个string到int的映射
ch := make(chan int) // 创建一个int类型的通道

每种方法有其适用场景,选择合适的声明方式可以让代码更清晰、更符合Go语言的习惯。





3.2 多变量声明

在Go语言中,可以同时声明多个变量,这样做可以使代码更简洁且易于维护。以下是几种常见的多变量声明方式:


  1. 使用var关键字

你可以使用var关键字来同时声明多个变量。这种方式可以用于全局变量的声明或者在函数内部。

  • 声明不初始化(这些变量会被初始化为各自类型的零值):

    1
    2
    3
    var a, b, c int
    var d, e string
    var f, g bool
  • 声明并初始化(可以显式指定类型,也可以让编译器通过初始化的值推断类型):

    1
    2
    var h, i int = 1, 2
    var j, k = "hello", "world"

  1. 使用短变量声明

在函数内部,你可以使用短变量声明:=来同时声明并初始化多个变量。这种方法更加简洁,常用于局部变量的声明。

1
2
x, y := 100, 200
name, age := "Alice", 30

ps:

1
2
3
i := 10 
j := 20
i, j = j, i // 这样一句话就可以交换i和j的值

执行过程可以分解为以下步骤:

1) 右侧求值:首先,表达式右侧的值被求值。这意味着 ji 的当前值被取出并临时存储(在这个过程中,j 的值是 20i 的值是 10)。
2) 元组赋值:赋值操作从左到右进行。根据右侧的求值结果,j 的值(20)被赋给 ii 的值(10)被赋给 j
3) 结果i 的值变成了 20,而 j 的值变成了 10

值的注意的是:= 要求至少有一个新变量被声明,所以会有如下情况:

1
2
3
a := 10
//a := 20 // 编译错误
a, b := 20, 10 // 编译正确

  1. 使用new关键字

虽然new关键字通常用于单个变量的声明,但你可以连续使用多次new来声明多个指针变量。

1
2
p1, p2 := new(int), new(float64)
*p1, *p2 = 10, 3.14

  1. 使用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"
)

这里,n3n4被推断为int类型,而name2被推断为string类型。这些变量可以是全局变量或局部变量,具体取决于声明它们的代码块的位置。

优点

使用分组声明变量的好处包括:

  • 可读性提升:将相关变量分组可以提高代码的可读性,特别是在声明多个全局变量时。
  • 组织性强:帮助程序员在逻辑上组织代码,尤其是在处理多个变量时,可以很容易地查看哪些变量是一组的。
  • 简洁性:避免重复使用多个var关键字,代码看起来更简洁。

分组变量声明是Go语言中一种风格上的选择,广泛应用于各种Go程序中,有助于维护大型项目的清晰度和结构性。





3.4 数据类型的基本介绍

每一种数据都定义了明确的数据类型,在内存中分配了不同大小的内存空间。


如何在程序查看某个变量的字节大小和数据类型(使用较多)

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"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 整形的类型

  1. 有符号整型(Signed Integers)

有符号整型可以表示正数、负数以及零。Go语言提供了以下几种有符号整型:

  • int8:8位有符号整型,范围从-128到127。
  • int16:16位有符号整型,范围从-32768到32767。
  • int32:32位有符号整型,范围从-2147483648到2147483647。在Go中,int32rune类型相同,常用于表示一个Unicode码点。
  • int64:64位有符号整型,范围从-9223372036854775808到9223372036854775807。

此外,还有一个特殊的有符号整型:

  • int:其大小没有明确的位数,取决于执行程序的操作系统平台。通常在32位系统上是32位,在64位系统上是64位。这是使用最广泛的整型,因为它在不同平台间提供了良好的兼容性。

  1. 无符号整型(Unsigned Integers)

无符号整型只能表示非负数(包括零)。Go语言提供了以下几种无符号整型:

  • uint8:8位无符号整型,范围从0到255。
  • uint16:16位无符号整型,范围从0到65535。
  • uint32:32位无符号整型,范围从0到4294967295。
  • uint64:64位无符号整型,范围从0到18446744073709551615。

和有符号整型一样,还有一个特殊的无符号整型:

  • uint:其大小同样取决于执行程序的操作系统平台。通常在32位系统上是32位,在64位系统上是64位。它通常用于需要非负整数,且大小可能较大的情况。

  1. 特殊的整型类型

除了常规的有符号和无符号整型之外,Go语言还提供了几种特殊的整型类型:

  • uintptr:一个无符号整型,足以存储指针的位宽。这主要用于底层编程,如与C语言库交互或其他需要直接处理内存地址的场合。
  • byte:实际上是uint8的别名,常用于处理原始数据,如文件流或网络数据包。

使用注意事项

选择正确的整型类型对于优化程序性能、内存使用和跨平台兼容性非常重要。通常,如果没有特定的大小需求,使用intuint是最方便的,因为它们在不同平台上自动适应合适的大小。对于特定的应用,如处理大文件或需要精确控制数据结构大小的系统编程,选择固定大小的整型(如int64uint32)可能更合适。

在编写程序时,还应注意整型溢出和隐式类型转换的问题,这些都可能导致意外的行为或错误。





3.5.2 整形的使用细节

  • int uint 的大小和系统有关。

  • Golang的整形默认声明为int型【字面量默认是int】

  • Golang 程序中整型变量在使用时,遵守保小不保大的原则,即: 在保证程序正确运行下,尽量使用占用空间小的数据类型。【如:年龄】

1
var age byte = 90




3.6 浮点型

3.6.1 浮点型的类型

在Go语言中,浮点型数据用于表示有小数部分的数值。Go提供了两种基本的浮点类型,它们用于不同精度的数学计算:float32float64

  1. float32

float32 是一种单精度浮点类型,提供大约6-7位十进制精度。它在内存中占用32位(4字节),并遵循IEEE 754标准。由于其较小的大小和较快的处理速度,在处理大量浮点运算且对精度要求不是极高的场景下,使用float32可以节省内存并提高性能。然而,它在进行复杂的数学运算或要求较高精度的应用中可能会引入较大的误差。

  1. float64

float64 是一种双精度浮点类型,提供大约15-16位十进制精度。它在内存中占用64位(8字节),同样遵循IEEE 754标准。float64 是Go中使用最广泛的浮点类型,因为它提供了较高的精度,非常适合需要高精度计算的应用,如科学计算、金融分析等。除非有特别的性能或内存使用考虑,通常推荐使用float64来避免精度问题。


使用浮点数的注意事项

  1. 精度问题:由于浮点数的表示方式,某些值不能精确表示,例如大多数的小数。这可能导致精度损失,尤其是在进行一系列计算时累积的错误。

  2. 算术运算:浮点数在进行加减乘除等基本算术运算时,可能会引入额外的误差,这一点在使用float32时尤为明显。

  3. 比较运算:直接比较两个浮点数是否相等通常是不可靠的,因为可能存在微小的差异。通常的做法是检查两个数的差的绝对值是否小于一个非常小的数(称为epsilon)。

  4. 零值:浮点数的零值表示为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 main

import (
"fmt"
"math"
)

func main() {
// 声明并初始化float32和float64变量
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)

// 使用math包中的函数
fmt.Printf("Sin of num2: %f\n", math.Sin(num2))
}

总结

在Go中,选择float32float64取决于应用的性能要求、内存效率以及对计算精度的需求。在大多数情况下,float64由于其更高的精度而被推荐用于通用编程。对于特定的应用场景,如图形处理或大规模浮点运算,float32可能是更合适的选择。





3.6.2 浮点型的使用细节

  • Golang 浮点类型有固定的范围和字段长度,不受具体 OS(操作系统)的影响。
  • Golang 的浮点型默认声明为 float64 类型
  • 浮点型常量有两种表示形式
    1. 十进制数形式:如:5.12.512 (必须有小数点)
    2. 科学计数法形式:如:
      5.1234e2 = 5.12*10的2次方
      5.12E-2 = 5.12/10的2次方
    3. 通常情况下,应该使用 float64 ,因为它比 float32 更精确。[开发中,推荐使用 foat64]




3.7 字符型

3.7.1 基本介绍

在Go语言中,处理字符数据涉及两种主要的数据类型:byterune。这两种类型分别用于处理ASCII字符和Unicode字符,非常适合在不同的应用场景中进行文本处理。


  1. byte

byte 类型是 uint8 的别名,代表一个8位无符号整数。它通常用于处理ASCII字符,因为ASCII字符集中的每个字符都可以在8位(即1字节)中表示。在Go字符串中,字符串本质上是一个字节的切片(slice),所以处理ASCII或基于字节的数据时常常使用 byte 类型。

示例代码

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
var b byte = 'A'
fmt.Printf("The byte is: %c\n", b) // 输出字符 'A'
}

在这个例子中,变量 b 被声明为 byte 类型,存储了ASCII字符 'A' 的字节表示。

一个用法,其他字母为var c byte = 'a' + byte(2), 这个字母为c。在Go中,当字符和数字进行运算时,实际上是在操作它们的数值。这里的计算结果是 97 + 2 = 99


  1. rune

rune 类型是 int32 的别名,用于表示一个Unicode码点。这使得 rune 能够存储任何Unicode字符,包括那些复杂的、多字节的字符,如汉字或表情符号。在Go中,rune 能够处理来自世界上任何语言的字符,非常适合构建国际化和多语言的应用程序。

示例代码

1
2
3
4
5
6
7
8
package main

import "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 main

import "fmt"

func main() {
s := "Hello, 世界"
r := []rune(s) // 将字符串转换为rune切片
r[7] = '界' // 修改切片中的元素
fmt.Println(string(r)) // 将rune切片转换回字符串
}

这个例子展示了如何将一个包含多语言字符的字符串转换为 rune 切片,修改它,然后再转换回字符串。


总结

在Go中,byterune 分别适用于不同的文本处理需求。选择正确的字符类型对于构建高效和可靠的文本处理功能至关重要。





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,它只有两个可能的值:truefalse。【占一个字节】


使用布尔型

布尔型常用于控制结构如条件语句(ifelse)、循环(forwhile 在 Go 中使用 for 实现)以及控制程序逻辑流的任何地方。它也常常用于函数返回值,以表示操作的成功、错误检查或任何二分逻辑情况。


示例代码

下面是一些使用布尔型的基本示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
a := true
b := false

if a {
fmt.Println("a is true")
}

if !b {
fmt.Println("b is not true")
}
}

在这个例子中,变量 ab 分别被赋予了布尔值 truefalse。接着使用 if 语句根据这些布尔值控制程序流。


布尔运算

Go语言支持标准的布尔逻辑运算,包括:

  • &&(逻辑与):如果两个操作数都为 true,结果才为 true
  • ||(逻辑或):如果两个操作数中至少有一个为 true,结果就为 true
  • !(逻辑非):如果操作数为 true,结果为 false;如果操作数为 false,结果为 true

运算示例

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
fmt.Println(true && false) // 输出: false
fmt.Println(true || false) // 输出: true
fmt.Println(!true) // 输出: false
}

这段代码演示了三种基本的布尔逻辑运算。





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

这里的 runeValuerune 类型,表示单个 Unicode 字符。


字符串和字节切片的转换

字符串可以与字节切片([]byte)相互转换。这允许程序直接修改或处理底层的字节数据。字符串转换为字节切片:

1
2
s := "hello"
b := []byte(s)

字节切片转换回字符串:

1
s := string(b)

性能考虑

字符串操作,特别是涉及拼接大量字符串的操作,可能会影响性能,因为每次拼接都可能生成新的字符串。在这种情况下,使用 strings.Builderbytes.Buffer 可以更有效地构建字符串,因为它们在内部使用了更为高效的数据结构来减少内存分配和复制。


示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"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 // s3 的值为 "Hello, world"
s1 += "Go" // s1 现在是 "Hello, 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 中是不可变的。尝试改变字符串中的字符将导致编译错误:

1
2
s := "Hello"
// s[0] = 'h' // 这会导致编译错误

要修改字符串,可以先将其转换为 []rune[]byte,修改后再转换回来:

1
2
3
4
s := "hello"
b := []byte(s)
b[0] = 'H'
s = string(b) // s 现在是 "Hello"

使用标准库

strings 包提供了一系列用于字符串处理的函数,如查找、替换、比较、截取等:

1
2
3
4
import "strings"

fmt.Println(strings.Contains("test", "es")) // 输出 true
fmt.Println(strings.ToUpper("test")) // 输出 "TEST"




3.9.2 string使用的注意事项

1) Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员

rune 和 Unicode
  1. rune 的定义:在 Go 语言中,runeint32 的别名。它用于表示一个 Unicode 码点,因此它是固定 32 位宽的。每个 rune 可以存储从 U+0000 到 U+10FFFF 的任何 Unicode 码点。

  2. 字符串和 UTF-8:Go 中的字符串以 UTF-8 格式存储,这是一种变长编码方式,意味着每个 Unicode 字符可以占用 1 到 4 个字节。这样的存储方式支持广泛的字符集而不会浪费存储空间。

  3. 使用 range 遍历字符串:当你使用 range 遍历一个字符串时,Go 自动处理 UTF-8 编码的复杂性。在每次迭代中,它解码 UTF-8 字节序列,返回当前字符的 Unicode 码点作为 rune 类型,以及这个字符在字符串中的起始字节位置。

  4. rune 接收 Unicode 字符:通过 range 循环,你可以把字符串中变长的每个字符(UTF-8 编码的)依次取出来,并使用 rune 类型来接收每个字符的完整码点。这使得字符处理变得简单,尤其是在需要字符级操作的场合,如文本分析或处理多语言文本。


示例代码

这里是一个演示如何使用 rangerune 来遍历字符串并处理每个字符的示例:

1
2
3
4
5
6
7
8
9
10
package main

import "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 main

import "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)
}
查看输出
1
2
3
4
0
0

false




3.11 基本数据类型的相互转换

Golang 和 java/c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数据类型不能自动转换


基本语法

1
数据类型(变量)

案例

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 main

import "fmt"

func main() {
var a int64 = 9999
var b int8 = int8(a)

fmt.Println(b) // 输出为15,并不是9999
}

例子

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
var a int32 = 12
var b int64

// b = a + 12 // 错
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

尽管 aint64 类型,而字面量 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 main

import "fmt"

func main() {
var a int32 = 12
var b int8
var c int8

b = int8(a) + 127 // b = 12 + 127 = 139,但会被截断为 int8 范围内的值
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 main

import "fmt"

func main() {
var num1 int = 90
var num2 float64 = 23.456
var b bool = true
var c byte = 'h'
var str string // 空的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 main

import (
"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)

/*
说明:
'f': 格式
10: 表示小数保留10位
64: 表示这个小数是float64
*/
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)

// strconv包中有一个函数Itoa
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 main

import (
"fmt"
"strconv"
)

func main() {
var str string = "true"
var b bool

/*
说明:
1. strconv.ParseBool(str) 函数会返回两个值(value bool, err error)
2. 因为只想获取value bool, 不想获取err error, 所以使用_忽略
*/
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 → 0bool → 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 main

import "fmt"

func main() {
var n1 int64 = 12
var n2 *int64 = &n1

fmt.Println(*n2) // 输出为12
}




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 main

import "fmt"

func main() {
// 值类型示例
a := 10
b := a
b = 20
fmt.Println(a, b) // 输出:10 20,修改b不会影响a

// 引用类型示例
arr1 := []int{1, 2, 3}
arr2 := arr1
arr2[0] = 100
fmt.Println(arr1, arr2) // 输出:[100 2 3] [100 2 3],修改arr2会影响arr1
}

在这个例子中,int是值类型,而slice是引用类型。对于值类型的赋值,修改后的变量不会影响原变量;而对于引用类型的赋值,修改后的变量会影响原变量。

希望这可以解答你的问题。如果有更多具体的需求或问题,随时告诉我!


这里统一说明一下默认值

  • 布尔类型是 false ,数值是 0 ,字符串是 “”。

  • 数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0]

  • 指针,slice,和 map 的零值都是 nil ,即还没有分配空间。





3.15 标识符命名规则

命名规则和C/C++一样……


其他:

  • 下划线_ 本身在Go中是一个特殊的标识符,称为空标识符。可以代表其他任何其他的标识符。但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用

  • 命名规范
    如果变量名、函数名、常量名首字母大写,则可以被其他包访问;
    如果变量名、函数名、常量名首字母小写,则只能在本包访问。
    【这是 Go 语言的一个明确的规定,可以简单的理解成,首字母大写是公开的,首字母小写是私有的,在 golang 没有public、private等关键字

    说的再详细一些

    • 公开(导出) :如果一个标识符(如类型、变量、常量、函数等)的名称以大写字母开头,它就可以从其他包中访问。这意味着这个标识符是对外公开的。

    • 私有(非导出) :如果标识符以小写字母开头,它就只能在其定义的包内部访问,对其他包是不可见的。





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 支持自定义数据类型

基本语法

1
type 自定义数据类型名 数据类型

值得一提的是,在go中,自定义类型名和原来的数据类型,被认为是两个不同的类型。


案例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
type myInt int

var num1 myInt
var num2 int
num1 = 40
num2 = int(num1) // 虽然都是int,但是go认为myInt和int是两个不同的数据类型
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 main

import "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) // 输出为30
}

在 Go 语言中,使用 type 关键字定义的新类型,它不会从其底层类型继承任何方法或行为。这种类型声明主要是为了提供类型安全和增加代码的可读性。以下是几个关键点:

  • 底层数据结构:新类型会采用相同的内部数据结构作为底层类型。这意味着如果底层类型是 int,那么新类型在内存中的存储方式和 int 完全相同。

  • 类型独立:尽管新类型和其底层类型在内存表示上相同,但在类型系统中它们是完全独立的。这样做的目的是为了增强类型安全,避免类型之间的隐式转换,减少潜在的错误。

  • 显式转换:可以在新类型和其底层类型之间进行显式类型转换。这是因为它们共享相同的数据表示,所以这种转换是安全的。

    1
    2
    3
    type MyInt int
    test1 := MyInt(10) // 可以
    test1 := MyInt(10.1) // 编译出错
  • 不继承方法:如果底层类型有方法,这些方法不会自动应用到新类型上。如果你希望新类型具有相似的方法,需要显式地为新类型定义这些方法。





3.19 常量

在Go语言中,定义常量的语法非常类似于定义变量,不过是将 var 关键字换成 const。常量一旦被赋值后,其值就不能被修改。


定义单个常量

你可以这样定义一个常量:

1
const Pi = 3.14159

定义多个常量

如果你想一次性定义多个常量,可以使用括号将它们组合在一起,类似于定义多个变量:

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


  • fmt.Scanln()
1
func Scanln(a …interface{}) (n int, err error)

换行时才停止扫描。最后一个条目后必须有换行或者到达结束位置。


  • fmt.Scanf()
1
func Scanf(format string, a …interface{}) (n int, err error)

Scanf从标准输入扫描文本。根据format参数指定的格式将成功读取的空白分隔的值保存进成功传递给本函数的参数。返回成功扫描的条目个数和遇到的任何错误。


例子

要求输入姓名、年龄、薪水,并打印

使用fmt.Scanln()

输入:

1
2
3
ggw
22
28500
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import "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
ggw 22 28500
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "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 字符串相关

  1. 统计字符串长度len
1
func len(v Type) int

内建函数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 main

import "fmt"

func main() {
str := "hello哈喽"
fmt.Println(len(str)) // 输出为11(5 + 3 + 3)
}

  1. 字符串遍历,处理中文问题[]rune(str)

先看一个错误的做法

1
2
3
4
5
6
7
8
9
10
package main

import "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 main

import "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. 字符串转整数
1
func Atoi(s string) (i int, err error)

  1. 整数转字符串
1
func Itoa(i int) string

  1. 字符串转[]byte
1
2
str := "hello go"
var bytes = []byte(str)

  1. [byte]转字符串
1
str = string([]byte{97, 98, 99})

  1. 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. 查找子串是否在指定的字符串中
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. 统计一个字符串有几个指定的子串
1
func Count(s, sep string) int

返回字符串s中有几个不重复的sep子串。

例:

1
2
fmt.Println(strings.Count("cheese", "e"))
fmt.Println(strings.Count("five", "")) // before & after each rune

Output:

1
2
3
5

  1. 不区分大小写的字符串比较【== 是区分字母大小写的】
1
func EqualFold(s, t string) bool

判断两个utf-8编码字符串(将unicode大写、小写、标题三种格式字符视为相同)是否相同。

例:

1
fmt.Println(strings.EqualFold("Go", "go"))    // true
1
fmt.Println("Go" == 'go')                    // false

  1. 返回子串在字符串第一次出现的 index 值
1
func Index(s, sep string) int

子串sep在字符串s中第一次出现的位置,不存在则返回-1。


  1. 返回子串在字符串最后一次出现的 index 值
1
func LastIndex(s, sep string) int

子串sep在字符串s中最后一次出现的位置,不存在则返回-1。


  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. 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
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. 将字符串的字母进行大小写的转换
1
func ToLower(s string) string

返回将所有字母都转为对应的小写版本的拷贝。

1
func ToUpper(s string) string

返回将所有字母都转为对应的大写版本的拷贝。


  1. 将字符串左右两边的空白去掉
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
a lone gopher

  1. 将字符串左右两边指定的字符去掉
1
func Trim(s string, cutset string) string

返回将s前后端所有cutset包含的utf-8码值都去掉的字符串。

例:

1
fmt.Printf("[%q]", strings.Trim(" !!! Achtung! Achtung! !!! ", "! "))

Output:

1
["Achtung! Achtung"]

  1. 将字符串左边指定的字符去掉
1
func TrimLeft(s string, cutset string) string

返回将s前端所有cutset包含的utf-8码值都去掉的字符串。


  1. 将字符串右边指定的字符去掉
1
func TrimRight(s string, cutset string) string

返回将s后端所有cutset包含的utf-8码值都去掉的字符串。


  1. 判断字符串是否以指定的字符串开头
1
func HasPrefix(s, prefix string) bool

判断s是否有前缀字符串prefix。


  1. 判断字符串是否以指定的字符串结束
1
func HasSuffix(s, suffix string) bool

判断s是否有后缀字符串suffix。





5.3 时间日期相关

时间和日期相关函数,需要导入 time 包

  1. time.Time 类型,用于表示时间
1
2
3
4
5
6
7
8
9
10
11
package main

import (
"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. 如何获取到其它的日期信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"time"
)

func main() {
// 1. 获取当前时间
now := time.Now()
fmt.Printf("now = %v\nnow type = %T\n", now, now)

// 2. 通过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. 格式化日期时间

方式 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 main

import (
"fmt"
"time"
)

func main() {
// 1. 获取当前时间
now := time.Now()
fmt.Printf("now = %v\nnow type = %T\n", now, now)

// 2. 通过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

这个时间一定要对,格式可以自己写


  1. 时间常量

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


  1. 结合 Sleep 来使用一下时间常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import (
"fmt"
"time"
)

func main() {
// 每秒打印一个数字,打印到100
for i := 0; i <= 100; i++ {
fmt.Println(i)
time.Sleep(time.Second)
}
}

  1. 时间戳
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 main

import (
"fmt"
"time"
)

func main() {
// 1. 获取当前时间
now := time.Now()
fmt.Printf("now = %v\nnow type = %T\n", now, now)

// Unix和Unixnano的使用
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. 统计程序执行时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"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

  1. len
1
func len(v Type) int

内建函数len返回 v 的长度,这取决于具体类型:

数组:v中元素的数量
数组指针:*v中元素的数量(v为nil时panic)
切片、映射:v中元素的数量;若v为nil,len(v)即为零
字符串:v中字节的数量
通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零


  1. new
1
func new(Type) *Type

内建函数new分配内存。其第一个实参为类型,而非值。其返回值为指向该类型的新分配的零值的指针。

例:

1
2
3
4
5
6
7
8
9
package main

import "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

  1. 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……

  1. 单分支

语法:

1
2
3
if 条件表达式 {
……
}

ps:一定要有{}


  1. 双分支

语法:

1
2
3
4
5
if 条件表达式 {
……
} else {
……
}

ps:以下写法都是错的

1
2
3
4
5
6
if 条件表达式 {
……
}
else { // else前不能换行
……
}

  1. 多分支

语法:

1
2
3
4
5
6
7
8
9
if 条件表达式1 {
……
} else if 条件表达式2 {
……
}
……
else {

}



特殊之处

Go的 if 语句允许你在条件判断之前执行一个语句,该语句和条件判断由分号(;)分隔。

1
2
3
4
if 初始化语句; 条件表达式 {
// 条件为真时执行的代码
}

这种在 if 语句中进行初始化的做法非常有用,因为它能帮助:

  1. 减少作用域污染:在 if 语句中声明的变量仅在 ifelse 块中可见,这有助于避免在更广泛的作用域中引入不必要的变量。
  2. 简化错误处理代码:这种方法在处理可能失败的函数调用时尤其有用,使得代码更紧凑、易读。

重要细节

  • 作用域:初始化语句中声明的变量的作用域仅限于if语句和相关的else ifelse块。
  • 初始化的限制if语句中的初始化只能在第一个if部分进行,不能在else ifelse部分进行新的初始化。
  • 逻辑清晰:这种方式鼓励程序员在一个地方进行变量初始化,使得程序逻辑更加清晰和集中。




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 main

import "fmt"

func main() {
var key byte // 定义一个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 main

import "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
// 1
for 循环条件 {
……
}

// 2
for {
……
}

// 与2等价
for ;; {
……
}

ps:Go语言中没有whiledo…while的概念


练习:打印九九乘法表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "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

break语句用于中断某个语句块的执行,用于中断for。

和C/C++差不多,但是go的switch会自动终止,不需要break了。


  • continue

和C/C++差不多


  • goto

不主张使用。

1
2
3
goto label
……
label: ……

  • return

和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 // 自动返回min和max
}

和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) // 输出10

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 utils

import "fmt"

var Name string = setName() // 执行【1】

func setName() string {
fmt.Println("utils.go 变量定义执行")
return "ggw"
}

func init() { // 执行【2】
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 main

import (
"fmt"
"go_code/project1/utils"
)

var Age int = setAge() // 执行【3】

func setAge() int {
fmt.Println("main.go 变量定义执行")
return 22
}

func init() { // 执行【4】
fmt.Println("main.go init()执行")
}

func main() { // 执行【5】
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 main

import "fmt"

func main() {
res := func (n1, n2 int) int {
return n1 + n2
} (10, 20)

fmt.Println(res) // 输出为30
}

用法二

匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
a := func (n1, n2 int) int {
return n1 + n2
}

res := a(10, 20)

fmt.Println(res) // 输出为30
}

用法三

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

var a = func (n1, n2 int) int {
return n1 + n2
}

func main() {
res := a(10, 20)

fmt.Println(res) // 输出为30
}




7.1.7 闭包

基本介绍:闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)

下面介绍可能难理解,反正就是外面初始化环境,返回的函数在这个环境下执行


通过一个实例来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "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)) // 输出:11
fmt.Println(f(2)) // 输出:13
fmt.Println(f(3)) // 输出:16
}

对上面代码进行说明:

1、AddUpper 是一个函数,返回的数据类型是 fun (int) int
2、闭包的说明:返回的是一个匿名函数, 但是这个匿名函数引用到函数外的n ,因此这个匿名函数就和n 形成一个整体,构成闭包。
3、可以这样理解: 闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。
4、当我们反复的调用 f 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。
5、要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。



闭包的最佳实践

请编写一个程序,具体要求如下

  1. 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包

  2. 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。

  3. 要求使用闭包的方式完成

  4. 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 main

import (
"fmt"
"strings"
)

// makeSuffix 函数返回一个闭包,该闭包检查并可能添加特定的文件后缀
func makeSuffix(suffix string) func(string) string {
// 返回的闭包
return func(filename string) string {
// 如果文件名不以指定的后缀结尾,则添加后缀
if !strings.HasSuffix(filename, suffix) {
return filename + suffix
}
// 如果已经有了后缀,就直接返回文件名
return filename
}
}

func main() {
// 创建一个闭包,用于处理.jpg后缀
addJpgSuffix := makeSuffix(".jpg")

// 测试闭包功能
fmt.Println(addJpgSuffix("mypicture")) // 输出: mypicture.jpg
fmt.Println(addJpgSuffix("holiday.jpg")) // 输出: holiday.jpg
fmt.Println(addJpgSuffix("family_photo")) // 输出: family_photo.jpg
fmt.Println(addJpgSuffix("portrait.jpeg")) // 输出: portrait.jpeg.jpg

// 创建一个闭包,用于处理.png后缀
addPngSuffix := makeSuffix(".png")
// 测试闭包功能
fmt.Println(addPngSuffix("mypicture")) // 输出: mypicture.png
}




7.1.8 注意事项和使用细节

  1. Go 函数不支持函数重载。

  2. 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    package main

    import "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
  3. 形参没有被使用并不会报错。





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 main

import "fmt"

func sum(n1, n2 int) int {
/*
当执行到defer时,暂不执行,而会将defer后面的
语句压入独立的栈中。
当函数执行完毕后,再从栈中,按照后入先出的方式出栈,执行
*/
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 main

import "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
package 包名

  • 引入包的语法
1
import "包的路径"
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 utils

// 为了让其他文件使用Sum()函数,需要将首字母大写
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 main

import (
"fmt"
"go_code/project1/utils" // D:\go\go_path_1.22.4\src\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 注意事项和使用细节

  1. 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母。

所以也可以不保持一致,但是不会这么做,那么不保持一致的情况如下:

utils.go

1
2
3
4
5
6
package abc // 不一致

// 为了让其他文件使用Sum()函数,需要将首字母大写
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 main

import (
"fmt"
"go_code/project1/utils" // D:\go\go_path_1.22.4\src\go_code\project1\utils
)

func main() {
var a float64 = 20
var b float64 = 10

sum := abc.Sum(a, b) // 对应修改
fmt.Println(sum)
}
  1. package语句要放在文件第一行。

  2. import 包时,路径从 $GOPATHsrc 下开始,不用带 src , 编译器会自动从 src 下开始引入

  3. 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public,这样才能跨包访问。比如 utils.go 的。
    对应的,首字母小写,只能被本包文件使用,其它包文件不能使用,类似 privat。
    变量也是同样来控制访问函数外部声明/定义的变量叫全局变量,作用域在整个包都有效,如果其首字母为大写,则作用域在整个程序有效

  4. 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件的。

  5. 如果包名较长,Go 支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了。

    1
    import ut "go_code/project1/utils"
    1
    2
    3
    4
    import (
    "fmt"
    ut "go_code/project1/utils"
    )
  6. 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义。

  7. 如果你要编译成一个可执行程序文件,就需要将这个包声明为 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 main

import "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.Newpanic内置函数。

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 main

import (
"errors"
"fmt"
)

// 函数去读取配置文件init.conf的信息
// 如果文件名传入不正确,我们就返回一个自定义的错误
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 定义和内存布局

  • 数组的定义
1
var 数组名 [数组大小]数据类型

  • 内存布局

和C/C++一样,地址连续。

例:

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
var arr [5]int64 // int64 8字节
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]intarr 实际上表示的是这个数组的整个数据结构,而不仅仅是一个指向数组首元素的指针。这与C语言中数组名被用作指向数组首元素的指针有所不同。尽管如此,在Go中,数组的名字在某些上下文中表现得类似于指针,尤其是在涉及内存地址时。

Go中数组的内存布局

在Go中,arr 指的是一个具体的数组结构,这个结构在内存中占据连续的空间,从 arr[0] 开始。这意味着数组 arr 的内存地址,即 &arr,实际上就是其第一个元素 arr[0] 的地址,即 &arr[0]。因此,输出 &arr&arr[0] 通常会得到相同的内存地址值。

数组是值类型

在Go中,数组是值类型。当数组作为参数传递给函数时,会进行数组的完整复制,而不是像C语言中那样只传递指针。这意味着数组的大小和元素是该类型的固有部分。因此,在Go中,尽管 arrarr[0] 的地址相同,这并不意味着 arr 是一个指针这只是因为数组的存储方式导致数组的首地址与其第一个元素的地址相同。数组在Go中是一个独立的、完整的数据结构,这与C语言中的数组(通常用作指向其首元素的指针)有明显的区别。





8.1.2 数组的使用

  • 访问数组元素
1
数组名[index]

  • 数组的初始化方式
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 main

import "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 main

import "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 注意事项和使用细节

  1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化。【和C/C++不同的是,go会报越界错误
  2. var arr []int 这时 arr 就是一个 slice 切片,而不是数组。
  3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用。
  4. 数组创建后,如果没有赋值,有默认值(零值)。【和C/C++区别】
  5. 使用数组的步骤 1. 声明数组并开辟空间 2. 给数组各个元素赋值(默认零值) 3. 使用数组。
  6. Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响。【和C/C++区别】
  7. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
  8. 长度是数组类型的一部分,在传递函数参数时需要考虑数组的长度,看下面案例。

案例1:

1
2
3
4
5
6
7
8
9
package main

func 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 main

func 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 main

func test(arr [3]int) {
}

func main() {
var nums = [3]int{1, 2, 3}
test(nums)
}

正确

  1. 在Go语言中,数组的大小必须在编译时是已知的,这意味着数组的长度必须是一个常量表达式




8.2 切片

先看一个需求:我们需要一个数组用于保存学生的成绩,但是学生的个数是不确定的,请问怎么办?

解决方案:使用切片

8.2.1 切片的基本介绍

1) 切片的英文是 slice

2) 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。

3) 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice) 都一样。

4) 切片的长度是可以变化的,因此切片是一个可以动态变化数组

5) 切片定义的基本语法:

1
var 切片名 []类型

快速入门案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"


func main() {
var arr [5]int = [...]int{1, 2, 3, 33, 4}

// 声明/定义一个切片
// slice := arr[1:3]
// 1. slice就是切片名
// 2. arr[1:3] 表示slice引用到arr这个数组
// 3. 引用arr数组的起始下标为1,最后下标为3(但是不包括3)
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() :

1
func cap(v Type) int

内建函数cap返回 v 的容量,这取决于具体类型:

数组:v中元素的数量,与 len(v) 相同
数组指针:*v中元素的数量,与len(v) 相同
切片:切片的容量(底层数组的长度);若 v为nil,cap(v) 即为零
信道:按照元素的单元,相应信道缓存的容量;若v为nil,cap(v)即为零





8.2.2 内存布局

切片不存储任何数据本身,它们只是对底层数组的引用


切片的内存结构

切片在内存中的表示由三个主要部分组成:

  1. 指针:指向切片所引用的数组的第一个元素的指针。这不一定是数组的物理起始地址,取决于切片是从数组的哪个部分开始的。
  2. 长度(length):切片当前包含的元素个数。长度决定了可以安全访问切片的元素数目。
  3. 容量(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 main

import "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 main

import "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 main

import "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 main

import "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) // 输出:[0 1 2]
fmt.Println(slice1) // 输出:[0 1 2 0 0]
fmt.Println(slice2) // 输出:[0 1]
}

  • 切片是引用类型,所以在传递时,遵守引用传递机制。

  • 指针,slice,和 map 的零值都是 nil,即还没有分配空间。




8.2.6 string和slice

  1. string 底层是一个 byte 数组,因此 string 也可以进行切片处理
1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
str := "hello world"
slice := str[3:]
fmt.Println("slice=", slice) // slice= lo world
}

  1. string 是不可变的,也就说不能通过 str[0] = ‘z’ 方式来修改字符串
1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
str := "hello world"
str[0] = 'x' // 编译不通过,因为string不可变
}

  1. 如果需要修改字符串,可以先将 string 转为 []byte / 或者 []rune,然后修改后转成 string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
str := "hello world"
arr1 := []byte(str)
arr1[0] = 'x'
str = string(arr1)
fmt.Println(str) // xello world

arr2 := []rune(str)
arr2[0] = '哈'
str = string(arr2)
fmt.Println(str) // 哈ello world
}




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 main

import "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) // [1 1 2 3 5 8 13 21]
}




8.3 二维数组

8.3.1 定义使用和内存布局

语法

1
var 数组名 [大小][大小]类型

初始化方法一:先声明再初始化

初始化方法二:声明的时候直接初始化

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 main

import "fmt"

func main() {
var arr = [2][3]int{{1, 2, 3},
{4, 5, 6}}

// 方式1
for i := 0; i < len(arr); i++ {
for j := 0; j < len(arr[i]); j++ {
fmt.Printf("%d ", arr[i][j]);
}
fmt.Println()
}

// 方式2
for _, v := range arr {
for _, v2 := range v {
fmt.Printf("%d ", v2)
}
fmt.Println()
}
}




8.4 二维切片

在Go语言中定义和使用二维切片(切片的切片)涉及几个步骤,从基础的创建和初始化到灵活的操作和应用。这里,我们将逐步介绍如何定义、初始化、填充、操作和使用二维切片。


  1. 定义二维切片

二维切片本质上是一个切片,其中的每个元素本身也是一个切片。这种结构允许你创建类似于二维数组的数据结构,但与传统的二维数组不同的是,二维切片的每一行可以独立调整大小。

基本定义语法:

1
var matrix [][]int

这行代码定义了一个名为 matrix 的二维切片,其中存储的元素类型为 int


  1. 初始化二维切片

二维切片在使用前需要初始化。因为二维切片实际上是切片的切片,所以你需要先初始化外层切片,然后初始化每一个内层切片。

使用 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. 填充二维切片

定义并初始化二维切片后,接下来可以填充它。

示例:填充数据

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. 访问和修改二维切片

访问和修改二维切片的元素非常直接。

示例:修改元素

1
matrix[0][0] = 10  // 将第一行第一列的元素修改为 10

示例:追加元素

如果需要在特定行追加新列或添加新行:

1
2
3
4
5
6
// 追加新列到第一行
matrix[0] = append(matrix[0], 99)

// 追加新行
newRow := []int{10, 20, 30, 40}
matrix = append(matrix, newRow)

  1. 遍历二维切片

遍历二维切片通常使用嵌套循环来完成。

示例:打印所有元素

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 main

import "fmt"

func main() {
// map声明和注意事项
var a map[string]string

// 在使用map前,需要先make,make的作用就是给map分配数据空间
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 增删改查

  1. 增加和更新
1
map["key"] = value //如果 key 还没有,就是增加,如果 key 存在就是修改

  1. 删除
1
delete(map, "key")

delete 是一个内置函数,如果 key 存在,就删除该 key-value,如果 key 不存在, 不操作,但是也不会报错。

细节说明
1、如果我们要删除 map 的所有 key ,没有一个专门的方法一次删除,可以遍历一下 key, 逐个删除
2、或者 map = make(…),make 一个新的,让原来的成为垃圾,被 gc 回收


  1. 查找
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 main

import "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 main

import "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 main

import (
"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

/*
要对map进行排序:
1. 先将map的key放入切片中
2. 对切片进行排序
3. 遍历切片
*/
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 main

import "fmt"

// 定义一个Cat结构体,将Cat的各个字段/属性信息进行管理
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 // 占用 1 字节
B int32 // 占用 4 字节
C int64 // 占用 8 字节
D string // 在 64 位系统中, 指针占用 8 字节
}

在64位的系统上,这个结构体的内存布局会受到对齐规则的影响,从而可能出现内存填充(padding):

  • A 作为 bool 类型,占用1字节,但后面可能需要填充3字节,以保证 B 作为 int32 能够从4字节边界开始。
  • B 正好对齐,占用4字节。
  • Cint64,从下一个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 // 占用 8 字节
B int32 // 占用 4 字节
A bool // 占用 1 字节
D string // 在 64 位系统中, 指针占用 8 字节
}

这样排列可以减少或消除填充,因为较小的字段可以填补由较大字段留下的间隙。

  • 使用 unsafe 包查看内存布局

如果你想查看Go中任何数据结构的实际内存布局,可以使用 unsafe 包的 SizeofAlignofOffsetof 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"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
}

说明

  • 从概念或叫法上看: 结构体的 字段 = 属性 = field

  • 字段是结构体的一个组成部分,一般是基本数据类型数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性


注意事项和细节说明

1) 字段的类型可以为:基本类型、数组或引用类型

2) 在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值),规则同前面讲的一样。

3)不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型





10.1.3 创建和访问字段

使用

现在有这样一个结构体:

1
2
3
4
type Person struct {
Name string
Age int
}

那么如何定义变量和访问字段呢

方式一:直接声明

1
var person Person

方式二:声明并定义

1
var person Person = Person{}
1
var person Person = Person{"merry", 20}

方式三:使用new分配结构体指针

1
var person *Person = new (Person)

指针类型怎么访问字段?

标准方式:

1
(*p).Name = "Jack"

这种方式类似C/C++,但是为了程序员使用方便,可以如下这么写:

1
p.Name = "Jack"

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. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型,字段顺序也要一样)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "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)
}
  1. 结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type Student struct {
Name string
Age int
}

type Teacher Student

func main() {
teacher1 := Teacher{"xxx", 30}
student1 := Student(teacher1)
fmt.Println(student1)
}
  1. 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 main

import (
"encoding/json"
"fmt"
)

type Student struct {
Name string `json:"name"` // `json:"name"` 就是 struct tag
Age int `json:"age"`
}

func main() {
stu := Student{"xxx", 15}

// 将stu变量序列化为json格式字符串
// hson.Marshal 函数中使用反射,后面会再说反射的概念
jsonStr, err := json.Marshal(stu)
if err != nil {
fmt.Println("json处理错误", err)
} else {
fmt.Println("jsonStr: ", string(jsonStr))
}

// 输出为 jsonStr: {"name":"xxx","age":15}
// 没有加tag输出为 jsonStr: {"Name":"xxx","Age":15}
// 总之这里就是可以让json字符串中变量小写,或者自定义
}




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 main

import "fmt"

type Person struct{
Name string
}
// 给Person类型绑定一方法
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. 如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

type Person struct{
Name string
}
// 给Person类型绑定一方法
func (p *Person) test() {
p.Name = "xxx"
}

func main() {
var p Person
p.Name = "tom"
// (&p).test() // go编译器也做了优化也可以写成下面的形式,等价的
p.test()
fmt.Println(p.Name)
}

【为了提高效率,通常用的也是指针,可以减少拷贝

  1. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型, 都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法。
  2. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。
  3. 如果一个类型实现了 String() 这个方法,那么 fmt.Println 默认会调用这个变量的 String() 进行输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

type Person struct{
Name string
}
// 给Person类型绑定一方法
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 model

type student struct{
Name string
Score float64
}

// 因为student结构体的首字母是小写的,因此只能在model包中使用
// 我们通过工厂模式解决
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 main

import (
"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 的新实例,并返回一个指向它的指针。这种方式通常会导致逃逸到堆上,因为:

  1. 返回指针:函数返回了一个指向 student 实例的指针。由于函数返回后指针依然有效,这意味着 student 实例的生命周期至少与这个返回的指针一样长。
  2. 未知的引用情况:返回的指针可能被赋给任何外部变量或者用于其他函数调用,编译器无法确保其只在局部使用,因此必须保守处理,分配在堆上。

如何查看逃逸分析结果

可以通过以下命令查看你的Go程序的逃逸分析报告,这有助于理解特定的变量为何需要在堆上分配:

1
go build -gcflags='-m' your_program.go

这条命令会在构建时输出编译器的逃逸分析详情,显示哪些变量逃逸到堆上以及原因。


总结

逃逸分析是Go编译器的一个重要特性,它帮助程序在保证性能的同时,自动管理内存。通过了解逃逸分析,开发者可以更好地理解内存的使用情况,有时还可以通过调整代码设计来优化性能和内存使用。在你的案例中,NewStudent 函数中的 student 实例很可能会因为逃逸到堆上而被垃圾回收器管理。





10.5 封装

封装(encapsulation)就是把抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只有通过被授权的操作(方法),才能对字段进行操作。

好处:
1) 隐藏实现细节
2) 提可以对数据进行验证,保证安全合理

  1. 如何体现封装

1) 对结构体中的属性进行封装
2) 通过方法 实现封装


  1. 封装的实现步骤

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 本身对面向对象的特性做了简化的.


  1. 案例

请写一个程序(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 model

import "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 main

import (
"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. 嵌入匿名结构体的基本语法
1
2
3
4
5
6
7
8
9
type Goods struct {
Name string
Price int
}

type Book struct {
Goods //这里就是嵌套匿名结构体 Goods
Writer string
}

当我们对结构体嵌入了匿名结构体使用方法会发生变化book.Goods.Goods的方法


  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
62
63
64
65
66
67
68
package main

import (
"fmt"
)

// 编写一个学生考试系统
type Student struct {
Name string
Age int
Score int
}

// 将Pupil 和 Graduate 共有的方法也绑定到 *Student
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
}

// 给 *Student 增加一个方法,那么 Pupil 和 Graduate都可以使用该方法
func (stu *Student) GetSum(n1 int, n2 int) int {
return n1 + n2
}

// 小学生
type Pupil struct {
Student // 嵌入了Student匿名结构体
}

// 显示他的成绩
// 这时Pupil结构体特有的方法,保留
func (p *Pupil) testing() {
fmt.Println("小学生正在考试中.....")
}

// 大学生
type Graduate struct {
Student // 嵌入了Student匿名结构体
}

// 显示他的成绩
// 这时Graduate结构体特有的方法,保留
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 嵌套了一个有名结构体,这种模式就是**组合**,如果是组合关系,那么在访问组合的结构体的字段或方法时,必须带上结构体的名字

1
2
3
type D struct{
a A // 有名结构体,是组合关系
}
1
2
var d D
d.a.Name

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 main

import "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 main

import "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 main

import "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 main

import "fmt"

// 定义一个接口
type Usb interface {
// 声明两个没有实现的方法
Start()
Stop()
}

type Phone struct {

}

// 让Phone实现usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作……")
}

func (p Phone) Stop() {
fmt.Println("手机停止工作……")
}

type Camera struct {

}

// 让Camera实现usb方法
func (p Camera) Start() {
fmt.Println("相机开始工作……")
}

func (p Camera) Stop() {
fmt.Println("相机停止工作……")
}

// 计算机
type Computer struct {

}

// 编写一个方法Working方法,接受一个Usb接口类型变量
// 只要是实现了Usb接口(所谓实现Usb接口,就是指实现了Usb接口声明的所有方法)
func (c Computer) Working(usb Usb) {
// 通过Usb接口变量来调用Start和Stop
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 main

type AInterface interface {
Say()
}

func main() {
var a AInterface
a.Say()
}
/*
报错信息:
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0xbe1290]

goroutine 1 [running]:
main.main()
d:/go/go_path_1.22.4/src/go_code/encapsulate/main/main.go:9 +0x10
exit status 2
*/

正确使用应该是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "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 main

import "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 main

type BInterface interface {
test01()
}

type CInterface interface {
test02()
}

type AInterface interface {
BInterface
CInterface
test03()
}

// 如果需要实现AInterface, 就需要将BInterface CInterface的方法都实现
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{}

1
var t interface{} = stu




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 main

import "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
// 这一句会报错,因为是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方法返回集合中的元素个数
Len() int
// Less方法报告索引i的元素是否比索引j的元素小
Less(i, j int) bool
// Swap方法交换索引i和j的两个元素
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 main

import (
"fmt"
"math/rand"
"sort"
)

// 1. 声明Hero结构体
type Hero struct {
Name string
Age int
}

// 2. 声明一个Hero结构体切片
type HeroSlice []Hero

// 3. 实现Interface接口
func (hs HeroSlice) Len() int {
return len(hs)
}

// Less表示按什么标准进行排序
// 1. 按照Hero的年龄从小到大进行排序
func (hs HeroSlice) Less(i, j int) bool {
// 对年龄排序
// return hs[i].Age < hs[j].Age
// 对姓名排序
return hs[i].Name < hs[j].Name
}

func (hs HeroSlice) Swap(i, j int) {
// temp := hs[i]
// hs[i] = hs[j]
// hs[j] = temp
// 上面三句话等价于下面一句话
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
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”例子

接口体现多态的两种方式

  1. 多态参数

在前面的 Usb 接口案例,(usb Usb),即可以接收手机变量,又可以接收相机变量,就体现了 Usb 接口 多态。


  1. 多态数组

演示一个案例:给 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 main

import "fmt"

// 定义一个接口
type Usb interface {
// 声明两个没有实现的方法
Start()
Stop()
}

type Phone struct {
name string
}

// 让Phone实现usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作……")
}

func (p Phone) Stop() {
fmt.Println("手机停止工作……")
}

type Camera struct {
name string
}

// 让Camera实现usb方法
func (p Camera) Start() {
fmt.Println("相机开始工作……")
}

func (p Camera) Stop() {
fmt.Println("相机停止工作……")
}

func main() {
// 定义一个Usb接口数组,可以存放Phone和Camera的结构体变量
// 这里就体现了多态数组
var usbArr [3]Usb

usbArr[0] = Phone{"vivo"}
usbArr[1] = Phone{"小米"}
usbArr[2] = Camera{"Sony"}

fmt.Println(usbArr) // 输出为:[{vivo} {小米} {Sony}]
}




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 main

import "fmt"

type Point struct {
x int
y int
}

func main() {
var a interface{}
var point Point = Point{1, 2}
a = point

// 如果我现在想将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 main

import "fmt"

func main() {
// 类型断言的其他案例
var x interface{}
var b2 float32 = 1.1
x = b2

y := x.(float32)
fmt.Printf("y的类型是%T, 值为%v", y, y)
// 输出为:y的类型是float32, 值为1.1
}

说明:在进行类型断言时,如果类型不匹配,就会报 panic,因此进行类型断言时,要确保原来的空接口指向的就是断言的类型.


如何在进行断言时,带上检测机制,如果成功就 ok,否则也不要报 panic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "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 main

import "fmt"

// 定义一个接口
type Usb interface {
// 声明两个没有实现的方法
Start()
Stop()
}

type Phone struct {
name string
}

// 让Phone实现usb接口的方法
func (p Phone) Start() {
fmt.Println("手机开始工作……")
}

func (p Phone) Stop() {
fmt.Println("手机停止工作……")
}

func (p Phone) Call() {
fmt.Println("手机打电话……")
}

type Camera struct {
name string
}

// 让Camera实现usb方法
func (p Camera) Start() {
fmt.Println("相机开始工作……")
}

func (p Camera) Stop() {
fmt.Println("相机停止工作……")
}

type Computer struct {

}

func (computer Computer) Working(usb Usb) {
usb.Start()
// 如果是Phone变量,除了调用Usb接口声明的方法外,
// 还需要调用Phone特有的方法,call()【类型断言】
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"}

// 遍历usbArr
// Phone 还有一个特有的方法call(),请遍历usbArr数组
// 如果是Phone变量,除了调用Usb接口声明的方法外,
// 还需要调用Phone特有的方法,call()【类型断言】
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 main

import "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 main

import (
"fmt"
"os"
)

func main() {
// 打开文件
// 下面的file变量,可称为 file对象,file指针,file句柄
file, err := os.Open("sample.txt")
if err != nil {
fmt.Println("open file error, err=", err)
}

// 使用defer取关闭文件
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 读文件

  1. 读取文件的内容并显示在终端(带缓冲区的方式),使用 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
aaa
ccc
bbb

例子:

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 main

import (
"bufio"
"fmt"
"io"
"os"
)

func main() {
// 打开文件
file, err := os.Open("sample.txt")
if err != nil {
fmt.Println("open file error, err=", err)
}

// 使用defer取关闭文件
defer func() {
err := file.Close()
if err != nil {
fmt.Println("close file error, err=", err)
}
}()

// 创建一个 *Reader,这是带缓冲的
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")
}

输出:

1
2
3
4
aaa
ccc
bbb
end

  1. 读取文件的内容并显示在终端(使用 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 main

import (
"fmt"
"io/ioutil"
)

func main() {
// 使用ioutil.ReadFile一次性将文件读取到位
content, err := ioutil.ReadFile("sample.txt")
if err != nil {
fmt.Println("Read file error, err=", err)
}

fmt.Println(string(content)) // content为[]byte类型

// ioutil.ReadFile 里面使用了open和close,所以不要close了
}




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_CREATE配合使用,文件必须不存在
O_SYNC int = syscall.O_SYNC // 打开文件用于同步I/O
O_TRUNC int = syscall.O_TRUNC // 如果可能,打开时清空文件
)

第三个参数:(只针对Unix系统)

1
2
3
4
5
type FileMode uint32

r->4
w->2
x->1

案例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 main

import (
"bufio"
"fmt"
"os"
)

func main() {
// 创建一个新文件,写入内容
// 1. 打开文件sample.txt
filePath := "sample.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Println("open file err:", err)
}

// 2. 及时关闭文件
defer func() {
if err := file.Close(); err != nil {
fmt.Println("close file err:", err)
}
}()

// 3. 准备写入
writer := bufio.NewWriter(file)
for i := 0; i < 10; i++ {
_, err := writer.WriteString("hello world\n")
if err != nil {
fmt.Println("write err:", err)
}
}

// 4. 因为writer是带缓冲的,因此上面的内容先写到缓冲区了,
// 所以现在要让缓冲区的内容写回文件中。
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

  1. 打开一个存在的文件(刚才创建的那个)中,将原来的内容覆盖成新的内容 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 main

import (
"bufio"
"fmt"
"os"
)

func main() {
// 1. 打开已存在的文件sample.txt
filePath := "sample.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
fmt.Println("open file err:", err)
}

// 2. 及时关闭文件
defer func() {
if err := file.Close(); err != nil {
fmt.Println("close file err:", err)
}
}()

// 3. 写入时使用带缓冲的*writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
_, err := writer.WriteString("hello shopee\n")
if err != nil {
fmt.Println("write err:", err)
}
}

// 4. 因为writer是带缓冲的,因此上面的内容先写到缓冲区了,
// 所以现在要让缓冲区的内容写回文件中。
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

  1. 打开一个存在的文件,在原来的内容追加内容 ‘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 main

import (
"bufio"
"fmt"
"os"
)

func main() {
// 1. 打开已存在的文件sample.txt
filePath := "sample.txt"
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
fmt.Println("open file err:", err)
}

// 2. 及时关闭文件
defer func() {
if err := file.Close(); err != nil {
fmt.Println("close file err:", err)
}
}()

// 3. 写入时使用带缓冲的*writer
writer := bufio.NewWriter(file)
for i := 0; i < 5; i++ {
_, err := writer.WriteString("become gopher\n")
if err != nil {
fmt.Println("write err:", err)
}
}

// 4. 因为writer是带缓冲的,因此上面的内容先写到缓冲区了,
// 所以现在要让缓冲区的内容写回文件中。
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 main

import (
"fmt"
"io/ioutil"
)

func main() {
// 把sample.txt文件的内容写到sample-copy.txt中
// 1. 首先将sample.txt的内容读到内存中
// 2. 将内容写到文件里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 main

import (
"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 main

import (
"bufio"
"fmt"
"io"
"os"
)

// CopyFile 编写一个函数,接收两个文件路径srcFileName dstFileName
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)
}
}()

// 通过src, 获取reader
reader := bufio.NewReader(srcFile)
// 打开dst
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)
}
}()
// 通过dstFile获取writer
writer := bufio.NewWriter(dstFile)
return io.Copy(writer, reader)
}

func main() {
// 将sample.jpg文件拷贝到sample-copy.jpg
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 main

import (
"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 main

import (
"flag"
"fmt"
)

func main() {
// 定义几个变量,用于接收命令行的参数值
var (
username string
password string
hostname string
port int
)

// 说明:
// &user 就是接收用户命令行的输入的 -u 后的参数值
// "" 是默认值
// "用户名,默认为空" 是说明
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:"-"`
// 字段在json里的键为"myName"
Field int `json:"myName"`
// 字段在json里的键为"myName"且如果字段为空值将在对象中省略掉
Field int `json:"myName,omitempty"`
// 字段在json里的键为"Field"(默认值),但如果字段为空值会跳过;注意前导的逗号
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 main

import (
"encoding/json"
"fmt"
)

// Monster 定义一个结构体
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: "牛魔拳",
}

// 将 monster 序列化
data, err := json.Marshal(&monster)
if err != nil {
fmt.Printf("序列号错误 err=%v\n", err)
}
// 输 出 序 列 化 后 的 结 果
fmt.Printf("monster 序列化后=%v\n", string(data))
}

// 将 map 进行序列化
func testMap() {
// 定义一个 map
var a map[string]interface{}
// 使用 map,需要 make
a = make(map[string]interface{})
a["name"] = "红孩儿"
a["age"] = 30
a["address"] = "洪崖洞"
// 将 a 这个 map 进行序列化
// 将 monster 序列化
data, err := json.Marshal(a)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
// 输出序列化后的结果
fmt.Printf("a map 序列化后=%v\n", string(data))
}

// 演示对切片进行序列化, 我们这个切片 []map[string]interface{}
func testSlice() {
var slice []map[string]interface{}
var m1 map[string]interface{}
// 使用 map 前,需要先 make
m1 = make(map[string]interface{})
m1["name"] = "jack"
m1["age"] = "7"
m1["address"] = "北京"
slice = append(slice, m1)

var m2 map[string]interface{}
// 使用 map 前,需要先 make
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

//对 num1 进行序列化
data, err := json.Marshal(num1)
if err != nil {
fmt.Printf("序列化错误 err=%v\n", err)
}
//输出序列化后的结果
fmt.Printf("num1 序列化后=%v\n", string(data))
}

func main() {
// 演示将结构体, map , 切片进行序列号
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 main

import (
"encoding/json"
"fmt"
)

// Monster 定义一个结构体
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: "牛魔拳",
}

// 将 monster 序列化
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 main

import (
"encoding/json"
"fmt"
)

// Monster 定义一个结构体
type Monster struct {
Name string
Age int
Birthday string //....
Sal float64
Skill string
}

// 演示将 json 字符串,反序列化成 struct
func unmarshalStruct() {
//说明 str 在项目开发中,是通过网络传输获取到.. 或者是读取文件获取到
str := "{\"Name\":\"牛魔王\",\"Age\":500,\"Birthday\":\"2011-11-11\",\"Sal\":8000,\"Skill\":\"牛魔拳\"}"

//定义一个 Monster 实例
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)
}

// 演示将 json 字符串,反序列化成 map
func unmarshalMap() {
str := "{\"address\":\"洪崖洞\",\"age\":30,\"name\":\"红孩儿\"}"

//定义一个 map
var a map[string]interface{}

//反序列化
//注意:反序列化 map,不需要 make,因为 make 操作被封装到 Unmarshal 函数
err := json.Unmarshal([]byte(str), &a)
if err != nil {
fmt.Printf("unmarshal err=%v\n", err)
}
fmt.Printf("反序列化后 a=%v\n", a)
}

// 演示将 json 字符串,反序列化成切片
func unmarshalSlice() {
str := "[{\"address\":\"北京\",\"age\":\"7\",\"name\":\"jack\"}," +
"{\"address\":[\"墨西哥\",\"夏威夷\"],\"age\":\"20\",\"name\":\"tom\"}]"

//定义一个 slice
var slice []map[string]interface{}
//反序列化,不需要 make,因为 make 操作被封装到 Unmarshal 函数
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 main

import "fmt"

func partition(arr []int, left, right int) int { // [left, right]
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) { // [left, right]
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 main

import (
"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

规则总结

  1. 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go, cal不是固定的。

  2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper。Test后面必须紧跟一个大写字母。

  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T

  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub

  5. 运行测试用例指令

    go test [如果运行正确,无日志,错误时,会输出日志]

    go test -v [运行正确或是错误,都输出日志]

  6. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

  7. t.Logf 方法可以输出相应的日志

  8. 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].

  9. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

  10. 测试单个文件,一定要带上被测试的原文件,eg: go test -v cal_test.go cal.go

  11. 测试单个方法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)上运行。

  • Go 协程的特点

    1) 有独立的栈空间

    2) 共享程序堆空间

    3) 调度由用户控制

    4) 协程是轻量级的线程

  • 值的注意的是如果主线程退出了,则协程及时还没有执行完毕也会退出。

  • Golang 的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显 Golang 在并发上的优势了





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. 用全局变量的互斥锁来解决需求
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 main

import (
"fmt"
"sync"
"time"
)

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.
// 2. 我们启动的协程多个,统计的将结果放入到 map 中

var (
myMap = make(map[int]int)
lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1

for i := 1; i <= n; i++ {
res *= i
}

lock.Lock()
myMap[n] = res
lock.Unlock()
}

func main() {
// 我们这里开启多个协程完成这个任务[10 个]

for i := 1; i <= 10; i++ {
go test(i)
}

// 休眠10秒,防止主进程结束
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

  1. 使用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

1
var 变量名 chan 数据类型

eg:

1
2
3
4
var intChan chan int 
var mapChan chan map[int]string
var perChan chan Person
var perChan2 chan *Person

说明:

  • channel 是引用类型

  • channel 必须初始化才能写入数据, 即 make 后才能使用。

  • 管道是有类型的,intChan 只能写入 整数 int


管道的初始化,写入数据到管道,从管道读取数据及基本的注意事项

  • 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 main

import "fmt"

func main() {
// 演示channel的使用
// 1. 创建一个可以存放三个int类型的管道
var intChan chan int
intChan = make(chan int, 3)

// 2. 看看intChan是什么
fmt.Printf("intChan 的值=%v\nintChan 本身的地址=%p\n", intChan, &intChan)

// 3. 像管道写数据
intChan <- 1
num1 := 10
intChan <- num1
intChan <- 50
//intChan <- 50 // 超过容量报错

fmt.Println("----------------------------")

// 4. 看看管道的长度和cap(容量)
//fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))

fmt.Println("----------------------------")

// 5. 从管道中读取数据
var num2 int = <-intChan
fmt.Printf("num2=%v \n", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan))

fmt.Println("----------------------------")

// 6. 在没有使用协程的情况下,如果管道数据已全部取出,那么再取会deadlock
num3 := <-intChan
num4 := <-intChan
// num5 := <-intChan // fatal error: all goroutines are asleep - deadlock!
fmt.Printf("num3=%v num4=%v num5=%v\n", num3, num4 /*, num5*/)
}
查看输出
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 main

import "fmt"

func main() {
intChan := make(chan int, 3)
intChan <- 1
intChan <- 2
close(intChan)
// 现在不能再写入到intChan中了
//intChan <- 3 // panic: send on closed channel
fmt.Println(<-intChan)
// 当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 main

import "fmt"

func main() {
intChan := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan <- i
}

// 遍历不能使用普通的 for 循环【因为len不固定,是动态变化的】
//for i := 0; i < len(intChan); i++ {
// fmt.Println(<-intChan)
//}

// 在遍历时,如果intChan没有关闭,则出现deadlock的错误
// 在遍历时,如果intChan关闭,则会正常遍历数据,遍历完毕后,就会退出遍历
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 main

import (
"fmt"
"sync"
)

// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map 中.
// 2. 我们启动的协程多个,统计的将结果放入到 map 中

var (
myMap = make(map[int]int)
lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
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() {
// 我们这里开启多个协程完成这个任务[10 个]
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 main

import "fmt"

func main() {
// 1. 在默认情况下,管道是双向的

// 2. 声明为只写
var chan1 chan<- int
chan1 = make(chan int, 3)
chan1 <- 1
//n2 := <-chan1 // 直接编译错误

// 3. 声明为只读
var chan2 <-chan int
chan2 = make(chan int, 3)
fmt.Println(chan1, chan2)
}

  • channel 只读和只写的最佳实践案例
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 main

import (
"fmt"
"time"
)

func main() {

// 使用 select 可以解决从管道取数据的阻塞问题

// 1.定义一个管道 10 个数据 int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan <- i
}

// 2.定义一个管道 5 个数据 string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)

}

// 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock

// 问题,在实际开发中,可能我们不好确定什么关闭该管道.
// 可以使用 select 方式可以解决
for {
select {
// 注意: 这里,如果 intChan 一直没有关闭,不会一直阻塞而 deadlock
// 会自动到下一个 case 匹配
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 main

import (
"fmt"
"time"
)

// 函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}

// 函 数
func test() {
// 这里我们可以使用 defer + recover
defer func() {

if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
// 定义了一个 map
var myMap map[int]string
myMap[0] = "golang" // error
}

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 反射的重要函数和概念

  1. reflect.TypeOf(变量名),获取变量的类型,返回reflect.Type类型

  2. 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{} 类型,以便进行常规类型操作
  3. 变量、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 main

import (
"fmt"
"reflect"
)

// 【基本数据类型】
func reflectTest01(b interface{}) {
// 1. 先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType=", rTyp)

// 2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)
n2 := 2 + rVal.Int()
fmt.Println("n2=", n2)
fmt.Printf("rVal=%v rVal type=%T\n", rVal, rVal)

// 下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
// 将 interface{} 通过断言转成需要的类型
num2 := iV.(int)
fmt.Println("num2=", num2)
}

type Student struct {
Name string
Age int
}

// 【结构体】
func reflectTest02(b interface{}) {
// 1. 先获取到 reflect.Type
rTyp := reflect.TypeOf(b)
fmt.Println("rType=", rTyp)

// 2. 获取到 reflect.Value
rVal := reflect.ValueOf(b)

// 下面我们将 rVal 转成 interface{}
iV := rVal.Interface()
fmt.Printf("iv=%v iv type=%T \n", iV, iV)
// 将 interface{} 通过断言转成需要的类型
// 这里,我们就简单使用了一带检测的类型断言.
// 同学们可以使用 switch 的断言形式来做的更加的灵活
stu, ok := iV.(Student)
if ok {
fmt.Printf("stu.Name=%v\n", stu.Name)
}
}

func main() {
// 1. 先定义一个 int
var num = 100
reflectTest01(num)
fmt.Println("----------------------------")

// 2. 定义一个 Student 的实例
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,获取变量的类别,返回的是一个常量

    1
    type Kind uint

    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 main

    import (
    "fmt"
    "reflect"
    )

    func test(b interface{}) {
    // 这里传的是指针,那么获取值得到的是个地址
    val := reflect.ValueOf(b)
    fmt.Printf("val type: %T\n", val)
    // 用Elem()方法的找到地址val指向的reflect.Value类型的空间
    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 main

import (
"fmt"
"reflect"
)

// Monster 定义了一个结构体
type Monster struct {
Name string `json:"name"`
Age int `json:"monster_age"`
Score float32 `json:"成绩"`
Sex string
}

// GetSum 方法,返回两个数的和
func (s Monster) GetSum(n1, n2 int) int {
return n1 + n2
}

// Set 方法, 接收四个值,给 s 赋值
func (s Monster) Set(name string, age int, score float32, sex string) {
s.Name = name
s.Age = age
s.Sex = sex
}

// Print 方法,显示 s 的值
func (s Monster) Print() {
fmt.Println("---start~ ")
fmt.Println(s)
fmt.Println("---end~ ")
}

func TestStruct(a interface{}) {
// 获取 reflect.Type 类型
typ := reflect.TypeOf(a)
// 获取 reflect.Value 类型
val := reflect.ValueOf(a)
//获取到 a 对应的类别
kd := val.Kind()
// 如果传入的不是 struct,就退出
if kd != reflect.Struct {
fmt.Println("expect struct")
return
}
// 获取到该结构体有几个字段
num := val.NumField()
fmt.Printf("struct has %d fields\n", num) // 4
// 变量结构体的所有字段
for i := 0; i < num; i++ {
fmt.Printf("Field %d: 值为=%v\n", i, val.Field(i))
// 获取到 struct 标签, 注意需要通过 reflect.Type 来获取 tag 标签的值
tagVal := typ.Field(i).Tag.Get("json")
// 如果该字段于 tag 标签就显示,否则就不显示
if tagVal != "" {
fmt.Printf("Field %d: tag 为=%v\n", i, tagVal)
}
}

//获取到该结构体有多少个方法
numOfMethod := val.NumMethod()
fmt.Printf("struct has %d methods\n", numOfMethod)

//var params []reflect.Value
//方法的排序默认是按照 函数名的排序(ASCII 码)
val.Method(1).Call(nil) //获取到第二个方法。调用它

//调用结构体的第 1 个方法 Method(0)
var params []reflect.Value //声明了 []reflect.Value
params = append(params, reflect.ValueOf(10))
params = append(params, reflect.ValueOf(40))
res := val.Method(0).Call(params) //传入的参数是 []reflect.Value, 返回[]reflect.Value
fmt.Println("res=", res[0].Int()) //返回结果, 返回的结果是 []reflect.Value*/
}

func main() {
//创建了一个 Monster 实例
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 main

import (
"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. 声明和初始化

    1
    var wait = sync.WaitGroup{}
    • 这行代码声明并初始化了一个 sync.WaitGroup 实例,命名为 wait。你可以使用 wait 来跟踪和等待多个 goroutine 的完成。
  2. 添加等待计数

    1
    wait.Add(1)
    • Add(n int) 方法用于设置需要等待的 goroutine 数量。在你启动每个 goroutine 之前,调用 Add(1) 增加等待计数。n 表示需要等待的任务数量。
  3. 启动 goroutine 并执行任务

    1
    2
    3
    4
    go func() {
    defer wait.Done() // 标记goroutine已完成
    // 执行任务
    }()
    • 每个 goroutine 在启动时需要在其执行的代码块中使用 defer wait.Done(),以确保该 goroutine 完成后调用 Done() 方法来减少等待计数。
    • Done() 方法减少一个等待计数,表示有一个 goroutine 已经完成。
  4. 等待所有任务完成

    1
    wait.Wait()
    • 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 main

import (
"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 main

import (
"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 main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second))
go GetIp(ctx)

// 5秒到了,手动结束协程
time.Sleep(5 * time.Second)
//cancel() // 可以手动取消,也可让他自然超时

// 模拟主线程阻塞
time.Sleep(1 * time.Second)

}

func GetIp(ctx context.Context) {
fmt.Println("获取ip中")
// 等待请求完成或者被取消
select {
case <-ctx.Done():
// 请求被取消
fmt.Println("请求超时或被取消", ctx.Err()) // 可以通过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 main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
go GetIp(ctx)

// 5秒到了,手动结束协程
time.Sleep(5 * time.Second)
//cancel() // 可以手动取消,也可让他自然超时

// 模拟主线程阻塞
time.Sleep(1 * time.Second)

}

func GetIp(ctx context.Context) {
fmt.Println("获取ip中")
// 等待请求完成或者被取消
select {
case <-ctx.Done():
// 请求被取消
fmt.Println("请求超时或被取消", ctx.Err()) // 可以通过err判断是超时还是取消
}
}

和WithDeadline的主要区别

  1. 时间表示
    • WithTimeout 使用的是相对时间,即从调用时刻开始计时的持续时间(如“5秒后”)。
    • WithDeadline 使用的是绝对时间,即特定的截止时间点(如“2024年8月13日下午3点”)。
  2. 灵活性
    • WithTimeout 更适合那些从当前时刻开始的定时操作。
    • WithDeadline 更适合那些需要在某个具体时间点之前完成的操作。
  3. 内部实现
    • WithTimeout 实际上是基于 WithDeadline 实现的。WithTimeout(parent, timeout) 实际上等价于 WithDeadline(parent, time.Now().Add(timeout))