0%

Go基础

Go语言概述

持续更新

Go语言特征

  • 由Google开源

    • 好爸爸
  • 编译型语言

    • Go 语言在性能上更接近于 Java 语言,虽然在某些测试用例上不如经过多年优化的 Java 语言,但毕竟 Java 语言已经经历了多年的积累和优化。Go 语言在未来的版本中会通过不断的版本优化提高单核运行性能

      image-20210911215629322

  • 21世纪的C语言

    • 执行性能好,天生支持并发
    • Go 语言并不是凭空而造的,而是和 C++、Java 和 C# 一样同属 C 系
  • 开发效率高

    • 语法简洁,没有花里胡哨的众多语法糖,关键字的数量(25 个)很少
    • 代码风格统一,Go 语言提供了一套格式化工具——go fmt。一些 Go 语言的开发环境或者编辑器在保存时,都会使用格式化工具进行修改代码的格式化,这样就保证了不同开发者提交的代码都是统一的格式
      • 在vscode中,由于插件的支持,执行源文件保存后,会自动使用go fmt 执行格式化
    • Go 语言在这 3 个条件之间做到了最佳的平衡:快速编译,高效执行,易于开发
  • 天生支持并发

    • 大多数现代编程语言(如Java,Python等)都来自90年代的单线程环境。虽然一些编程语言的框架在不断地提高多核资源使用效率,例如 Java 的 Netty 等,但仍然需要开发人员花费大量的时间和精力搞懂这些框架的运行原理后才能熟练掌握
    • Go于2009年发布,当时多核处理器已经上市。Go语言在多核并发上拥有原生的设计优势,Go语言从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验
    • 硬件的提升已经达到瓶颈,因此需要从语言的底层,从软件层面提升性能
    • 经过 Go 语言重构的系统能使用更少的硬件资源获得更高的并发和I/O吞吐表现。充分挖掘硬件设备的潜力也满足当前精细化运营的市场大环境
  • Go语言主要解决的一些问题

    • Go 语言的主要目标是将静态语言的安全性和高效性与动态语言的易开发性进行有机结合,达到完美平衡
    • Go 语言的另一个目标是对网络通信、并发和并行编程有着极佳的支持,从而更好地利用大量的分布式和多核的计算机
    • Go 语言中另一个非常重要的特性就是它的构建速度(编译和链接到机器代码的速度),一般情况下构建一个程序的时间只需要数百毫秒到几秒,相对来说C++就慢多了
    • 依赖管理是现今软件开发的一个重要组成部分,但是 C 语言中 头文件 的概念却导致越来越多一个大型项目因为依赖关系而使得构建需要长达几个小时的时间的情况出现。人们愈发需要一门具有严格的、简洁的依赖关系分析系统从而能够快速编译的编程语言,这正是 Go 语言采用包模型的根本原因,这个模型通过严格的依赖关系检查机制来加快程序构建的速度,提供了非常好的可量测性
    • 由于内存问题(通常称为内存泄漏)长期以来一直伴随着 C++ 的开发者们,Go 语言的设计者们认为内存管理不应该是开发人员所需要考虑的问题。因此尽管 Go 语言像其它静态语言一样执行本地代码,但它依旧运行在某种意义上的虚拟机,以此来实现高效快速的垃圾回收(使用了一个简单的标记 - 清除算法)
    • Go 语言还能够在运行时进行反射相关的操作
    • 因为 Go 语言没有类和继承的概念,所以它和 Java 或 C++ 看起来并不相同。但是它通过接口(interface)的概念来实现多态性Go 语言有一个清晰易懂的轻量级类型系统,在类型之间也没有层级之说。因此可以说这是一门混合型的语言
  • Go的运行时(Rust完全没有运行时,性能因此更好,甚至超过C)

    尽管 Go 编译器产生的是本地可执行代码,然而这些代码仍旧运行在 Go 的 runtime(这部分的代码可以在 runtime 包中找到)当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收(第 11.8 节)、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等

    runtime 主要由 C 语言编写(自 Go 1.5 起开始自举),并且是每个 Go 包的最顶级包。你可以在目录 $GOROOT/src/runtime 中找到相关内容

    垃圾回收器 Go 拥有简单却高效的标记 - 清除回收器。它的主要思想来源于 IBM 的可复用垃圾回收器,旨在打造一个高效、低延迟的并发回收器。目前 gccgo 还没有回收器,同时适用于 gc 和 gccgo 的新回收器正在研发中。使用一门具有垃圾回收功能的编程语言不代表你可以避免内存分配所带来的问题,分配和回收内容都是消耗 CPU 资源的一种行为

    Go 的可执行文件都比相对应的源代码文件要大很多,这恰恰说明了 Go 的 runtime 嵌入到了每一个可执行文件当中。当然,在部署到数量巨大的集群时,较大的文件体积也是比较头疼的问题。但总得来说,Go 的部署工作还是要比 Java 和 Python 轻松得多。因为 Go 不需要依赖任何其它文件,它只需要一个单独的静态文件,这样你也不会像使用其它语言一样被各种不同版本的依赖文件混淆

Go语言可以干什么

  1. 区块链研发
  2. web服务端,游戏软件工程师
  3. 云计算、云原生工程师
  4. ….

Go特性的缺失

  • 许多能够在大多数面向对象语言中使用的特性 Go 语言都没有支持,但其中的一部分可能会在未来被支持

    • 为了简化设计,不支持函数重载和操作符重载
    • 为了避免在 C/C++ 开发中的一些 Bug 和混乱,不支持隐式转换
    • Go 语言通过另一种途径实现面向对象设计(第 10-11 章)来放弃类和类型的继承
    • 尽管在接口的使用方面(第 11 章)可以实现类似变体类型的功能,但本身不支持变体类型
    • 不支持动态加载代码
    • 不支持动态链接库
    • 不支持泛型
    • 通过 recover 和 panic 来替代异常机制(第 13.2-3 节)
    • 不支持断言
    • 不支持静态变量

Go安装与环境配置

Go安装

  • 对于Mac来说,就是下载官网的pkg安装包,无脑安装即可

环境配置

  • 可以使用go env查看与设置环境变量,在操作系统的配置文件中可以直接使用go env设置的环境变量
  1. GOROOT

    1. Go的安装目录,Mac下默认是/usr/local/go
    2. 使用pkg包安装之后,$GOROOT/bin目录被加入到/etc/paths.d/go中,以使得全局可以使用bin目录下的go工具,不用手动设置此环境变量
  2. GOPATH

    1. 工作目录,通过go env -w GOPATH=$GOPATH设置
    2. 事实上,在Go1.14及之后的版本中启用了Go Module模式之后,不一定非要将代码写到GOPATH目录下,所以也就不需要我们再自己配置GOPATH了,使用默认的即可
      1. Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,也不再强制我们把代码必须写在GOPATH下面的src目录了,你可以在你电脑的任意位置编写go代码
  3. GOPROXY

    1. 使用国内的下载源 go env -w GOPROXY=https://goproxy.cn,direct
  4. 下边的标准项目结构中的bin目录是存放项目可执行文件的地方,为了使得项目生成的可执行文件可以在任务地方执行,将该bin目录加入到环境变量

    1. nano ~/.bash_profile

      1
      export PATH=$PATH:$GOPATH/bin
    2. 参考

配置标准项目结构

  • 项目应该存储到$GOPATH目录下,标准结构如下(强制,但是go1.14之后引入模块系统后,不强制)

    • src放置源码
      • go install下载的第三方包源码也会在此目录
    • bin存放生成的可执行文件
      • 执行go install命令时会将可执行文件存储到此目录
    • pkg 存放中间缓存文件(编译后的库文件)
  • 团队开发与企业开发环境下的src目录的不同结构

    image-20210911190041149

    image-20210911190138299

安装IDE

vscode

  • 安装Google官方的Go插件即可
  • 右下角会提示安装一些额外的辅助工具(代码补全等等),点击install all,就会将这些工具安装在$GOPATH/bin目录中
  • Vscode code snippets的设置参考
    • 比如设置fmt.Println("hello world")的快捷键等等

GoLand

Hello World

构建步骤

  1. src目录下创建helloWorld文件夹作为项目文件夹,创建hello.go文件

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

    // 导入依赖包
    import "fmt"

    // 入口函数
    func main() {
    fmt.Println("hello world")
    }
  2. 初始化模块go mod init 项目名,go1.14后默认用module的方式管理项目依赖

  3. 编译(包含安装自身包和依赖包到可执行文件

    1. 在src目录下的项目根目录下执行go build 就会在同级目录下生成对应的可执行文件,可执行文件的文件名默认叫做模块名
      1. 注意:在版本go1.17.1下,执行go build,会报go: go.mod file not found in current directory or any parent directory; see 'go help modules'异常,参考Stack Overflow上的回答解决
        1. 应该是go的模块功能特性(go 1.14后引入),在创建项目中,要在项目文件夹下执行go mod init 项目名,否则就会报上述错误,执行命令后会在项目根目录下生成一个go.mod模块描述文件
          1. Go1.14版本之后,都推荐使用go mod模式来管理依赖环境了,也不再强制我们把代码必须写在GOPATH下面的src目录了,你可以在你电脑的任意位置编写go代码,具体的参考后边的依赖管理介绍
        2. 或者是直接设置go env -w GO111MODULE=off不建议,实际上是关闭了默认支持的go module依赖管理体系
    2. 或者在任意目录直接执行go build $GOFILEPATH
      1. $GOFILEPATH是项目的根目录位置,从src目录往下开始写就行,因为默认会去$GOPATH/src下去找
      2. 此时生成的可执行文件的位置就在命令执行时的当前目录
    3. 也可以直接go build main.go 直接指定源文件执行,此时默认的生成的可执行文件的文件名是源文件的文件名
    4. 可以指定-o 参数指定生成的可执行文件的名字
  4. 直接运行

    1. go run main.go直接编译运行,不会生成对应的可执行文件
  5. 安装可执行文件

    1. go install main.go,相当于先执行go build再把生成的可执行文件复制到$GOPATH/bin
    2. 除了安装自定义的包,也可以暗转第三方的依赖包
  6. 代码格式化

    1. Go 开发团队不想要 Go 语言像许多其它语言那样总是在为代码风格而引发无休止的争论,浪费大量宝贵的开发时间,因此他们制作了一个工具:go fmt(gofmt)。这个工具可以将你的源代码格式化成符合官方统一标准的风格,属于语法风格层面上的小型重构。遵循统一的代码风格是 Go 开发中无可撼动的铁律,因此你必须在编译或提交版本管理系统之前使用 gofmt 来格式化你的代码
      1. 在vscode中安装Go的插件后,实际上就会在保存文件时自动触发格式化
    2. 尽管这种做法也存在一些争论,但使用 gofmt 后你不再需要自成一套代码风格而是和所有人使用相同的规则。这不仅增强了代码的可读性,而且在接手外部 Go 项目时,可以更快地了解其代码的含义。此外,大多数开发工具也都内置了这一功能。
    3. Go 对于代码的缩进层级方面使用 tab 还是空格并没有强制规定,一个 tab 可以代表 4 个或 8 个空格。在实际开发中,1 个 tab 应该代表 4 个空格,而在本身的例子当中,每个 tab 代表 8 个空格。至于开发工具方面,一般都是直接使用 tab 而不替换成空格。
    4. 使用方法
      1. *在命令行输入 gofmt –w program.go 会格式化该源文件的代码然后将格式化后的代码覆盖原始内容(如果不加参数 -w 则只会打印格式化后的结果而不重写文件);gofmt -w .go 会格式化并重写所有 Go 源文件;gofmt map1 会格式化并重写 map1 目录及其子目录下的所有 Go 源文件
      2. gofmt 也可以通过在参数 -r 后面加入用双引号括起来的替换规则实现代码的简单重构,规则的格式:<原始内容> -> <替换内容>
        1. 实例:
          1. gofmt -r '(a) -> a' –w *.go 上面的代码会将源文件中没有意义的括号去掉。
          2. gofmt -r 'a[n:len(a)] -> a[n:]' –w *.go 上面的代码会将源文件中多余的 len(a) 去掉。( 译者注:了解切片(slice)之后就明白这为什么是多余的了 )
          3. gofmt –r 'A.Func1(a,b) -> A.Func2(b,a)' –w *.go 上面的代码会将源文件中符合条件的函数的参数调换位置。
      3. 如果想要了解有关 gofmt 的更多信息,请访问该页面
  7. 调试

    1. 目前可用的调试器是 gdb,但是Go 在这方面的发展还不是很完善
    2. 可以使用一些内置语法功能实现调试的目的
      1. 在合适的位置使用打印语句输出相关变量的值(print/println 和 fmt.Print/fmt.Println/fmt.Printf)。
      2. 在 fmt.Printf 中使用下面的说明符来打印有关变量的相关信息:
        1. %+v 打印包括字段在内的实例的完整信息
        2. %#v 打印包括字段和限定类型名称在内的实例的完整信息
        3. %T 打印某个类型的完整说明
      3. 使用 panic 语句来获取栈跟踪信息(直到 panic 时所有被调用函数的列表)。
      4. 使用关键字 defer 来跟踪代码执行过程
  8. 版本升级

    1. go fix 用于将你的 Go 代码从旧的发行版迁移到最新的发行版,它主要负责简单的、重复的、枯燥无味的修改工作,如果像 API 等复杂的函数修改,工具则会给出文件名和代码行数的提示以便让开发人员快速定位并升级代码。Go 开发团队一般也使用这个工具升级 Go 内置工具以及 谷歌内部项目的代码。go fix 之所以能够正常工作是因为 Go 在标准库就提供生成抽象语法树和通过抽象语法树对代码进行还原的功能。该工具会尝试更新当前目录下的所有 Go 源文件,并在完成代码更新后在控制台输出相关的文件名称
  9. 代码测试

    1. go test 是一个轻量级的单元测试框架,参考后续内容
  10. 生成代码文档(可以是web服务器的方式)

    1. go doc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。它也可以作为一个提供在线文档浏览的 web 服务器,golang.org 就是通过这种形式实现的。
    2. 一般用法
      1. go doc package 获取包的文档注释,例如:go doc fmt 会显示使用 godoc 生成的 fmt 包的文档注释。
      2. go doc package/subpackage 获取子包的文档注释,例如:go doc container/list。
      3. go doc package function 获取某个函数在某个包中的文档注释,例如:go doc fmt Printf 会显示有关 fmt.Printf() 的使用说明。
      4. 默认的,这个工具只能获取在 Go 安装目录下 ../go/src 中的注释内容。此外,它还可以作为一个本地文档浏览 web 服务器。在命令行输入 godoc -http=:6060,然后使用浏览器打开 localhost:6060 后,你就可以看到本地文档浏览服务器提供的页面。
      5. godoc 也可以用于生成非标准库的 Go 源码文件的文档注释
    3. 如果想要获取更多有关 godoc 的信息,请访问该页面(在线版的第三方包 godoc 可以使用 Go Walker)。

交叉编译

  • go支持交叉编译,可以在一台机器上构建运行在具有不同操作系统和处理器架构上运行的应用程序,也就是说编写源代码的机器可以和目标机器有完全不同的特性(操作系统与处理器架构)

  • 比如在mac下编译Windows或Linux下执行的程序(Linux下一样)

    1
    2
    CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build
    CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build

入口程序

  • 项目的入口程序是main包下的main函数,如果项目中没有main包,那么执行go build后不会产生可执行文件;如果main包下没有main函数会在执行编译时报错(与go源文件的文件名无关)
    • 类似于C语言(类C语言),Go中完全以一个函数作为执行单元,函数之外不能有非声明形的语句(可以有变量、常量、函数、类型的声明,但是不能有类似于fmt.Println("hello world")这样的语句)
  • 一个module中只能由一个入口函数

Go基础语法

标识符与关键字

标识符

  • 字母,数字、下划线,并且只能以字母和下划线开头

关键字、数据类型

  • 25个关键词

    1
    2
    3
    4
    5
    break        default      func         interface    select
    case defer go map struct
    chan else goto package switch
    const fallthrough if range type
    continue for import return var
  • 37个关键词

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Constants:    true  false  iota  nil

    Types: int int8 int16 int32 int64
    uint uint8 uint16 uint32 uint64 uintptr
    float32 float64 complex128 complex64
    bool byte rune string error

    Functions: make len cap new append copy close delete
    complex real imag
    panic recover
    • Types即Go中的数据类型
    • Constants即Go中的4种特殊的字面量

变量与常量

  • Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用(静态类型语言),Go语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用(所有的变量都会默认初始化,当然也可以在声明时直接初始化),同一作用域内不支持重复声明。 并且Go语言的局部变量声明(或初始化)后必须使用(全局变量可以声明但是不使用)
    • Go语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

变量

  • 变量声明

    1
    2
    3
    4
    var 变量名 变量类型
    var name string
    var age int
    var isOk bool
    • var是变量声明的关键词,而不是变量类型
  • 批量声明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var (
    a string
    b int
    c bool
    d float32
    )

    var(
    // 类型推导
    name = "lijia"
    age int
    number int32
    )
    • 当然也可以在批量声明的同时进行批量的初始化
    • 也可以在函数内部使用
  • 声明同时初始化

    1
    2
    3
    4
    5
    6
    7
    var 变量名 类型 = 表达式

    var name string = "Q1mi"
    var age int = 18

    // 批量初始化(类型推导)
    var name, age = "Q1mi", 20
    • 类型推导有点动态类型语言那个味道了,这也是Go的特征之一,静态类型与动态类型的兼顾
  • 函数内部的短变量声明(快速初始化)

    1
    2
    3
    4
    func main() {
    name := "lijia"
    fmt.Println(name)
    }
    • 注意仅仅是在函数内部可以使用
      • 换句话说,函数内部可以使用关键字声明变量,也可以使用短变量声明,但是函数外部必须使用关键字
    • 所谓快速就是省去了var关键字兼具类型推导
    • 使用快速初始化时,:=符号右边必须有新的未定义变量,不能全是已定义的变量
  • 匿名变量与多重赋值

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

    import "fmt"

    func foo() (int,string) {
    return 10, "lijia"
    }

    func main() {
    age, _ := foo();
    _, name := foo();

    fmt.Println(age, name)
    }
    • 在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示
      • 所谓多重赋值也可以理解成上边说的,批量的初始化,特殊的情况就是,当函数有多个返回值时可以一次性的接受所有返回值,注意这里也使用到了短变量声明
      • 匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明,其存在就是为了在多重赋值时忽略某个值
    • _下划线也可以作为匿名常量使用,参考下边的例子

常量

  • 常量的定义与变量类似,无非就是使用const关键字,并且必须在声明的同时进行初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const pi = 3.1415
const e = 2.7182

const(
// 类型推导
PI = 3.14
E = 2.7
)

const(
PI1 = 3.14
// E1这个常量的值也是3.14
E1
)

  • 同样有批量声明的方式,不同于变量的地方在于const同时声明多个常量时,如果省略了值则表示和上面一行的值相同(当然根据类型推导,类型也是一样的)

iota

  • 常量计数器,只能在常量表达式中使用,包括单行的声明,也包括批量的声明都可以使用
  • 在遇到const关键字时,iota会被初始化为0,使用时分成以下两个情况
    • 在常量批量定义的const代码块中,iota可以认为是行号计数器(注意iota的计数从const代码块的第一行自动开始,不管表达式中有没有用到iota
      • 注意是每经过一行常量定义就递增1,不管改行首有多个常量的声明
    • 在单行的const普通声明语句中,iota一直为0,因为每遇到const都会重置iota为0
  • 使用iota能简化定义,在定义枚举时很有用
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
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)
const (
n0 = 2 // 2
n1 = 1 // 1
n2 // 1
n3 = iota // 3
n4 // 4 可以认为是 n4 = iota 符合const批量定义中 如果省略了值则表示和上面一行的值相同的标准
n5 // 5 可以认为是 n5 = iota
)
const m0 = iota // 0

// 使用匿名常量跳过计数
const (
n1 = iota //0
n2 //1
_
n4 //3
)

// 定义数量级
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)

// 多个iota定义在一行,易错,需要注意
const (
a, b = iota + 1, iota + 2 //1,2 第一行 iota为0
c, d //2,3 等价于 c,d = iota + 1, iota + 2 第二行 iota为1 满足 如果省略了值则表示和上面一行的值的标准
e, f //3,4 等价于 e,f = iota + 1, iota + 2 第三行 iota为2 满足 如果省略了值则表示和上面一行的值的标准
)

nil

  • 切片、函数、指针变量的默认为nil

数据类型

基本数据类型

整型

image-20210913194335042

image-20210913194609255

  • 注意后三个特殊类型的整型,根据编译目的环境的操作系统平台的不同,表示不同的长度,即在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异(最后一个uintptr指针的长度也是随着平台的不同而有不同的长度(地址长度,32位就是32,64位就是64))
    • uintintuintptr的长度都是一个机器字
    • 关于uintptr数据类型的使用最多的就是后边会提到的unsafe.Sizeof函数
  • 获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示
  • 在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint(应该使用明确长度的类型)
数字字面量
  • Go1.13版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字

    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
    func main() {
    // 2进制
    v1 := 0b0101
    // 8进制
    v2 := 0o45
    // 16进制
    v3 := 0xfff
    // 16进制浮点数
    v4 := 0x1p-2

    // 输出较长的整数时,可以使用_做分隔符,以便于计数
    v5 := 123_456

    // 以十进制输出
    fmt.Println(v1, v2, v3, v4,v5)


    // 以指定格式输出整数
    // 十进制
    var a int = 10
    fmt.Printf("%d \n", a) // 10
    fmt.Printf("%b \n", a) // 1010 占位符%b表示二进制

    // 八进制 以0开头
    var b int = 077
    fmt.Printf("%o \n", b) // 77

    // 十六进制 以0x开头
    var c int = 0xff
    fmt.Printf("%x \n", c) // ff
    fmt.Printf("%X \n", c) // FF
    }
    • 对于16进制浮点数的表示参照下边的浮点型章节
    • 数字字面量默认是int类型

浮点型

  • Go语言支持两种浮点型数:float32float64

    • float32类型的数据的长度是32位,float64同理
    • 默认指定一个小数字面量后的,变量的类型是float64
  • 这两种浮点型数据格式遵循IEEE 754标准:

    • float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32
    • float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64
    1
    2
    3
    4
    5
    6
    7
    8
    9
    package main
    import (
    "fmt"
    "math"
    )
    func main() {
    fmt.Printf("%f\n", math.Pi)
    fmt.Printf("%.2f\n", math.Pi)
    }
    • 可以发现导入多个包的时候,使用的也是批量的语法,使用圆括号即可
  • 对于浮点型数据的字面量表示有两种

    • 一个浮点数的完整十进制字面量形式可能包含一个十进制整数部分、一个小数点、一个十进制小数部分和一个以10为底数的整数指数部分。 整数指数部分由字母e或者E带一个十进制的整数字面量组成(xEn表示x乘以10n的意思,而xE-n表示x除以10n的意思)。 常常地,某些部分可以根据情况省略掉

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      1.23
      01.23 // == 1.23
      .23
      1.
      // 一个e或者E随后的数值是指数值(底数为10)。
      // 指数值必须为一个可以带符号的十进制整数字面量。
      1.23e2 // == 123.0
      123E2 // == 12300.0
      123.E+2 // == 12300.0
      1e-1 // == 0.1
      .1e0 // == 0.1
      0010e-2 // == 0.1
      0e+5 // == 0.0
    • 从Go 1.13开始,Go也支持另一种浮点数字面量形式:十六进制浮点数字面量。 在一个十六进制浮点数字面量中,

      • 一个十六进制浮点数字面量必须以一个以2为底数的整数指数部分。 这样的一个整数指数部分由字母p或者P带一个十进制的整数字面量组成(yPn表示y乘以2n的意思,而yP-n表示y除以2n的意思)

      • 和整数的十六进制字面量一样,一个十六进制浮点数字面量也必须使用0x或者0X开头。 和整数的十六进制字面量不同的是,一个十六进制浮点数字面量可以包括一个小数点和一个十六进制小数部分

        1
        2
        3
        4
        5
        0x1p-2     // == 1.0/4 = 0.25
        0x2.p10 // == 2.0 * 1024 == 2048.0
        0x1.Fp+0 // == 1+15.0/16 == 1.9375
        0X.8p1 // == 8.0/16 * 2 == 1.0
        0X1FFFP-16 // == 0.1249847412109375
        • 不合法的十六进制浮点数表示

          1
          2
          3
          0x.p1    // 整数部分表示必须包含至少一个数字
          1p-2 // p指数形式只能出现在浮点数的十六进制字面量中
          0x1.5e-2 // e和E不能出现在十六进制浮点数字面量的指数部分中

复数

  • 没错就是信号系统中的那个有实部和虚部的复数

    • complex64 实部和虚部为32位
    • complex128 实部和虚部为64位
    1
    2
    3
    4
    5
    6
    func main() {
    var c1 complex64 = 1 + 2i
    var c2 complex128 = 1 - 2i

    fmt.Println(c1, c2)
    }

布尔

  • bool
    • true
    • false(默认)
  • Go 语言中不允许将整型强制转换为布尔型
    • Java同样不能
  • 布尔型无法参与数值运算,也无法与其他类型进行转换

字符串

  • Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符

    1
    2
    3
    4
    func main() {
    name := "李佳"
    fmt.Println(name)
    }
  • 转义字符

    image-20210913203132386

  • 多行字符串

    1
    2
    3
    4
    5
    6
    func main() {
    desc :=
    `你好吗? \n
    我很好`
    fmt.Println(desc)
    }
    • 使用反引号构造多行字符串,反引号中的转义字符原样输出,注意反引号之间的任一个回车和空格都会原样输出
  • 字符串操作

    image-20210913211051281

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import (
    "fmt"
    "strings"
    )

    func main() {
    desc := "hahah?"
    fmt.Println(desc)
    // 获取字符串长度
    len := len(desc)

    isContain := strings.Contains(desc, "?")

    fmt.Println("字符串的长度: ", len)
    fmt.Println("字符串是否包含问好: ", isContain)

    }
    • stirngs是一个额外的包,应该可以看做是字符串的工具包

byte与rune

  • Go中的字符分为两种

    • uint8类型,或者叫 byte 型,代表了ASCII码的一个字符
    • rune类型,代表一个 UTF-8字符
      • 处理中文等复杂字符时需要使用rune类型,rune类型实际是一个int32
    • 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。Go中的字符串同样是不能修改的。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成
      • rune字符可以强转为string(参考下边计算字符串中汉字数量的函数)
      • UTF8编码下一个中文汉字由3~4个字节组成
  • 字符串的两种遍历方式

    • 按照字节byte进行遍历
    • 按照rune进行遍历
    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
    func tranversalString() {
    s := "hello 李佳"

    // 按照字节遍历
    for i := 0; i < len(s); i++ {
    // 字符串的本质是[]byte,可以直接索引
    fmt.Printf("%v(%c)", s[i], s[i])
    }
    fmt.Println()
    // 按照rune遍历
    for _, v := range s {
    // 注意要将rune以字符串的形式输出的话,应该先转为字符串格式或者以Printf做格式化输出
    fmt.Printf("%v(%c)", v, v)
    }
    }



    // 题目,判断字符串内的汉字的数量
    func getnumOfChinese() {
    s := "hello沙河小王子haha"
    count := 0
    // 因为要判断汉字的个数,所有通过rune数组的形式进行遍历
    for _, v := range s {
    // 将rune类型的字符转为string,也就是[]byte后判断长度如果大于三个就是utf-8下的汉字
    if len(string(v)) >= 3 {
    count++
    }
    }
    fmt.Printf("字符串{%s}内部有[%d]个汉字", s, count)
    }

    // 格式化输出的练习

    func printValue() {
    i := 12
    var f float32 = 2.345
    var b bool = true
    s := "李佳"

    fmt.Printf("%d: %T \n", i, i) // 12: int
    fmt.Printf("%f: %T \n", f, f) // 2.345000: float32
    fmt.Printf("%v: %T \n", b, b) // true: bool
    fmt.Printf("%s: %T \n", s, s) // 李佳: string
    }
    • For-range遍历字符串时的value是rune类型,不能直接和string作比较,需要首先将value转为string再比较

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      // 统计单词出现的个数
      func main() {
      sequence := "how do you do"
      res := analysis(sequence)
      fmt.Println(res)
      }

      func analysis(s string) map[string]int {
      resMap := make(map[string]int)

      lastIndex := 0
      for index, value := range s {
      // 先转string再比较
      if string(value) == " " {
      word := s[lastIndex:index]
      resMap[word]++
      lastIndex = index + 1
      }
      }

      resMap[s[lastIndex:]]++
      return resMap
      }

      -

  • Go中的字符串类似于Java中的字符串是不可变的,即任何对字符串的修改都会重新分配内存,要修改字符串,需要先将其转换成[]rune[]byte(强制类型转换),完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 更改字符串
    func changeString() {
    s := "big"

    // 强转
    sByte := []byte(s)
    // 注意区分快速声明与赋值
    sByte[0] = 'p'

    // 强转回string
    fmt.Println(string(sByte))

    s1 := "李佳"
    sRune := []rune(s1)

    sRune[0] = '宋'
    fmt.Println(string(sRune))

    }
    • 注意string可以强转为两种类型的字符数组(本质上应该是切片类型而不是数组类型,字符串并不能强转为数组,只能强转为切片类型)

数据类型转换

  • Go只支持强制类型转换,不支持隐式的类型转换,例子可以参考上边的字符串的修改的案例T(表达式)

    1
    2
    3
    4
    5
    6
    7
    func sqrtDemo() {
    var a, b = 3, 4
    var c int
    // math.Sqrt()接收的参数是float64类型,需要强制转换
    c = int(math.Sqrt(float64(a*a + b*b)))
    fmt.Println(c)
    }

复杂数据类型

  • 数组、切片、结构体、函数、map、通道(channel)等,见后边的分析

各个数据类型的长度

image-20211009141628825

  • 注意基本数据类型和复杂数据类型的区分,后者的本质存储是指针;字符串类型以及切片类型的区分(二者本质上是类似的);接口类型变量的长度是2个机器字,分别是接口的动态类型与动态值(接口章节中有介绍)
  • 所谓机器字就是在32位OS上是32位,64位OS中是64位

各个数据类型的分类

  • 实际上就是引用类型与值类型的区分
  • 引用类型
    • map
    • slice
  • 值类型
    • 基本数据类型
    • 数组
    • 指针
    • 结构体

Go中的运算符

  • 其实都是常见的运算符,这里只是为了保持知识的完整性,因此记录一下
  • 基本就是C系列语言通用的运算符

算数运算符

image-20210914080939075

  • 对于注意事项的理解可以看下边的代码

    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
    func demo() {
    var a int = 12

    a++
    // 13
    fmt.Println(a)

    // 报错
    // ++a
    // 报错
    // b := ++a
    // 报错
    // b := a++

    a--
    // 12
    fmt.Println(a)

    // 报错
    // b := a--
    // 报错
    // --a
    // 报错
    // b := --a

    }

关系运算符

image-20210914081213153

逻辑运算符

image-20210914081609430

位运算符

image-20210914081726547

赋值运算符

image-20210914081801975

Go的流程控制

if

1
2
3
4
5
6
7
if 表达式1 {
分支1
} else if 表达式2 {
分支2
} else{
分支3
}
  • 特殊之处:
    • 相较于Java语言肯定是if表达式的括号没了
    • 除此之外更具特点的地方在于:Go语言规定与if匹配的左括号{必须与if和表达式放在同一行,{放在其他位置会触发编译错误。 同理,与else匹配的{也必须与else写在同一行,else也必须与上一个ifelse if右边的大括号在同一行

if的特殊结构

  • 在if表达式前加一个执行语句,然后根据变量值进行判断,执行语句与判断语句之间用分号;分割
1
2
3
4
5
6
7
8
9
10
11
func ifDemo2() {
if score := 65; score >= 90 {
fmt.Println("A")
} else if score > 75 {
fmt.Println("B")
} else {
fmt.Println("C")
}

// 跳出if判断后,就不能再访问score了
}
  • 与一般的if判断语句相比,其特殊之处在于执行语句中定义的变量的作用域

for循环

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
// 普通的for循环
func forDemo() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}

// 省略初始化的for循环
func forTest2() {
i := 0
// 注意初始化的分号不要省略
for ; i < 20; i++ {
fmt.Println(i)
}

fmt.Println()
}

// 只保留判断条件的for循环
// 这种写法不要忘记 更改循环条件,否则容易陷入死循环
func forTest3() {
i := 0
for i < 23 {
fmt.Println(i)
i++
}
}

func forTest4 () {
// 无限循环4
for {
循环体语句
}
}
  • 其实与Java也类似,就是初始化值的地方使用的是快速声明的方式,并且没有括号
  • for循环可以通过breakgotoreturnpanic语句强制退出循环

forRange循环

1
2
3
4
5
6
7
8
9
10
func forRangeTest() {
var s string = "李佳"

// 显然这里用到了一个多重赋值,range 关键字后直接加要遍历的数据结构
// 访问不同的数据结构会有不同的返回
// 在进行字符串的遍历中(实际上是rune数组的遍历),索引如果不关注的话就直接用匿名变量承接即可
for _, v := range s {
fmt.Printf("%c", v)
}
}
  • Go语言中可以使用for range遍历数组、切片、字符串、map 及通道(channel);range关键字根据后边的数据结构的不同会有不同的返回(有多重返回,也需要多重赋值)。 通过for range遍历的返回值有以下规律:

    1. 数组、切片、字符串返回索引和值。
    2. map返回键和值。
    3. 通道(channel)只返回通道内的值
  • 使用for-range循环是有坑的,即range关键字返回的value实际上是迭代的数据的副本

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 创建一个整型切片,并赋值
    slice := []int{10, 20, 30, 40}
    // 迭代每个元素,并显示值和地址
    for index, value := range slice {
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
    }

    /* 输出结果为:
    Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
    Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
    Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
    Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C
    */
    • 实际上就是在每一轮循环中,range函数将遍历的value的值赋值给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
      type student struct {
      name string
      age int
      }

      func main() {
      m := make(map[string]*student)
      stus := []student{
      {name: "小王子", age: 18},
      {name: "娜扎", age: 23},
      {name: "大王八", age: 9000},
      }

      for _, stu := range stus {
      m[stu.name] = &stu
      }
      for k, v := range m {
      fmt.Println(k, "=>", v.name)
      }
      }

      /*
      输出结果是
      小王子 => 大王八
      娜扎 => 大王八
      大王八 => 大王八
      */
      • 在为m的value进行填充值的时候,虽然经过了一轮遍历,但是实际上存储的都是变量stu的地址,而在经过三轮for循环不断的向stu变量赋新的值,直到最后一轮赋值为结构体{name: "大王八", age: 9000},此时跳出for循环后,虽然stu变量不可用,但是该内存地址被m中的value引用,因此再遍历时的值全是{name: "大王八", age: 9000}
        • 注意结构体的赋值是值赋值而不是引用赋值,与string、数组和基本类型类似,与切片、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
      type s struct {
      i int
      }

      func main() {
      m := make([]s, 0)
      m = append(m, s{i: 0})
      m = append(m, s{i: 1})
      m = append(m, s{i: 2})
      for _, e := range m { //e是值拷贝
      e.i = 3
      }
      for _, x := range m {
      fmt.Printf("%v\n", x.i)
      }
      }

      /*
      1
      2
      3
      */

      type s struct {
      i int
      }

      func main() {
      m := make([]s, 0)
      m = append(m, s{i: 0})
      m = append(m, s{i: 1})
      m = append(m, s{i: 2})
      for index := range m { //获取索引
      m[index].i = 3 //通过下标获取元素进行修改
      }
      for _, x := range m {
      fmt.Printf("%v\n", x.i)
      }
      }

      /*
      3
      3
      3
      */
      • 同理分析可得
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      type Map map[string]int

      func main() {
      slice := []Map{
      {"1": 1},
      {"2": 2},
      }

      for _, v := range slice {
      v["233"] = 233
      }

      for _, v := range slice {
      fmt.Println(v)
      }
      }
      /*
      map[1:1 233:233]
      map[2:2 233:233]
      */
      • 当存储的数据类型换成是引用类型时,其表现形似就完全不同了
    • for与range的底层实现

switch

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
// 普通的switch
func switchDemo() {
finger := 3

switch finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
default:
fmt.Println("无效的输入")
}
}

// switch语句使用类似if中的特殊结构
// switch表达式的内部定义的变量的作用域同样的被限制在switch语句内部
func switchDemo1() {
switch finger := 3; finger {
case 1:
fmt.Println("大拇指")
case 2:
fmt.Println("食指")
case 3:
fmt.Println("中指")
case 4:
fmt.Println("无名指")
case 5:
fmt.Println("小拇指")
// case分支可以枚举多个值,用逗号分隔
case 6,7,8:
fmt.Println("牛逼")
default:
fmt.Println("无效的输入")
}

// 无法访问finger变量
}

// 在switch-case的分支上使用判断表达式进行条件判断,此时switch
// 后边不用再跟变量
func switchDemo2() {
age := 30
switch {
case age < 10:
fmt.Println("幼年")
case age >=10 && age < 25:
fmt.Println("青年")
case age >= 25 && age <= 50:
fmt.Println("中年")
}
}

// fallthough语句的使用,就是可以执行满足条件的case的下一个case
// 下边的例子输出的是 a b
func fallthroughDemo() {
s := "a"
switch {
case s == "a":
fmt.Println("a")
fallthrough
case s == "b":
fmt.Println("b")
case s == "c":
fmt.Println("c")
default:
fmt.Println("...")
}
}
  • 与Java类似,不同的地方在于,switch的表达式不用括号,以及一些变种的形式

goto

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
// goto跳转到指定标签
func gotoDemo1() {
var breakFlag bool
for i:= 0; i<= 10; i++ {
for j := 0; j <=10; j++ {
if j == 2 {
breakFlag = true
break
}
fmt.Printf("%d<-->%d", i, j)
}

if breakFlag {
break
}
}
}

func gotoDemo2() {
for i:= 0; i<= 10; i++ {
for j := 0; j <=10; j++ {
if j == 2 {
goto breakTag
}
fmt.Printf("%d<-->%d", i, j)
}
}

breakTag:
fmt.Println("结束for循环")
}
  • 其实就是类似于C语言中的,代码之间的无条件快速跳转,goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。Go语言中使用goto语句能简化一些代码的实现过程。

break

1
2
3
4
5
6
7
8
9
10
11
12
13
func breakDemo() {
breakNode:
for i := 0; i < 10; i++ {
for j := 1; j < 10; j++ {
for k := 1; k < 10; k++ {
if i+j+k == 20 {
fmt.Println(i, j, k)
break breakNode
}
}
}
}
}
  • break语句可以结束forswitchselect的代码块
  • 可以认为go语言中的break是一个增强版的break,因为后边可以跟一个类似于goto那样的标签,可以从标签指定的forswitchselect的代码块上退出
    • 标签要求必须定义在对应的forswitchselect的代码块上
    • 这个特性可以让跳出多重循环变得更简单

continue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// continue标签的使用
func continueDemo() {
// continueNode:
for i := 0; i < 10; i++ {
for j := 1; j < 10; j++ {
continueNode:
for k := 1; k < 10; k++ {
if i+j+k == 20 {
fmt.Println(i, j, k)
continue continueNode
}
}
}
}
}
  • continue只能用在for循环
  • 类似于break,continue后边也能跟标签,当然此时标签只能定义在for循环上

数组

数组的定义

1
2
3
4
5
func arrayDemo() {
var array1 [23]int
var array2 [24]int
fmt.Println(array1, array2)
}
  • 与Java不同,但是也是类C语言体系的常规数组定义形式

  • 与Java中的数组类似,数组会进行默认初始化

  • 访问越界(下标在合法范围之外)时,会panic(Go中的异常机制)

  • 数组的长度肯定是常量,但是与Java中的数组最显著的不同在于:Go中的数组长度也是类型的一部分,并且Go中数组变量的赋值实际上是数组的复制,而不是指针的复制(与Java完全不同,这也就解释了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
    47
    48
    49
    func arrayDemo() {
    var a [3]int
    var b [4]int
    // [0 0 0] [0 0 0 0]
    fmt.Println(a, b)

    a = b // 无法完成赋值,因为类型不兼容

    }


    func arrayDemo1() {
    var a [3]int
    var b [3]int
    //[0 0 0] [0 0 0]
    fmt.Println(a, b)
    a = b
    a[0] = 233
    // [233 0 0] [0 0 0]
    fmt.Println(a, b)
    b[0] = 233
    // [233 0 0] [233 0 0]
    fmt.Println(a, b)
    }





    // 关于Go中的数组是值传递的一个形参的例子
    func modifyArray(x [3]int) {
    x[0] = 100
    }

    func modifyArray2(x [3][2]int) {
    x[2][0] = 100
    }
    func main() {
    a := [3]int{10, 20, 30}
    modifyArray(a) //在modify中修改的是a的副本x
    fmt.Println(a) //[10 20 30]
    b := [3][2]int{
    {1, 1},
    {1, 1},
    {1, 1},
    }
    modifyArray2(b) //在modify中修改的是b的副本x
    fmt.Println(b) //[[1 1] [1 1] [1 1]]
    }

    与之相比,Java中就完全不同,数组变量仅仅是一个指针而已,并且不会把长度也看做是类型的一部分

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int[] a = new int[3];
    int[] b = new int[4];
    // 指针的复制
    a = b;
    // 0 0 0 0
    System.out.println(Arrays.toString(b));
    a[0] = 233;
    // 233 0 0 0
    System.out.println(Arrays.toString(b));
    // 233 0 0 0
    System.out.println(Arrays.toString(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
func arrayDemo1() {
// 1. 只声明而没有初始化,或者说是默认初始化
var array [3]int

// 2. 使用指定的值完成初始化
// 直接进行类型推导,数组初始化值按照 类型{初始值} 的形式组织
var array1 = [3]int{1, 2, 3}

var array2 = [3]string{"我", "爱", "你"}

// 3. 方法2中实际引入了不必要的约束,编译器可以通过初始值的个数自动推断数组的长度,而不用显式的注明
// 此时数组长度位置可以留空(对应的是切片类型),也可以写...与Java语言类似的可变个数的意思
var array3 = [...]int{1, 2, 3}

//4. 使用指定值进行初始化的情况下,还可以指定特定索引位置的值,未指定的位置就是默认值,长度会自行推导
array4 := [...]int{1: 1, 3: 5}


fmt.Printf("type of array: %T \n", array)
fmt.Printf("type of array3: %T \n", array3)
fmt.Printf("type of array4: %T \n", array4)
fmt.Println(array, array1, array2, array3, array4, len(array4))

}
  • 注意数组长度省略时的...与直接省略时不一样的

    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
    func arrayDemo1() {

    // 直接空白-------切片类型
    array := []int{1,2,3}
    array1 := []int{1,2,3,4}

    fmt.Printf("type of array: %T \n", array)
    fmt.Printf("type of array1: %T \n", array1)

    // 不会报错,因为类型是一样的都是[] int
    // array直接进行对应的扩容或者缩容
    array = array1
    // [1 2 3 4]
    fmt.Println(array)

    // 使用...

    array2 := [...]int{1,2,3}
    array3 := [...]int{1,2,3,4}

    fmt.Printf("type of array2: %T \n", array2)
    fmt.Printf("type of array3: %T \n", array3)

    // 直接报错,因为...是可以进行长度推导的,而不是可以自行扩容的数组类型
    array2 = array3

    }
    • ...对应的数组类型是确定长度的数组类型,如果直接留空白的话,数组类型是不带长度的(实际上就是后边提到的切片类型),此时可以比较任意的进行赋值,因为长度已经不是限制因素了(会自动扩容缩容,并不会导致出现异常)

数组的遍历

  • 两种遍历方法,其实前边已经说过
    • 正常的索引遍历方式
    • for-range遍历
1
2
3
4
5
6
7
8
9
10
11
12
13
func arrayDemo1() {
array := [...]int{1, 2, 3, 4}

// 普通for循环
for i := 0; i < len(array); i++ {
fmt.Println(array[i])
}
// for-range
for _, v := range array {
fmt.Println(v)
}

}

二维数组

二维数组的定义

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
func arrayDemo1() {

// 二维数组定义时的初始值的最后一个逗号分隔符是必要的,不能省略的
array := [2][2]int{
{1,2},
{3,4},
}

fmt.Println(array)
fmt.Println(array[1][1])
fmt.Printf("type of array: %T \n", array)

// 使用数组的长度推测时注意,只有最外层的维度可以使用,内层不能使用长度推测,必须明确指明(数组类型)或者留空(切片类型)
array1 := [...][]int{
{1,2},
{3,4},
{5,6},
}

fmt.Println(array1)
fmt.Println(array1[1][1])
fmt.Printf("type of array1: %T \n", array1)


// 同样的如果留空的话(实际上就是切片类型),就长度作为类型一部分的限制,又因为Go中的数组赋值是值传递的,所以就会出现一个畸形的二维数组(应该说是不建议这样的)
array1[1] = []int{1,2,3,4}

// [[1 2] [1 2 3 4] [5 6]]
fmt.Println(array1)
}
  • 使用多行字面量的格式定义二维数组时,最后必须预留一个逗号分隔符,否则会报错,这是一种编译器提供的强制的编程习惯,如果写成一行的话,就不强制了,包括后边的数据类型定义中都是类似的

二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func arrayDemo1() {

// 使用数组的长度推测时注意,只有最外层的维度可以使用,内层不能使用长度推测,必须明确指明(数组类型)或者留空(切片类型)
array1 := [...][2]int{
{1, 2},
{3, 4},
{5, 6},
}

for _, v1 := range array1 {
for _, v2 := range v1 {
fmt.Printf("%d ", v2)
}
fmt.Println()
}
}

数组的注意事项

  1. [n]*T表示指针数组,*[n]T表示数组指针(后边指针会学到)

  2. 数组是值传递的(或者说数组类型不是引用类型)

    1. 关于数组不是引用类型的补充:相同类型的数组可以直接使用==!=进行比较,比较的方式应该就是逐项比较,有一个位置的数据不同就表示两个数组不同

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      func arrayDemo() {
      array := [...]int{1, 2, 3, 3}
      array1 := [...]int{1, 2, 3, 4}
      array2 := [...]int{1, 2, 3, 3}

      // false
      fmt.Println(array == array1)
      // true
      fmt.Println(array == array2)
      }

切片

  • 切片类型是从数组类型中引出的,因为数组长度作为数组类型的一部分这一特性导致Go中的数组应用有比较多的限制,并且数组本身也有长度固定无法扩展的问题,因此引入切片类型解决数组的缺陷
  • 从上边的数组学习中也能看出,切片类型与数组的使用方式是类似的,但是这是两个不兼容的类型
  • 切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容
    • 可以将Go中的切片某种程度上理解为Java中的ArrayList,同样封装原生的数组,实现可变长的数组
    • 切片类型是引用类型,而数组是值基本类型
  • 切片底层就是一个数组;切片的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合

切片的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func sliceDemo() {

// 声明,执行默认初始化--nil
var s []int

// 类型推导,做了初始化
var s1 = []int{1,2}

s2 := []int{3,4}

// 类似数组的指定索引位置的初始化
s3 := []int{1:3,4:3}

fmt.Println(s == nil) // true
fmt.Println(s1 == nil) // false
fmt.Println(s2) // [3,4]

// 切片类型之间的比较不能使用 == 或者说只能和nil进行比较
// fmt.Println(s1 == s2)

}
  • 可以发现切片的声明与初始化与数组是很类似的
  • 切片类型的变量因为是引用类型只能和常量nil做==比较判断,而不能和其他的切片类型变量做比较;而在数组中,可以直接用==做值(数组成员)的比较
    • ==运算符在Go中不用来判断引用类型的引用地址值是否相等,而只用来判断变量值是否相等,所以在这里不能用这个运算符

切片的长度和容量

1
2
3
4
5
6
7
8
9
10
11
func sliceDemo() {

s := []int{1:3,4:3}

fmt.Println(s)
// 切片的长度 -- 5
fmt.Println(len(s))
// 切片的容量 -- 5
fmt.Println(cap(s))

}
  • 切片拥有自己的长度容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量
  • 切片的长度和容量的区别是
    • 切片长度就是切片中实际的成员数量,容量就是切片的底层数组的容量(参考后边的切片底层的描述)

切片的本质

  • 切片的本质就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)

    举例说明:a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5]

    image-20210917084842304

    s2 := a[3:6]

    image-20210917085019954

  • 从示意图可以总结出,实际上切片就是独立于数组的单独的存储结构,维护了指向底层数组的指针,指针指向的就是数组上切片的开始位置索引,并以结束索引确定长度,容量的话就是从开始索引位置一直到底层数组的末尾

    • 使用切片表达式作用在字符串上时,实际以byte数组作为底层数组进行切片

    • 使用切片表达式作用在数组上时,直接以该数组作为底层数组

    • 使用切片表达式作用在切片上时,实际以目的切片的底层数组的特定切片范围作为底层数组(此后新的切片与之前的切片并无联系,只是用了同一个底层数组)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      func sliceDemo() {
      array := []int{1, 2, 3, 4, 5}

      s1 := array[:4]
      s2 := s1[1:3]

      // [1 2 3 4] [2 3]
      fmt.Println(s1, s2)

      array[2] = 334

      // [1 2 334 4] [2 334]
      fmt.Println(s1, s2)

      }
      • 可以从案例中发现,两个切片都依赖于一个底层数组,数组内容改变后,切片的内容也会相应的改变

切片的构造

  • 前边说的切片的定义初始化用的都是类似数组枚举的形式,其实更多的应该使用切片表达式或者是make函数的形式动态的构造切片

切片表达式

  • 需要注意的是,切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式
    • 切片表达式可以用来构造字符串或切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 一个入门的例子
func sliceDemo() {

// 使用切片表达式构造子字符串
s := "qwerkt"
// slic实际上是字符串类型
slic := s[1:6]
fmt.Println(slic)


// 强转为切片后,在切片的基础上使用切片表达式来创建新的切片
c := "我爱你"
// slic1是切片类型[]rune
slic1 := []rune(c)[0:2]

// 切片可以强转为字符串
fmt.Println(string (slic1)) // 我爱

// 切片遍历
for _,v := range slic1{
fmt.Printf("%c \n", v)
}

}

简单切片表达式

  • 切片表达式中的lowhigh表示一个索引范围(左包含,右不包含
  • 切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片
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
func sliceDemo() {

array := [...]int{1, 2, 3, 4}

// 由数组通过切片表达式构造切片
s := array[1:3]
// [0:3]
s1 := array[:3]
// [2:4]
s2 := array[2:]
// [0:4]
s3 := array[:]

// v: [2 3], type: []int, len: 2, cap: 3
fmt.Printf("v: %v, type: %T, len: %d, cap: %d \n", s, s, len(s), cap(s))
// v: [1 2 3], type: []int, len: 3, cap: 4
fmt.Printf("v: %v, type: %T, len: %d, cap: %d \n", s1, s1, len(s1), cap(s1))
// v: [3 4], type: []int, len: 2, cap: 2
fmt.Printf("v: %v, type: %T, len: %d, cap: %d \n", s2, s2, len(s2), cap(s2))
// v: [1 2 3 4], type: []int, len: 4, cap: 4
fmt.Printf("v: %v, type: %T, len: %d, cap: %d \n", s3, s3, len(s3), cap(s3))

}


// 本案例可结合切片的本质的图进行分析
func sliceDemo1() {
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]
// s:[2 3] len(s):2 cap(s):4
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
s2 := s[3:4] // 索引的上限是cap(s)而不是len(s)
// s2:[5] len(s2):1 cap(s2):1
fmt.Printf("s2:%v len(s2):%v cap(s2):%v\n", s2, len(s2), cap(s2))
}
  • 切片长度=high-low(应该就是实际中切片的成员数量),容量等于得到的切片的底层数组的容量
  • 省略了low则默认为0;省略了high则默认为切片操作数的长度(对于切片来说就是切片的容量)(low和high可以同时省略)
  • 切片表达式中的两个索引必须在合理范围内,对于数组和字符串就是0 <= low <= high <= len(a);对于切片来说(由切片构造子切片)就是0 <= low <= high <= cap(s)注意是切片的容量而不是长度)如果索引不在合理范围内就会触发panic

完整切片表达式

  • 注意一个前提条件,只有数组(或者指向数组的指针)和切片支持完整切片表达式(字符串不支持完整切片表达式)
  • 所谓的完整相对于简单来说就是多了一个max索引,也就是说切片的长度没有变,但是简单切片表达式中,切片的容量默认就是从low开始一直到底层数组的末尾,完整切片表达式中引入的第三个索引–max索引就是用来精确控制确定容量末尾的位置的
    • 0 <= low <= high <= max <= len(a)(对于切片来说就是0 <= low <= high <= max <= cap(a)
  • 在完整切片表达式中只有第一个索引值(low)可以省略;它默认为0
1
2
3
4
5
6
7
8
9
10
11
12
func sliceDemo() {

array := [...]int{1, 2, 3, 4, 5}

s := array[:2:4]
s1 := array[:2:5]
// s:[1 2] len(s):2 cap(s):4
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))
// s:[1 2] len(s):2 cap(s):5
fmt.Printf("s:%v len(s):%v cap(s):%v\n", s1, len(s1), cap(s1))

}
  • 结果切片的容量是max-low,也可以将max索引视作不包含的索引

make函数构造切片

  • 前边的切片的构造都是基于已有的字符串或者数组和切片构造的,如果凭空或者动态的创建切片就用make函数(应该在底层用创建数组并执行默认初始化的操作

  • make([]T, size, cap) 函数返回值就是对应类型的切片

    • T:切片的元素类型
    • size:切片中元素的数量(切片的长度)
    • cap:切片的容量(底层数组的长度)
    1
    2
    3
    4
    5
    6
    7
    func sliceDemo() {

    s := make([]int, 3, 4)
    // s:[0 0 0] len(s):3 cap(s):4
    fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))

    }
    • 因为切片的底层是数组所以会进行默认初始化

    • 显然应有cap >= size

    • 如果cap与size一样的话,只需要指定一个参数即可

      1
      2
      3
      4
      5
      6
      7
      func sliceDemo() {

      s := make([]int, 3)
      // s:[0 0 0] len(s):3 cap(s):3
      fmt.Printf("s:%v len(s):%v cap(s):%v\n", s, len(s), cap(s))

      }
  • 需要注意的是,make函数虽然没有提供自定义初始化值的功能,但是len范围内的数据都是有效的数据(即便是默认初始值),也就是说如果要打印的话,是可以打印出len以内的数据的,或者执行append的话,是从len+1开始添加的,如果只是做内存分配的话,len参数一般设置为0

  • 另外一个需要注意的是,使用make函数构造切片,如果设置的size为0的话,后续为切片添加值,将只能使用append方法,而不能使用中括号进行索引,如果使用中括号索引了size以外的index就会报错

    1
    2
    3
    4
    5
    var s []int = make([]int, 0, 30)
    func main(){
    // panic: runtime error: index out of range [0] with length 0
    fmt.Println(s[0])
    }
  • 下边指针章节会学到实际上makenew两个内置函数就是用来为引用类型的数据分配内存空间的

切片的遍历

  • 与数组的遍历是一样的

    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
    func sliceDemo() {

    s := []int{1, 2, 3, 4, 5}

    // 普通for循环
    for i:= 0; i < len(s); i++ {
    // 与数组类似的索引访问
    fmt.Println(s[i])
    }

    // for-range遍历
    for index, value := range s {
    fmt.Println(index, value)
    }

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

    s1 := s[:3]

    // 普通for循环
    for i:= 0; i < len(s1); i++ {
    // 与数组类似的索引访问
    fmt.Println(s1[i])
    }

    // for-range遍历
    for index, value := range s1 {
    fmt.Println(index, value)
    }
    }
    • 注意,切片的遍历只在len范围内遍历,而不是cap范围内

切片的扩容

  • 实际上指的就是使用append函数(如果切片底层的数组不够存放数据后,函数内部也可以按照一定的扩容策略执行数组自动的扩容)向切片添加数据
  • 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)
  • 切片能够进行扩容,就是切片区别于数组的最大特点(也是之所以使用切片的原因)
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
func appedDemo() {
var s []int
// true
fmt.Println(s == nil)

/*
* append函数可以直接对一个默认初始化(nil)的切片进行扩容
* 可以猜测其底层默认是没有指向任何数组的,执行append函数时,创建一个数组
* 并让切片s的指针指向该数组,再返回切片s的引用
*/
s = append(s, 1, 2, 3)
// slice value: [1 2 3], len: 3, cap: 3
fmt.Printf("slice value: %v, len: %d, cap: %d \n", s, len(s), cap(s))

s1 := []int{4, 5}
// 添加另一个切片的元素
s = append(s, s1...)

/*
* 之前底层数组的容量为3,已经全用完了,此时,再添加两个元素,肯定不够用,因此
* append的底层就会按照一定的扩容策略,创建新的数组,此时,切片的指针的指向就已经改变了
*/

// slice value: [1 2 3 4 5], len: 5, cap: 6
fmt.Printf("slice value: %v, len: %d, cap: %d \n", s, len(s), cap(s))

s = append(s, 6)

/*
* 只添加一个新的元素吗,此时数组容量是够用的,因此不执行扩容
*/

// slice value: [1 2 3 4 5 6], len: 6, cap: 6
fmt.Printf("slice value: %v, len: %d, cap: %d \n", s, len(s), cap(s))

}
  • 每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”, 此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值
    • 为什么说append函数的返回值一般都用原变量接收呢?因为切片是一个引用类型,如果发生扩容的话,会重新产生一个新的切片出来,需要变量去接收,并且如果没有扩容的话,如果用一个新的切片类型的变量接收了,那么原变量与这个新变量都能直接对底层数组进行值的修改,容易混乱
  • 注意不要混淆扩容与初次初始化,比如下边根据扩容策略可以看到对应的数组的长度,是按照2的幂次进行扩容的,但是如果定义的切片初始容量就是某非2的幂次也是合理的,只不过如果使用append添加数据时,如果要扩容还是会按照最近的2的幂次进行扩容

切片的扩容策略

  • 首先使用一个例子来展示下切片的扩容策略

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func appedDemo() {

    var s []int

    /*
    [0] len:1 cap:1 ptr:0xc000012070
    [0 1] len:2 cap:2 ptr:0xc0000120a0
    [0 1 2] len:3 cap:4 ptr:0xc00001e060
    [0 1 2 3] len:4 cap:4 ptr:0xc00001e060
    [0 1 2 3 4] len:5 cap:8 ptr:0xc00001c100
    [0 1 2 3 4 5] len:6 cap:8 ptr:0xc00001c100
    [0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc00001c100
    [0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc00001c100
    [0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc000018100
    [0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc000018100
    */
    for i := 0; i < 10; i++ {
    s = append(s, i)
    fmt.Printf("%v len:%d cap:%d ptr:%p\n", s, len(s), cap(s), s)
    }

    }
    • 容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍
    • 可以发现append函数的返回的切片的指针在扩容时会改变(创建了新的切片),未扩容时不会改变(未创建新的切片)———-也就是说不是直觉上认为的,扩容只是底层数组的改变,切片只不过指向新的数组而已,事实上扩容时不仅仅重新分配数组,也会重新分配切片(这里实际上有误区,完全可以将切片的地址看做是数组的地址)
  • 源码可查看$GOROOT/src/runtime/slice.go

切片的复制

  • 所谓的切片的复制就是因为切片本身是一个引用类型,直接赋值的话,实际上是引用的复制,指向的还是同一块内存;可以使用copy函数可以完成值的复制``

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    func copyDemo() {
    srcSlice := []int{1, 2, 3, 4}

    destSlice := make([]int, 2)

    destSlice1 := make([]int, 7)

    destSlice2 := []int{6, 6, 6, 6, 6, 6}

    copy(destSlice, srcSlice)
    copy(destSlice1, srcSlice)
    copy(destSlice2, srcSlice)

    // [1 2]
    fmt.Println(destSlice)
    // [1 2 3 4 0 0 0]
    fmt.Println(destSlice1)
    // [1 2 3 4 6 6]
    fmt.Println(destSlice2)

    }
    • 使用copy函数时不用担心目的切片空间不够,空间有多少放多少即可,或者说如果目的空间很大,那就剩下的维持原值即可

切片删除

  • 切片没有原生的用来删除数据的API,只能使用append函数做曲线救国:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func deleteDemo() {
    s := []int{1, 2, 3, 4}

    // 0xc0000b4000----[1 2 3 4]
    fmt.Printf("%p----%v \n", s, s)

    // 删除索引位置2的数据
    s = append(s[:2], s[3:]...)

    // 0xc0000b4000----[1 2 4]
    fmt.Printf("%p----%v \n", s, s)

    }
    • 本来担心,如果删除使用append不会导致没必要的数组扩容吗,实际上并不会:append的目标切片是原切片的一个二次切片,底层数组是同一个数组,又因为是做删除,所以数组空间是绝对够的,不会触发无所谓的扩容

关于切片的注意事项

  1. 切片类型的变量因为是引用类型只能和常量nil做==比较判断,而不能和其他的切片类型变量做比较

    • ==运算符在Go中不用来判断引用类型的引用地址值是否相等,而只用来判断变量值是否相等,所以在这里不能用这个运算符

    • 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil

      1
      2
      3
      var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
      s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
      s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
      • 所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断
  2. 不像数组,数组的赋值是完全的数值拷贝,互不影响,而切片的赋值是引用传递,会相互影响

    1
    2
    3
    4
    5
    6
    7
    func main() {
    s1 := make([]int, 3) //[0 0 0]
    s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组
    s2[0] = 100
    fmt.Println(s1) //[100 0 0]
    fmt.Println(s2) //[100 0 0]
    }
  3. 在打印或者遍历切片时,只会遍历或者打印len范围内的数据,而不是cap范围内的数据,因为len内的是有效数据,而len到cap范围内的数据仅仅是默认初始化的数据,没有意义

  4. 切片的地址对应的就是其底层数组的“开始”地址

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func main() {

    // 通过以数组构建切片的方式探究切片与底层数组的关系,make函数会在底层构建数组,因此不好研究


    array := [...]int{1,3,4,5}

    // 数组的地址 0xc0000b4000
    fmt.Printf("%p \n", &array)

    s := array[:3]
    // 切片地址:0xc0000b4000
    fmt.Printf("%p \n", s)

    s1 := array[2:4]
    // 切片地址:0xc0000b4010 正好差两个int元素
    fmt.Printf("%p \n", s1)
    }
    • image-20210917084842304

      image-20210917085019954

      • 图与数据未必一致,但是可以辅助理解
    • 类似的也可以解释前边扩容的部分,为什么数组的扩容与切片地址的改变是同步的,因为不需要扩容时,切片的指针也是不变的,但是一旦需要扩容时,就需要创建新的数组,切片的指针也需要指向新的数组

    • 切片由三部分构成,地址(可以理解为切片的地址,也等同于其对应的底层数组的地址),长度,容量;三者有一个不一样都是不同的切片,不是唯地址论的,参考下边的例子

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      func main() {
      type Map map[string][]int
      m := make(Map)
      s := []int{1, 2}
      s = append(s, 3)
      // [1 2 3]
      fmt.Printf("%+v\n", s)
      m["q1mi"] = s
      s = append(s[:1], s[2:]...)
      // [1 3]
      fmt.Printf("%+v\n", s)
      // [1 3 3]
      fmt.Printf("%+v\n", m["q1mi"])
      }
      • type关键词实际上就是自定义类型,或者说是指定一个类型的代称,在C系语言中也是比较常见的

      • 注意一下几点

        • 切片是引用类型
        • 切片时由地址、长度,容量三个要素决定的,在执行完s = append(s[:1], s[2:]...)语句后,m["q1mi"]s就是连个不同的切片了,尽管地址(对应同一个数组)一样,但是长度和容量不同

        image-20210920171853175

Map

  • 无序的键值对容器、引用类型、其内部使用散列表实现

map的定义

  • 与切片类似,map的定义也使用make函数,或者说切片与map都使用make函数来分配内存

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func mapDemo() {
    // map[key type]value type
    // 第二个参数可以不指定,但是建议指定
    scoreMap := make(map[string]int, 4)

    scoreMap["li"] = 12
    scoreMap["li"] = 45
    scoreMap["jj"] = 34
    // map是可以进行自动扩容的
    scoreMap["aa"] = 89
    scoreMap["dd"] = 90

    // 45 显然Go中的map的key也是唯一的
    fmt.Println(scoreMap["li"])
    // // map[aa:89 dd:90 jj:34 li:45]
    fmt.Println(scoreMap)
    // map[string]int
    fmt.Printf("%T \n", scoreMap)
    }
    • map类型的变量默认初始值为nil,需要使用make()函数来分配内存
    • make函数的第二个参数可以不指定(自动扩容),但是建议指定恰当的容量,应该与Java类似于底层的散列表的容量以及扩容有关
    • map添加键值对的方式就是直接使用方括号即可
  • 除了使用make函数进行初始化之外,也可以使用字面量进行初始化

    1
    2
    3
    4
    5
    6
    userInfo := map[string]string{
    "name": "li",
    "age": "12",
    }

    fmt.Println(userInfo)
    • 实际上可以发现,直接打印map时,会按照对key排序的顺序进行打印,只是打印有序,但是默认的for-range遍历是无序的,不要搞混了
    • 使用字面量初始化map后,默认为map分配了内存空间

map的遍历

  • for-range遍历

    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
    func mapDemo() {
    scoreMap := make(map[string]int, 2)

    scoreMap["li"] = 12
    scoreMap["aa"] = 89
    scoreMap["dd"] = 90
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["娜扎"] = 60

    for k, v := range scoreMap {
    fmt.Println(k, v)
    }

    // for _, v := range scoreMap {
    // fmt.Println(v)
    // }

    // for k, _ := range scoreMap {
    // fmt.Println(k)
    // }

    for k := range scoreMap {
    fmt.Println(k)
    }
    }
    • for-range遍历的顺序与插入的顺序无关,如果恰好一样,就只是恰好而已,多运行几次,就会发现并不一致
    • 如果遍历过程中只想获得key或者value,当然可以使用匿名变量,在只想获取key时可以直接只用一个变量接收即可
  • 按照指定顺序进行遍历

    • 实际上也是曲线救国的方式,毕竟map内部存储实际上是无序的:将map的key存储到切片中,然后对切片进行排序,最后使用排序后的切片的顺序来遍历map即可

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      func mapDemo() {
      scoreMap := make(map[string]int, 2)

      scoreMap["li"] = 12
      scoreMap["aa"] = 89
      scoreMap["dd"] = 90
      scoreMap["张三"] = 90
      scoreMap["小明"] = 100
      scoreMap["娜扎"] = 60

      slice := make([]string, 0, len(scoreMap))

      for k := range scoreMap {
      slice = append(slice, k)
      }

      sort.Strings(slice)

      for _,s := range slice {
      fmt.Println(s, scoreMap[s])
      }

      }
      • len函数可以得到map键值对的数量

map的常用操作

删除键值对

  • 使用delete函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func mapDemo() {
    scoreMap := make(map[string]int, 2)

    scoreMap["li"] = 12
    scoreMap["aa"] = 89
    scoreMap["dd"] = 90
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["娜扎"] = 60

    for k := range scoreMap {
    fmt.Println(k)
    }

    fmt.Println()
    delete(scoreMap, "dd")

    for k := range scoreMap {
    fmt.Println(k)
    }

    }

判断某键值对是否存在

  • Go中并没有提供判断键值对是否存在的API,而是提供了一种特殊的语法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    func mapDemo() {
    scoreMap := make(map[string]int, 2)

    scoreMap["li"] = 12
    scoreMap["aa"] = 89
    scoreMap["dd"] = 90
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["娜扎"] = 60

    score := scoreMap["jjk"]
    // 0 ----value的类型 int的默认值
    fmt.Println(score)

    // 在索引目的key的同时,获取其是否存在的信息
    score1, hasKey := scoreMap["jjk"]

    if hasKey {
    fmt.Println(score1)
    } else {
    fmt.Println("不存在")
    }

    }

map与slice的组合使用

  • 值为map类型的切片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    func main() {
    // 切片的元素类型是map
    var mapSlice = make([]map[string]string, 3)
    for index, value := range mapSlice {
    fmt.Printf("index:%d value:%v\n", index, value)
    }
    fmt.Println("after init")
    // 对切片中的map元素进行初始化
    mapSlice[0] = make(map[string]string, 10)
    mapSlice[0]["name"] = "小王子"
    mapSlice[0]["password"] = "123456"
    mapSlice[0]["address"] = "沙河"
    for index, value := range mapSlice {
    fmt.Printf("index:%d value:%v\n", index, value)
    }
    }
    • 在Java中都没怎么用过这种数据构造,可能还是见识少

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public static void main(String[] args) {
      Map<String, String>[] array = new Map[3];

      array[0] = new HashMap<>();
      array[0].put("name", "李佳");
      array[0].put("age", "12");
      array[1] = new HashMap(){
      {
      put("name", "宋丹丹");
      put("age", "13");
      }
      };
      // [{name=李佳, age=12}, {name=宋丹丹, age=13}, null]
      System.out.println(Arrays.toString(array));
      }
  • 值为切片类型的map

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    func main() {
    var sliceMap = make(map[string][]string, 3)
    fmt.Println(sliceMap)
    fmt.Println("after init")
    key := "中国"
    value, ok := sliceMap[key]
    if !ok {
    value = make([]string, 0, 2)
    }
    value = append(value, "北京", "上海")
    sliceMap[key] = value
    fmt.Println(sliceMap)
    }
    • 同样附上Java中的实现

      1
      2
      3
      4
      5
      6
      7
      8
      public static void main(String[] args) {

      Map<String, String[]> map = new HashMap<>();
      map.put("China", new String[]{"北京", "上海"});
      map.put("America", new String[]{"Washington", "New York"});

      System.out.println(map);
      }

更自由的map

  • 参考后边的空接口实现的可以存储任意类型的map

函数

普通函数

  • 与Java相比,结构上的区别大致了解,更重要的区别在于,Go中的函数甚至可以有多个返回值(如果有多个,就用括号包裹,并用逗号分隔)

    1
    2
    3
    func 函数名(参数)(返回值){
    函数体
    }
  • 函数的参数与返回值都是可选的

参数

参数类型简写

  • 函数的参数中如果相邻变量的类型相同,则可以省略前边的参数的类型

    1
    2
    3
    4
    // a b c三个参数的类型都是int
    func functionDemo(a, b , c int, d string) {

    }

可变参数

  • 与Java的类似,也是使用...可变参数都要放在参数表中的最后一个,只是具体的写法不太一样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    func main() {
    functionDemo(1,2,3,4,5,6)
    }

    func functionDemo(a, b int, c ...int) {
    for _,v := range c{
    fmt.Println(v)
    }
    }
    • 可变参数按照切片处理,Java中是按照数组来处理

返回值

多返回值

1
2
3
4
5
6
7
8
9
func functionDemo(a, b int, c ...int) (int, int) {
return 1, 2
}

func main() {
a,b := functionDemo(1, 2, 3, 4, 5, 6)

fmt.Println(a,b)
}

返回值预定义(命名)

1
2
3
4
5
6
7
8
9
10
11
12
func functionDemo1(a, b int, c ...int) (res, res1 int) {
res = 1
res1 = 2
return
}

func main() {

c,d := functionDemo1(1, 2, 3, 4, 5, 6)

fmt.Println(c,d)
}
  • 返回值预定义就是在函数声明的返回值处直接声明返回值变量(同样满足函数参数中的简写原则),在函数体中可以直接使用,并且在返回值变量得到赋值后,直接一个return语句即可将返回值返回
  • 注意即便是仅有一个返回值,在使用预定义时也要用括号包裹

变量作用域

全局变量

  • 全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量

局部变量

函数内部定义变量

  • 函数内定义的变量无法在该函数外使用
  • 如果局部变量和全局变量重名,优先访问局部变量

块作用域变量

  • if、for、switch等流程控制代码块内或者是一个自定义的代码块内定义的变量仅在代码块内部生效

包作用域

  • 在同一个包内,函数名不能重复

函数式编程

  • 所谓函数式编程,在形式上即可以将函数作为一种特殊类型,并且可以将函数作为一个整体进行赋值、作为参数或返回值

函数类型与函数类型的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
// 定义一个函数类型,类似于一个接口
type calculation func(int, int) int

// 使用函数赋值
var f calculation = sum
// 类型推测同样可用
f1 := sub
// 3 1
fmt.Println(f(1, 2), f1(3, 2))

}

func sum(a, b int) int {
return a + b
}

func sub(a, b int) int {
return a - b
}
  • 函数类型类似于一个接口,使用type关键字定义,type关键字不仅仅可以定义函数类型,还可以定义其他任意类型,其实就是给复杂类型起一个别名
  • 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
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
type cal func(int, int) int

func main() {
// 3
fmt.Println(calc(1, 2, sum))
// -1
fmt.Println(calc(1, 2, sub))


f, err := do("+")

if(err == nil) {
fmt.Println(f(1,2))
}

}

/*
* 函数作为参数
*/

// func calc(x, y int, op func(int, int) int) int {
// return op(x, y)
// }

func calc(x, y int, op cal) int {
return op(x, y)
}

func sum(a, b int) int {
return a + b
}

func sub(a, b int) int {
return a - b
}


/*
* 函数作为返回值
*/

func do(op string) (func(int, int) int, error) {
switch op {
case "+":
return sum, nil
case "-":
return sub, nil
default:
err := errors.New("无法识别的操作符")
return nil, err
}
}

匿名函数

  • 前边的将函数作为参数或者返回值,本质上是函数作为特殊类型的值进行传递,但是函数都是彼此单独定义的,如果要在函数内部定义函数就要使用匿名函数,而不能使用一般的函数定义形式

    • 匿名函数就是普通的函数定义形式不包括函数名部分而已
    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
    type cal func(int, int) int

    func main() {

    add := func(a, b int) int {
    return a + b
    }

    fmt.Println(add(1, 2))

    var sub cal = func(i1, i2 int) int {
    return i1 - i2
    }

    fmt.Println(sub(3, 2))



    // 匿名函数的自执行
    c := func (a, b int) int {
    return a * b
    }(2,3)

    fmt.Println(c)

    }


    • 匿名函数有一个有趣的特点,就是自执行,这一块与JavaScript类似
    • 匿名函数的定义时一次性的,但是其执行是简洁高效的

闭包

  • 闭包应该是函数式编程中的一个特色,在JavaScript的学习中也遇到过

  • 对于闭包的理解其实就是闭包 = 函数 + 调用环境,其出现的场景一般就是匿名函数作为返回值,导致函数的定义环境与调用环境不同,此时分析函数内部的作用机制,要放到其定义的环境中分析(在函数变量的声明周期内)

  • 通过下边的4个例子理解闭包

    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
    func main() {
    var f = adder()
    // x = 10
    fmt.Println(f(10)) //10
    // x = 10 + 20
    fmt.Println(f(20)) //30
    // x = 30 + 30
    fmt.Println(f(30)) //60

    // 新的函数生命周期
    f1 := adder()
    // x = 40
    fmt.Println(f1(40)) //40
    // x = 40 + 50
    fmt.Println(f1(50)) //90
    }

    func adder() func(int) int {
    var x int

    // 匿名函数内部使用了域外的变量
    return func(i int) int {
    x += i
    return x
    }
    }
    • f就是一个闭包。 在f的生命周期内,变量x也一直有效
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    func adder2(x int) func(int) int {
    return func(y int) int {
    x += y
    return x
    }
    }
    func main() {
    var f = adder2(10)

    // x = 10 + 10
    fmt.Println(f(10)) //20
    // x = 20 + 20
    fmt.Println(f(20)) //40
    // x = 40 + 30
    fmt.Println(f(30)) //70

    // 新的函数生命周期
    f1 := adder2(20)
    fmt.Println(f1(40)) //60
    fmt.Println(f1(50)) //110
    }
    • 与上一个例子本质上类似,只不过从函数内定义的变量变成了函数参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func makeSuffixFunc(suffix string) func(string) string {
    return func(name string) string {
    if !strings.HasSuffix(name, suffix) {
    return name + suffix
    }
    return name
    }
    }

    func main() {
    jpgFunc := makeSuffixFunc(".jpg")
    txtFunc := makeSuffixFunc(".txt")
    fmt.Println(jpgFunc("test")) //test.jpg
    fmt.Println(txtFunc("test")) //test.txt
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func calc(base int) (func(int) int, func(int) int) {
    add := func(i int) int {
    base += i
    return base
    }

    sub := func(i int) int {
    base -= i
    return base
    }
    return add, sub
    }

    func main() {
    f1, f2 := calc(10)
    // base = 10 + 1 base = 11 - 2
    fmt.Println(f1(1), f2(2)) //11 9
    // base = 9 + 3 base = 12 - 4
    fmt.Println(f1(3), f2(4)) //12 8
    // base = 8 + 5 base = 13 - 6
    fmt.Println(f1(5), f2(6)) //13 7
    }

闭包的使用场景

  • 通过闭包实现一个生成器,实现一定程度的数据隔离,看下边的例子

    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"

    // 定义一个玩家生成器,它的返回类型为 func() (string, int),输入名称,返回新的玩家数据
    func genPlayer(name string) func() (string, int) {
    // 定义玩家血量
    hp := 1000
    // 返回闭包
    return func() (string, int) {
    // 引用了外部的 hp 变量与name变量, 形成了闭包
    return name, hp
    }
    }

    func main() {
    // 创建一个玩家生成器
    generator := genPlayer("犬小哈")

    // 返回新创建玩家的姓名, 血量
    name, hp := generator()

    // 打印
    fmt.Println(name, hp)
    }
    • 闭包具有面向对象语言的特性 —— 封装性,变量 hp 无法从外部直接访问和修改
  • sync.Once的任务函数需要传递函数参数

    1
    func (o *Once) Do(f func()) {}
    • Do函数本身只接受无参函数,此时使用闭包可以从函数定义的位置向函数传递参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import (
    "fmt"
    "sync"
    )

    var once sync.Once

    func OnceTask(i int) func() {
    return func() {
    fmt.Println(i)
    }
    }

    func main() {
    once.Do(OnceTask(12))
    }
  • defer语句指定的函数有error抛出时,无法在单个defer语句中完成error的处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    func process(conn net.Conn){
    // defer conn.Close()
    defer func(conn net.Conn) {
    err := conn.Close()
    if err != nil {
    fmt.Println("Something error when close conn")
    }
    }(conn) // 关闭连接
    }

    // 但是暂时不清楚与下边直接使用变量的形式有什么区别
    func process(conn net.Conn) {
    // defer conn.Close()
    defer func() {
    err := conn.Close()
    if err != nil {
    fmt.Println("Something error when close conn")
    }
    }() // 关闭连接
    }

defer语句

  • 所谓的defer语句就是defer关键字后跟一个语句(实际上应该是跟一个函数执行语句,常常是匿名函数的自调用)defer关键字的作用在于对关键字之后的语句进行延迟处理,defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行
  • 由于defer语句延迟调用的特性,所以defer语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等

defer执行时机

  • 在Go语言的函数中return语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后,RET指令执行前

    image-20210922120121719

    • 图中的函数指的是defer关键字在的函数,defer关键字从属的函数在返回之前,需要按照一定的顺序(先注册的defer语句最后被执行,最后被注册的defer语句最先被执行)执行defer语句

defer使用案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* start
* 3
* 2
* 1
* end
*/
func main() {
fmt.Println("start")
defer fmt.Println(1)
defer fmt.Println(2)
defer fmt.Println(3)
fmt.Println("end")
}
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
// 未指定返回参数,可以认为返回值应是一个临时变量,执行return x 时实际执行的是: temp := x x++ return temp所以defer语句的执行
// 并不影响返回值 因此返回5
func f1() int {
x := 5
defer func() {
x++
}()
return x
}

// 指定了返回值为变量x 因此return 5 实际执行为 x = 5 x++ return x 因此返回6
func f2() (x int) {
defer func() {
// --5--
fmt.Printf("--%d-- \n", x)
x++
}()
return 5
}

// 指定返回变量为y return x 实际执行的是 y = x x++ return y 因此返回5
func f3() (y int) {
x := 5
defer func() {
x++
}()
return x
}

// 本用例是最有迷惑性的,首先因为定义了返回值参数,因此return 5实际执行的是 x = 5 匿名函数执行 return x
// 匿名函数的执行,并没有影响,其一是匿名函数自执行的参数x必须在defer注册时就确定也就是0,因此匿名函数执行是参数为0
// 其二 也是更重要的就是 匿名函数执行的参数只不过是参数名叫x而已,是定义在匿名函数内部的局部变量,此时只是做了值传递,不会对函数外部的
// 返回值变量 x有影响
func f4() (x int) {
defer func(x int) {
// --0--
fmt.Printf("--%d-- \n", x)
x++
}(x)
return 5
}

func main() {
// 5
fmt.Println(f1())
// 6
fmt.Println(f2())
// 5
fmt.Println(f3())
// 5
fmt.Println(f4())
}
  • 结合上边的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
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}

/*
A 1 2 3
B 10 2 12
BB 10 12 22
AA 1 3 4
*/
func main() {
x := 1
y := 2
// 1. 首先执行参数中的calc("A", x, y) 输出 A 1 2 3 然后注册defer 语句 calc("AA", 1, 3)
defer calc("AA", x, calc("A", x, y))
x = 10
// 2. 执行calc("B", x, y) 输出 B 10 2 12 然后注册defer语句 calc("BB", 10, 12)
defer calc("BB", x, calc("B", x, y))
y = 20
// 3. 按照次序执行defer语句
// 4. calc("BB", 10, 12) 输出 BB 10 12 22
// 5. calc("AA", 1, 3) 输出 AA 1 3 4
}
  • defer注册要延迟执行的函数时该函数所有的参数都需要确定其值
    • 确定的值,是注册时的上下文的值,然后保持不变,不受后来的参数变量的值的变化的影响

常用的内置函数

image-20210922141837979

  • 这里针对学习panicrecover两个内置函数

Go中的异常处理

  • Go语言中目前(Go1.17)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效
    • defer一定要在可能引发panic的语句之前定义
    • 本质上是借用了defer语句会在return指令之前执行的特点,因为调用panic后会直接程序异常,即执行程序结束,此时就会去执行defer语句注册的异常处理函数,在异常处理函数中使用recover就可以恢复崩溃的函数,使得程序可以继续执行下去(对于入口函数main函数使用recover不会生效,因为已经是最顶层的函数了)
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
func funcA() {
fmt.Println("func A")
}

func funcB() {
panic("panic in B")
}

func funcC() {
fmt.Println("func C")
}

/*
func A
panic: panic in B

goroutine 1 [running]:
main.funcB(...)
.../code/func/main.go:12
main.main()
.../code/func/main.go:20 +0x98
*/
func main() {
funcA()
funcB()
// funcB panic后 funcC不再执行
funcC()
}



func funcA() {
fmt.Println("func A")
}

func funcB() {
defer func() {
err := recover()
// panic in B
fmt.Println(err)
//如果程序出出现了panic错误,可以通过recover恢复过来
// 这里需要判断的原因在于,panic未必执行,也就是为可能有异常而已,如果执行defer时,之前没有panic报异常,则recover函数会返回nil
if err != nil {
fmt.Println("recover in B")
}
}()
panic("panic in B")
}

func funcC() {
fmt.Println("func C")
}
func main() {
funcA()
funcB()
// funcB recover之后 funcC继续执行
funcC()
}

指针

  • Go中的指针介于Java(不提供指针,但是可以通过Unsafe获取操作内存空间的能力)和C/C++(提供全功能的指针)之间,Go语言中的指针不能进行偏移和运算(只读的),是安全指针
  • 在Go中使用指针,只需要&*两个运算符即可

指针地址与指针类型

指针地址

  • &取地址,直接放在变量前边即可,Go语言中的值类型(int、float、bool、string、array、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
func main() {

array := [...]int{1, 2, 3}

s := array[:3]

m := make(map[int]int)
m[1] = 2

i := 3

ptr := &m
ptr1 := &i
ptr2 := &array
ptr3 := &s

// map array等复杂数据类型的指针不能直接打印出来,需要使用%p打印
fmt.Println(ptr) // &map[1:2]
fmt.Println(ptr1) // 0xc000128008
fmt.Println(ptr2) // &[1 2 3]
fmt.Println(ptr3) // &[1 2 3]

fmt.Printf("%p \n", ptr) // 0xc000122018 map类型的变量m的地址
fmt.Printf("%p \n", ptr1) // 0xc000128008 int变量的地址
fmt.Printf("%p \n", ptr2) // 0xc00012c000 array地址
fmt.Printf("%p \n", s) // 切片类型的变量直接打印地址值与数组地址一样 0xc00012c000
fmt.Printf("%p \n", ptr3) // 0xc00011c018 切片类型变量s的 存储地址

fmt.Printf("%p \n", m) // map的地址 可以直接输出地址
fmt.Printf("%p \n", array) // 数组也不能直接按照%p输出地址 %!p([3]int=[1 2 3])
fmt.Printf("%p \n", i) // int变量不能直接按照%p输出地址 %!p(int=3)

}

指针类型

  • 指针类型也可以视作是值类型,也就是说同样可以使用&关键字获取指针类型变量的地址
1
2
3
4
5
6
7
8
9
func main() {
a := 12
b := &a

fmt.Printf("a:%d ptr:%p\n", a, &a) // a:12 ptr:0xc0000b0008
fmt.Printf("b:%p type:%T\n", b, b) // type:*int
fmt.Println(&b) // 0xc0000aa018

}
  • 注意指针类型的书写方式* T,比如* int等等

指针取值

  • * 根据地址取出内存中对应的值,与&是互补操作

  • 变量、指针地址、指针变量、取地址、取值的相互关系和特性如下(很久没有使用过指针后,使用这两个操作符很容易混淆):

    • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
    • 指针变量的值是指针地址。
    • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
  • 一个常见的例子

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    func modify1(x int) {
    x = 100
    }

    func modify2(x *int) {
    *x = 100
    }

    func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100

    b := 11
    &a = &b // cannot assign to &a
    }
    • Go中的指针是安全指针,也就是说指针类型变量的值可以修改,但是变量的指针也就是变量的地址值是不能修改的,表现在代码上就是main函数中的&a是不可写的,只可读的
    • Go中的函数参数传递实际上也是值传递,对于值数据类型就是值复制,对于引用类型就是引用(地址)的复制,对于指针变量来说,自然也是值传递,与上边说的地址不可更改不同的是,地址对应的内容显然是可以更改的,因此又*x = 100的操作更改了变量a的值

new与make

  • 这两个函数都是用来为引用类型的变量分配内存空间的,值类型的变量不用显式的分配内存空间,声明后会自动分配,但是,对于引用类型的变量必须显式的分配(当然不仅限于使用这两个函数,字面量初始化或者比如slice中使用切片表达式构建都会分配对应的内存空间)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    func pointer () {
    var a *int
    fmt.Println(a) // nil
    *a = 100 // 给nil地址值赋值,显然会触发panic panic: runtime error: invalid memory address or nil pointer dereference
    fmt.Println(*a)
    }

    func pointer1() {
    var b map[string]int
    fmt.Println(b) // map[] 只声明但是未分配内存
    fmt.Println(b == nil) //true 引用类型的变量的零值为nil
    b["沙河娜扎"] = 100 // panic: assignment to entry in nil map
    fmt.Println(b)
    }
    • 对于指针类型的变量,倾向于认为是值类型的变量(但是在书本教程中都认为是引用类型,总之能理解就好),但是引用类型的变量本质和指针类型的变量类似,指针类型的变量的默认零值是nil

new

  • func new(Type) *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
    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
    type Map map[int]int 

    func main() {
    newDemo()
    }

    func newDemo() {

    /*
    * 指针变量获取合法值
    */

    var a *int

    a = new(int)

    *a = 10

    fmt.Println(*a) // 10


    /*
    * 值类型的内存初始化
    */
    ptr := new(int)

    fmt.Println(ptr) // 0xc0000ba008

    fmt.Println(*ptr) // 0

    *ptr = 12

    fmt.Println(*ptr) // 12

    // 数组类型可以正常使用new分配内存
    arrayPtr := new([3]int)

    (*arrayPtr)[0] = 1
    // 1 0 0
    fmt.Println(*arrayPtr)


    /*
    * 引用类型的内存初始化
    */

    m := new(Map)
    fmt.Println(m == nil) // false
    (*m)[12] = 24 // panic: assignment to entry in nil map


    // 直接字面量声明初始化
    m1 := Map{
    1:1,
    2:2,
    }

    mPtr := &m1

    // 比较花哨的使用map类型指针的方式
    (*mPtr)[12] = 12

    // map[1:1 2:2 12:12]
    fmt.Println(m1)


    /*
    * 切片类型的内存初始化
    */
    slicePtr := new([]int)

    fmt.Println(slicePtr == nil) // false

    fmt.Printf("len: %d, cap: %d, value: %v \n", len(*slicePtr), cap(*slicePtr), *slicePtr) // len: 0, cap: 0, value: []

    var slice []int = *slicePtr

    // 必须先执行扩容
    slice = append(slice, 2)
    slice[0] = 1

    /*
    * 这里再次遇到slice的认知问题:不能仅仅靠指针来判断是否是同一个切片,即便指针相同也只能表示切片的底层数组相同,而切片的另外两个要素:len与cap未必相同,就导致两个切片未必是相同的切片
    */

    fmt.Printf("len: %d, cap: %d, value: %v \n", len(*slicePtr), cap(*slicePtr), *slicePtr) // len: 0, cap: 0, value: []
    fmt.Printf("len: %d, cap: %d, value: %v \n", len(slice), cap(slice), slice) // len: 1, cap: 1, value: [1]

    }
    • 从上边的案例中可以看到,使用new函数也可以初始化引用类型的内存空间,但是用起来真的很麻烦(相当于返回了一个二级指针供我们使用),对于slice、map以及channel的内存创建的话,直接使用下边的make函数吧
      • 在初始化切片时,注意必须先执行扩容,因为默认的new创建的内存空间对应的数组的长度为0
      • 在初始化map时,发现内存分配成功了(不为nil)但是放键值对失败了,究其原因,猜测仍然是其内部的哈希数组的长度为默认的0,并且没有提供扩容的API,所以暂时不能使用(直接用make不香吗)

make

  • func make(t Type, size ...IntegerType) Type make也是用于内存分配的,区别于new,它只用于slice、map以及channrl的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

make与new的区别

  • 二者都是用来做内存分配的
  • make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身
    • new函数也可以用来初始化这三个,但是用起来很麻烦,并且有一定的问题,参考上边的描述
  • 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

结构体

  • Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性

自定义类型与类型别名

  • 自定义类型

    1
    type Map map[int]int
    • 使用type关键字定义一个全新的类型Map,该类型具有map[int]int类型的特性
    • 在同一个包内不可重复
  • 类型别名 Go1.9版本添加的新功能,本质上是同一个类型,只不过有一个新的名字罢了

    1
    type MyInt = int
    • 同样使用type关键字,格式与自定义类型很类似,但是由本质的区别

    • runebyte就是Go内部定义的类型别名,也就说本质上(编译后)没有这两个类型

      1
      2
      type byte = uint8
      type rune = int32
  • 二者的区别

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

    import "fmt"

    // 自定义类型
    type MyInt int

    // 类型别名
    type Int = int

    func main() {

    var a MyInt
    var b Int

    fmt.Printf("%T \n", a) // main.MyInt
    fmt.Printf("%T \n", b) // int
    }
    • a的类型是main.MyInt,表示main包下定义的NewInt类型
    • b的类型是intInt类型只会在代码中存在,编译完成时并不会有Int类型
      • 换句话说,所谓的类型别名只是类似语法糖的东西
  • type关键字后续还有其他的用法:类型断言部分

结构体

  • 类似于Java这种面型对象的语言中的类的概念,在Go中如果想表达一个事物的部分或者全部属性时可以使用结构体自定义数据类型,组织多种类型共同描述一个新的自定义类型,实际上Go通过结构体struct来实现面向对象
    • 本质上是一种聚合型的数据类型
    • 注意结构体的赋值是值赋值而不是引用赋值,与string、数组和基本类型类似,与切片、map不同

结构体的定义

定义新类型

1
2
3
4
5
type Person struct {
// 相同类型的字段可以定义在同一行
name, address string
age int8
}
  • type与struct两个关键字一起来定义结构体,或者说定义一个自定义类型

匿名结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {

// 不指定结构体类型名称
var user struct {
name string
age int
}
// user.name = "李佳"
// user.age = 12

fmt.Printf("%+v \n", user) // {name: age:0}

fmt.Printf("%p \n", &user) // 0xc00000c030


// 第二种写法 短变量声明的写法
user1 := struct {
name string
age int
}{}


}
  • 定义临时数据结构时可以使用,有点Java匿名内部类的意思
  • 关于匿名结构体需要注意的是,匿名结构体类型的变量声明时同时进行了隐式的实例化(分配内存)
  • 注意有两种写法

嵌套结构体

  • 结构体之间是可以嵌套的,但是注意不要互相嵌套(编译器会有错误提示)

    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
    type Address struct {
    country string
    province string
    city string
    }
    type Person struct {
    name string
    age int8
    address Address
    desc string
    }

    func main() {

    p := Person{
    name: "lee",
    age: 12,
    address: Address{
    country: "China",
    province: "Hebei",
    city: "dz",
    },
    desc: "handsome",
    }

    fmt.Printf("%#v \n", p) // main.Person{name:"lee", age:12, address:main.Address{country:"China", province:"Hebei", city:"dz"}, desc:"handsome"}
    }

结构体的匿名字段

  • 所谓匿名字段就是结构体定义时,只指定类型,而不指定属性名,本质上是声明了一个与类型同名的属性,因此匿名字段可以与一般字段混用,但是某一类型的匿名字段只能出现一次,否则就会出现字段名的重复

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    type Student struct{
    string
    int8
    desc string
    }

    func main() {
    s := Student{
    "lijia",
    12,
    "handsome",
    }

    fmt.Printf("%#v \n", s) // main.Student{string:"lijia", int8:12, desc:"handsome"}
    fmt.Println(s.int8) // 12
    fmt.Println(s.string) // lijia

    }

嵌套匿名字段

  • 在嵌套结构体中,嵌套的结构体字段也可以是匿名形式的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    type Address struct {
    country string
    province string
    city string
    }
    type Person struct {
    name string
    age int8
    Address
    desc string
    }

    func main() {

    var p Person

    // 访问匿名字段
    p.Address.city = "dz"
    // 直接访问嵌套匿名结构体的字段
    p.country = "China"
    p.age = 12

    fmt.Printf("%#v \n", p) // main.Person{name:"", age:12, Address:main.Address{country:"China", province:"", city:"dz"}, desc:""}
    }
    • 之所以单独提这个就是因为,访问匿名结构体字段时,可以通过类型名访问,也可以直接访问嵌套的匿名结构体的字段,当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找
      • 注意这个特性只在匿名嵌套结构体的场景下有效
嵌套结构体中的字段名冲突
  • 这也是嵌套结构体中很容易出现的场景,此时访问冲突的字段时,自动索引内部嵌套的结构体的属性的特征不再生效,必须显式指定是哪个嵌套结构体的字段

    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
    //Address 地址结构体
    type Address struct {
    Province string
    City string
    CreateTime string
    }

    //Email 邮箱结构体
    type Email struct {
    Account string
    CreateTime string
    }

    //User 用户结构体
    type User struct {
    Name string
    Gender string
    Address
    Email
    }

    func main() {
    var user User
    user.Name = "沙河娜扎"
    user.Gender = "男"
    // user3.CreateTime = "2019" //报错:ambiguous selector user3.CreateTime
    user.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
    user.Email.CreateTime = "2000" //指定Email结构体中的CreateTime

    user1 := newUser("lee", "男", *newAddress("Hebei", "dz", "2021"), *newEmail("877324234", "2022"))

    fmt.Printf("%#v \n", user1)

    }

    func newAddress(province, city, createTime string) *Address {
    return &Address{
    Province: province,
    City: city,
    CreateTime: createTime,
    }
    }

    func newEmail(account, createTime string) *Email {
    return &Email{
    Account: account,
    CreateTime: createTime,
    }
    }

    func newUser(name, gender string, address Address, email Email) *User {
    return &User{
    Name: name,
    Gender: gender,
    Address: address,
    Email: email,
    }
    }
    • 上边的案例中除了指出发生冲突时要显式指定字段名称之外,也大概写了下这种复杂嵌套下的结构体实例创建的过程(使用构造函数的方式)

结构体字段的可见性

  • 可见性的控制是结构体实现封装性的手段之一
  • 结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
    • 从描述中可以看出,Go中的可见性划分仅仅有包内可见,和全局可见

结构体实例化

简单实例化

  • 结构体实例化之后才会分配内存,可以使用点引用的方式直接赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    type Person struct {
    name, address string
    age int8
    }

    func main() {
    var lee Person
    lee.name = "李佳"
    lee.address = "中国"
    lee.age = 12

    fmt.Println(lee) // {李佳 中国 12}
    fmt.Printf("%#v \n", lee) // main.Person{name:"李佳", address:"中国", age:12}
    fmt.Printf("%+v \n", lee) // {name:李佳 address:中国 age:12}
    fmt.Printf("%v \n", lee) // {李佳 中国 12}
    }
    • 注意常见的的格式化输出方式

指针类型结构体

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
func main() {

// lee的类型是 *Person 即结构体指针
lee := new(Person)

(*lee).name = "李佳"
lee.address = "中国"

fmt.Println(*lee) // {李佳 中国 0}

/*
* 对比slice的指针的使用
*/

slicePtr := new([]int)

fmt.Println(slicePtr == nil) // false

fmt.Printf("%p \n", slicePtr) // 0xc00000c030 切片地址
fmt.Printf("%p \n", *slicePtr) // 0x0 切片底层数组地址

fmt.Printf("len: %d, cap: %d, value: %v \n", len(*slicePtr), cap(*slicePtr), *slicePtr) // len: 0, cap: 0, value: []

var slice []int = *slicePtr
fmt.Printf("%p \n", slice) // 0x0 切片底层数组地址

// 必须先执行扩容
slice = append(slice, 2)
slice[0] = 1 // (*slicePtr)[0] = 1
fmt.Printf("%p \n", slice) // 0xc000012078 扩容了所以变了 切片底层数组地址

/*
* 这里再次遇到slice的认知问题:不能仅仅靠指针来判断是否是同一个切片,即便指针相同也只能表示切片的底层数组相同,而切片的另外两个要素:len与cap未必相同,就导致两个切片未必是相同的切片
*/

fmt.Printf("len: %d, cap: %d, value: %v \n", len(*slicePtr), cap(*slicePtr), *slicePtr) // len: 0, cap: 0, value: []
fmt.Printf("len: %d, cap: %d, value: %v \n", len(slice), cap(slice), slice) // len: 1, cap: 1, value: [1]



/*
* 对比数组指针的使用
*/

arrayPtr := new([3]int)

(*arrayPtr)[0] = 1
// 1 0 0
fmt.Println(*arrayPtr)


}
  • 其实就是使用new关键字来分配内存以进行实例化,需要注意的是,Go中支持直接在结构体指针变量上使用点引用的方式来访问字段,根据上一章的学习,其实应该先使用*取值后再访问的(两种方法都可以)
    • 实际上这种使用结构体类型指针直接进行点引用的操作时Go实现的语法糖:lee.name = "李佳"的实现是(*lee).name = "李佳"

取结构体的地址进行实例化

  • 实际上也是初始化的方法之一(都可以初始化了,自然已经实例化了)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    func main() {

    lee := Person{}
    fmt.Printf("%p %+v\n", &lee, lee) // 0xc0000241b0 {name: address: age:0}

    lee.age = 12

    dan := &Person{}
    fmt.Printf("%p %+v\n", &dan, *dan) // 0xc00000e030 {name: address: age:0}

    dan.age = 23
    }
    • 一种是直接使用类型 + 大括号的方式实例化,大括号内可以执行初始化,对应的变量类型就是声明的类型–Person
    • 另外一种是在第一种的基础上使用指针的形式实例化化(大括号内同样可以执行初始化),对应的变量类型是声明的类型的指针–*Person
      • 这里再次用到了结构体指针直接使用点引用的语法糖
    • 后边的结构体的构造函数中会使用这个形式的实例化方式

结构体初始化

  • 结构体实例化后内存分布的都是字段的零值,需要执行赋值初始化(使用声明同时初始化的方式更好)

键值对形式的初始化

  • 使用大括号形式的实例化方式进行初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    func main() {

    lee := Person{
    name: "李佳",
    age: 12,
    }
    fmt.Printf("%+v\n", lee) // {name:李佳 address: age:12}

    dan := &Person{
    name: "sdd",
    age: 23,
    }
    fmt.Printf("%+v\n", *dan) // {name:sdd address: age:23}

    }
    • 不初始化的字段,默认保持零值
    • 例子中对应了前边说的取结构体地址进行实例化的方式,在大括号内也可以顺便进行初始化

值列表形式的初始化

  • 所谓值列表,就是初始化中只提供值,而不提供字段名,更简洁,当然也有更多限制

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    func main() {

    dd := Person{
    "sdd",
    "dz",
    23,
    }
    fmt.Printf("%+v\n", dd) // {name:sdd address:dz age:23}

    ddPtr := &Person{
    "sdd",
    "dz",
    23,
    }
    fmt.Printf("%+v\n", ddPtr) // &{name:sdd address:dz age:23}

    }
    • 限制
      • 必须初始化结构体的所有字段
      • 初始值的填充顺序必须与字段在结构体中的声明顺序一致
      • 该方式不能和键值初始化方式混用

直接使用大括号

1
2
3
4
5
6
7
8
9
10
func main() {
persons := []Person{
{name: "lijia"},
{name: "dandan", age: 12},
{name: "jk", age: 23, address: "dz"},
}

fmt.Println(persons)

}
  • 一般用在结构体的集合类型中

使用构造函数进行初始化

  • 参考下边的构造函数和方法的章节

结构体的内存分布

  • 结构体占用一块连续的内存,可以从下边的案例看出

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

    type Person struct {
    name string
    age int8
    address string
    }

    type test struct {
    a int8
    b int8
    c int8
    d int8
    }

    func main() {
    lee := Person{
    name: "lijia",
    age: 12,
    }

    fmt.Printf("%p \n", &lee.name) // 0xc000098180
    fmt.Printf("%p \n", &lee.age) // 0xc000098190
    fmt.Printf("%p \n", &lee.address) // 0xc000098198

    dandan := Person{}
    fmt.Println(unsafe.Sizeof(dandan)) // 40

    var somebody struct{}
    fmt.Println(unsafe.Sizeof(somebody)) // 0

    var somebody1 Person
    fmt.Println(unsafe.Sizeof(somebody1)) // 40

    n := test{
    1, 2, 3, 4,
    }
    fmt.Printf("n.a %p\n", &n.a) // n.a 0xc0000a0060
    fmt.Printf("n.b %p\n", &n.b) // n.b 0xc0000a0061
    fmt.Printf("n.c %p\n", &n.c) // n.c 0xc0000a0062
    fmt.Printf("n.d %p\n", &n.d) // n.d 0xc0000a0063

    }
    • 空结构体是不占用内存的,注意空结构体指的是没有定义任何字段的结构体,不是只声明不初始化的结构体
    • 关于基本数据类型和复杂数据类型的空间占用可以回顾前边的数据类型部分的图表

Unsafe包的几个方法

  • Sizeof函数 函数返回操作数在内存中的字节大小
    • Sizeof函数返回的大小只包括数据结构中固定的部分,例如字符串对应结构体中的指针和字符串长度部分,但是并不包含指针指向的字符串的内容。Go语言中非聚合类型通常有一个固定的大小,尽管在不同工具链下生成的实际大小可能会有所不同。考虑到可移植性,引用类型或包含引用类型的大小在32位平台上是4个字节,在64位平台上是8个字节
    • 注意聚合类型(数组、结构体)中的内存对齐机制
      • 由于地址对齐这个因素,一个聚合类型(结构体或数组)的大小至少是所有字段或元素大小的总和,或者更大因为可能存在内存空洞。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐(译注:内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响)
  • unsafe.Alignof 函数返回对应参数的类型需要对齐的倍数。和 Sizeof 类似, Alignof 也是返回一个常量表达式,对应一个常量。通常情况下布尔和数字类型需要对齐到它们本身的大小(最多8个字节),其它的类型对齐到机器字大小
  • unsafe.Offsetof 函数的参数必须是一个字段 x.f,然后返回 f 字段相对于 x 起始地址的偏移量,包括可能的空洞
  • 虽然这几个函数在不安全的unsafe包,但是这几个函数调用并不是真的不安全,特别在需要优化内存空间时它们返回的结果对于理解原生的内存布局很有帮助
参考

Go中的内存对齐

  • 暂未深入学习–待看
参考

结构体的构造函数与方法

构造函数

  • 可以通过构造函数的形式实现结构体的初始化,构造函数这个说法有点Java中的类构造函数的意思了
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
type Person struct {
name string
age int8
address string
desc string
}

func main() {
p1 := newPerson("li", "dz", "ss", 12)
p2 := newPerson("li", "dz", "ss", 12)
fmt.Println(p1)
p1.age = 23
fmt.Println(p1)
fmt.Println(p2)

}

/*
func newPerson(name, address, desc string, age int8) Person {
return Person{
name: name,
age: age,
address: address,
desc: desc,
}
}
*/

func newPerson(name, address, desc string, age int8) *Person {
return &Person{
name: name,
age: age,
address: address,
desc: desc,
}
}
  • 结构体类型实际上是一个值类型,所以构造函数返回一个结构体并赋值给变量时实际上会执行数据的复制,当结构体比较大时就会有性能问题,因此一般构造函数会返回结构体的指针,这也是为什么前边会单独说明取地址形式的结构体初始化的原因之一
  • 所谓的构造函数不过就是特殊形式的函数而已,特殊之处在于其专门用来构建结构体实例,就像是Java中的类构造函数用来初始化类实例一样
  • 构造函数的命名一般就是new + 类型名(首字母大写)

方法

  • 由构造函数可以推测出,实际上Go想实现的中的方法,不过也只是特殊形式的函数而已,但是为了进一步实现所谓类中方法的this或者self这个特性,实际上Go中的方法的形式又与一般的函数不一样,多了一个接收者的角色,所谓的接收者实际上就是特定类型的变量,其作用就是作为this或者self。格式如下:

    1
    2
    3
    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
    }
    • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为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
type Person struct {
name string
age int8
address string
desc string
}

func main() {
p1 := newPerson("li", "dz", "ss", 12)
p2 := newPerson("dd", "dz", "kk", 12)

p1.printPerson()
p2.age = 23
p2.printPerson()

}

func newPerson(name, address, desc string, age int8) *Person {
return &Person{
name: name,
age: age,
address: address,
desc: desc,
}
}

func (p Person) printPerson() {
fmt.Printf("welcome %s \n Your age is %d \n", p.name, p.age)
}
  • 方法与函数的区别在于:函数是没有类型归属的,可以独立调用,而方法因为引入了接收者,所以只能被接收者所属类型的实例调用,而不能独立调用执行
指针类型接收者
  • 以指针的形式可以直接修改对应的实例的属性,并且方法执行后修改生效,这是与Java中的this或者是Python中的self比较类似的
  • 什么时候应该使用指针类型接收者
    • 需要修改接收者中的值
    • 接收者是拷贝代价比较大的大对象
    • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
值类型接收者
  • 实际上调用方法时执行了特定类型实例到接收者的值拷贝,此时只能读,实例属性的修改不会生效,因为修改操作只针对副本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main() {
p1 := newPerson("li", "dz", "ss", 12)
p2 := newPerson("dd", "dz", "kk", 12)

p1.setAge1(8)
fmt.Println(p1.age) // 12 未修改
p2.setAge(3)
fmt.Println(p2.age) // 3 修改成功

}

// 指针类型
func (p *Person) setAge(age int8) {
p.age = age
}
// 值类型
func (p Person) setAge1(age int8) {
p.age = age
}
  • 可以发现即便构造函数返回的是结构体指针,但是也可以顺利执行接收者为将诶构体类型的方法,这是因为实现了读指针类型取值的语法糖,这个语法糖在前边的结构体指针也可以使用点引用也提到了,并且在后边的接口实现章节中也会有体现
任意类型都可以自定义方法
  • 前边提到了结构体类型的定义方法,实际上使用type定义的任意的自定义类型都可以自定义方法,当然自定义类型应该是当前包内的类型,不能是其他包的类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    //MyInt 将int定义为自定义MyInt类型
    type MyInt int

    //SayHello 为MyInt添加一个SayHello的方法
    func (m MyInt) SayHello() {
    fmt.Println("Hello, 我是一个int。")
    }
    func main() {
    var m1 MyInt
    m1.SayHello() //Hello, 我是一个int。
    m1 = 100
    fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
    }

结构体的继承

  • 结构体在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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    type Animal struct {
    name string
    }

    func newAnimal(name string) *Animal {
    return &Animal{
    name: name,
    }
    }

    func (a *Animal) move() {
    fmt.Println(a.name + "在移动")
    }

    func (a *Animal) eat() {
    fmt.Println(a.name + "在吃东西")
    }

    type Dog struct {
    *Animal
    }

    func newDog(name string) *Dog {
    return &Dog{
    &Animal{
    name: name,
    },
    }
    }
    func (d *Dog) wang() {
    fmt.Println(d.name + "在狂吠")
    }

    // 方法覆盖
    func (d *Dog) eat() {
    fmt.Println(d.name + "在吃骨头")
    }

    func main() {
    d := newDog("旺财")
    d.wang() // 旺财在狂吠
    d.eat() // 旺财在吃骨头
    // 可以调用继承的方法
    d.move() // 旺财在移动

    a := newAnimal("小动物")

    huntAnimal(a)
    huntAnimal(d) // 类型错误 不支持多态
    huntAnimal1(d) // 类型错误 不支持多态
    }

    func huntAnimal(animal *Animal) {
    animal.eat()
    }

    func huntAnimal1(animal Animal) {
    animal.eat()
    }
    • 参考eat函数,实际上由于匿名嵌套结构下的属性的解析过程是先从顶层结构体内开始找再到嵌套结构体内部找,所以也可以实现方法的覆盖
    • 参考huntAnimal函数,Go不能实现子类继承的多态,因为类的继承关系只是结构体内部嵌套引入的,但是在类型系统上毫无关系
      • 应该还有其他办法,比如后边的接口应该可以实现对应的多态
      • 换句话说就是Go中可以并不能实现类型提升(但是可以实现接口类型提升,参照后面的接口部分)

多继承

  • 众所周知,Java中是没有多继承的,原因之一就在于出现冲突,在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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    type Horse struct {
    Name string
    }

    func NewHorse(name string) *Horse {
    return &Horse{Name: name}
    }

    func (h *Horse) Eat(name string) {
    fmt.Println("马儿 " + h.Name + "在吃" + name)
    }

    func (h *Horse) Run() {
    fmt.Println("马儿 " + h.Name + "在奔跑")
    }

    func (h *Horse) Sleep() {
    fmt.Println("马儿 " + h.Name + "在站着睡觉")
    }


    type Donkey struct {
    Name string
    }

    func NewDonkey(name string) *Donkey {
    return &Donkey{Name: name}
    }

    func (h *Donkey) Eat(name string) {
    fmt.Println("驴子 " + h.Name + "在吃" + name)
    }

    func (h *Donkey) Run() {
    fmt.Println("驴子 " + h.Name + "在奔跑")
    }

    func (h *Donkey) Sleep() {
    fmt.Println("驴子 " + h.Name + "在站着睡觉")
    }


    type Mule struct {
    *Horse
    *Donkey
    }

    func NewMule(h *Horse, d *Donkey) *Mule {
    return &Mule{h, d}
    }


    func main() {
    m := animal.NewMule(animal.NewHorse("丽莎"),animal.NewDonkey("蹄子"))
    m.Horse.Run()
    }
    • 可以发现当匿名属性是指针类型的时候,默认的属性名是指针类型对应的数据类型,因此调用时也不会使用*开头的属性名(不合法的变量名)

结构体与JSON序列化

  • 实际上就是使用Go中的原生模块json的方法实现JSON序列化与反序列化
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
import (
"encoding/json"
"fmt"
)

// 学生
type Student struct {
ID int
Gender string
Name string
}

// 班级
type Class struct {
Title string

Students []*Student
}

func main() {

/*
初始化班级实例
*/
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}

for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%013d", i),
Gender: "男",
ID: i,
}

c.Students = append(c.Students, stu)
}

// JSON序列化
data, err := json.Marshal(c)
if err != nil {
fmt.Println("JSON marshal failed")
return
}
fmt.Printf("json: %s \n", data)

// JSON反序列化

// jsonString = string(data)

// 初始化一个空的Class实例
c1 := &Class{}
err = json.Unmarshal(data, c1)

if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
  • 注意结构体指针的使用,因为结构体是值传递(并且可能存在因为结构体比较大导致的值复制耗费性能的问题),因此在结构体对应的函数、方法乃至于结构体集合中,都常使用结构体指针
  • 注意Unmarshal方法的第一个参数的类型是[]byte,因此对于字符串需要先强转为[]byte
  • 私有字段即小写字母开头的结构体属性不会被序列化

结构体标签

  • Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来(参考后续的结构体反射部分)Tag结构体字段的后方定义,由一对反引号包裹起来,具体的格式:

    1
    `key1:"value1" key2:"value2"`
    • 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔,但是注意键和值之间只有冒号没有空格
    • 为结构体编写Tag时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //Student 学生
    type Student struct {
    ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
    Gender string //json序列化是默认使用字段名作为key
    name string //私有不能被json包访问
    }

    func main() {
    s1 := Student{
    ID: 1,
    Gender: "男",
    name: "沙河娜扎",
    }
    data, err := json.Marshal(s1)
    if err != nil {
    fmt.Println("json marshal failed!")
    return
    }
    fmt.Printf("json str:%s\n", data) //json str:{"id":1,"Gender":"男"}
    }
  • 可以将Go中结构体的标签理解成Java中的注解,都可以通过反射获取结构体(类)的元数据

结构体知识补充

  • 实际上就是对于slice和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
      type Person struct {
    name string
    age int8
    dreams []string
    }

    func (p *Person) SetDreams(dreams []string) {
    p.dreams = dreams
    }

    func main() {
    p1 := Person{name: "小王子", age: 18}
    data := []string{"吃饭", "睡觉", "打豆豆"}
    p1.SetDreams(data)

    // 你真的想要修改 p1.dreams 吗?
    data[1] = "不睡觉"
    fmt.Println(p1.dreams) // [吃饭 不睡觉 打豆豆] p1的数据被误修改
    }

    - 正确的做法是在方法中进行slice的拷贝

    ```go
    func (p *Person) SetDreams(dreams []string) {
    p.dreams = make([]string, len(dreams))
    copy(p.dreams, dreams)
    }
    • 同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题

一个小demo

  • 使用“面向对象”的思维方式编写一个学生信息管理系统。
    1. 学生有id、姓名、年龄、分数等信息
    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
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
type Student struct {
id, name string
age int8
score int
}

func newStudent(id, name string, age int8, score int) *Student {
return &Student{
id: id,
name: name,
age: age,
score: score,
}
}

type StudentManage struct {
students []*Student
}

func newStudentManage(num int) *StudentManage {
return &StudentManage{
students: make([]*Student, 0, num),
}
}

// 展示学生列表
func (sm *StudentManage) showStduentList() {

for _, v := range sm.students {

fmt.Printf("id: %s | name: %s | age: %d | score: %d \n", v.id, v.name, v.age, v.score)

}
}

// 添加学生
func (sm *StudentManage) addStudent(student *Student) {
sm.students = append(sm.students, student)
}

// 编辑学生信息
func (sm *StudentManage) updateStudent(id string, student *Student) {
for i, v := range sm.students {
if v.id == id {
sm.students = append(sm.students[:i], sm.students[i+1:]...)
sm.students = append(sm.students, student)
}
}
}

// 删除学生信息
func (sm *StudentManage) deleteStudent(id string) {
for i, v := range sm.students {
if v.id == id {
sm.students = append(sm.students[:i], sm.students[i+1:]...)
}
}
}

func main() {
// 创建学生管理系统实例,初始化为可以存储20个学生
sm := newStudentManage(20)
// 添加学生
for i := 0; i < 10; i++ {
// Sprint函数可以执行int转string
s := newStudent(fmt.Sprint(i), fmt.Sprintf("stu%02d", i), 23, 66)
sm.addStudent(s)
}

sm.showStduentList()

sm.deleteStudent("5")
fmt.Println()
sm.showStduentList()

sm.updateStudent("3", newStudent("3", "jk", 12, 89))
fmt.Println()
sm.showStduentList()

}
  • 注意切片易错
  • 注意方法使用指针类型的接收者

Go 包

  • Go语言的工程化依赖于Go中的包,确实前边也讲过了函数和变量一般都是包内可见的,并且结构体的属性也可以通过大小写来区分是包内可见还是全局可见
  • 一个Go module就是一个Go最小工程(一个Go工程可以包含多个模块),一个Go最小工程(模块)可以有多个包,每个包需要在单独的文件夹中
    • 一个模块应该最多只有一个main包,以及对应的main方法
  • 包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmtosio

包的定义

  • 一个包可以简单理解为一个存放.go文件的文件夹。 该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包

    1
    package 包名
    • 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下
    • 包名可以不和文件夹的名字一样(当然包名和包下的文件名也无关),包名不能包含 - 符号
    • 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件

包可见性

  • 实际上想让一个包里边的成员变为全局可见,只需要将其定义的变(常)量名、函数名、类型名设置为首字母大写即可,与结构体属性类似
    • 对于全局变量而言符合上述规则,但是对于局部变量即便其定义为首字母大写,也只能在当前包的当前函数使用(同样的道理也适用于结构体中定义的变量和接口中定义的函数,其可见性是由自身的定义可见性与外部结构的可见性共同决定的,全部的全局可见才是完全的全局可见)

包导入

  • 使用import关键字进行导入 import "包的路径"
  • 注意事项
    • import导入语句通常放在文件开头包声明语句的下
    • 导入的包名需要使用双引号包裹起来
    • 包名是从$GOPATH/src/后开始计算的,使用/进行路径分割
    • Go语言中禁止循环导入包

单行导入

1
2
import "包1"
import "包2"

多行导入

1
2
3
4
import (
"包1"
"包2"
)

本地包导入

  • import语句中的包的位置指的是包文件夹的相对路径,使用/进行路径分割,前边有说明,并且匹配文件夹名字时可以不区分大小写
    • 也可以理解为是module名/package名,因为模块名一般就是包所在模块的相对路径
  • 导入包后,在程序中直接使用对应包中定义的package名来调用暴露的方法等,或者是指定包的别名
  • 参考:使用go module导入本地包

自定义包名

  • 当导入的包名比较长,或者是产生命名冲突时,会在导入包时使用自定义包名

单行

1
import 别名 "包的路径"

多行

1
2
3
4
import (
"fmt"
别名 "包的路径"
)

匿名导入

  • 如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包

    1
    import _ "包的路径"
    • 匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中

包的init函数

  • 在执行包导入的时候,会触发所有被导入的包内部的init函数(包括所有层级的被导入的包以及main包本身)执行,init()函数没有参数也没有返回值。 init()函数在程序运行时自动被调用执行,不能在代码中主动调用它
  • init函数中可以访问全局变量,对于main包,init函数会先于main方法执行

init函数执行顺序

  • init函数执行顺序其实也就是包导入的顺序

  • Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码

  • 在运行时,被最后导入的包会最先初始化并调用其init()函数

    image-20211014125344719

接口

  • 为了支持面向对象,Go中也提供了接口这个抽象类型,其内部定义了一组方法,并规定任何实现了其全部方法的类(结构体)都是对于该接口的实现,因为类的引入,使得Go有了对于开闭原则的支持(面向接口编程),实现整个Go项目的稳定性与可复用性
  • 与Java中的接口不同的是,Go中的接口只声明了方法,而没有声明数据属性,当然也没有Java中的默认方法等接口的高级功能

接口定义

  • 接口的格式如下

    1
    2
    3
    4
    5
    type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2

    }
    • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如:有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义
      • 见仁见智吧
    • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问,与一般的类中的方法类似
    • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略

接口嵌套(接口继承)

  • 一个接口可以实现多个接口,以完成复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Hunter 捕猎者
type Hunter interface {
hunt(name string)
}

// Animal 动物
type Animal interface {
Eat(string)
Run()
Sleep()
}

// Carnivore 肉食动物
type Carnivore interface {
Animal
Hunter
}

接口的实现

  • 与typescript类似,Go中实现接口的方式就是在某类型中实现接口的所有方法即自动称为接口的实现类,而不用像Java中使用implements显式声明接口与类的关系
    • 注意必须实现接口的全部方式,才是对接口的实现

用接口类型实现多态

  • 前边说了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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// github.com/JJLAAA/helloWorld/interface中的_interface包
package _interface

type Animal interface {
Eat(string)
Run()
Sleep()
}

// main包 Dog类
type Dog struct {
name string
}

func newDog(name string) *Dog {
return &Dog{name: name}
}

func (d *Dog) Eat(food string) {
fmt.Println("小狗" + d.name + "在吃" + food)
}

func (d *Dog) Run() {
fmt.Println("小狗" + d.name + "在奔跑")
}

func (d *Dog) Sleep() {
fmt.Println("小狗" + d.name + "在睡觉")
}

// main包 Cat类
type Cat struct {
name string
}

func newCat(name string) *Cat {
return &Cat{name: name}
}

func (d *Cat) Eat(food string) {
fmt.Println("小猫" + d.name + "在吃" + food)
}

func (d *Cat) Run() {
fmt.Println("小猫" + d.name + "在奔跑")
}

func (d *Cat) Sleep() {
fmt.Println("小猫" + d.name + "在睡觉")
}

// 入口
import in "github.com/JJLAAA/helloWorld/interface"

func main() {

d := newDog("旺财")
c := newCat("咪咪")

animalRun(d) // 小狗旺财在奔跑
animalRun(c) // 小猫咪咪在奔跑

}

func animalRun(a in.Animal) {
a.Run()
}

确保类实现接口

  • 前边说的类实现接口,都是手动实现接口所有方法即可,不像Java中有显式的类型约束,那么在使用接口的实现类时,除了用接口的类型限制外,还有一种利用匿名变量保证类实现接口的巧妙方式

    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"
    in "github.com/JJLAAA/helloWorld/interface"
    )

    type Dog struct {
    name string
    }
    /*
    使用匿名变量不会占用命名空间
    */
    var _ in.Animal = &Dog{}

    func newDog(name string) *Dog {
    return &Dog{name: name}
    }

    func (d *Dog) Eat(food string) {
    fmt.Println("小狗" + d.name + "在吃" + food)
    }

    func (d *Dog) Run() {
    fmt.Println("小狗" + d.name + "在奔跑")
    }

    func (d *Dog) Sleep() {
    fmt.Println("小狗" + d.name + "在睡觉")
    }

接口实现类使用值接收者和指针接收者的区别

  • 前边介绍类的方法的时候提到了不同的接收者的区别,接收者的角色就类似于Java中的this,在Go中不同种类的接收者的特殊行为也不一样:当接口实现类的方法是指针类型的接收者的时候,指针类型的变量不能承载类(结构体)指针类型的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import in "github.com/JJLAAA/helloWorld/interface"
type Animal = in.Animal

func main() {

d := newDog("旺财")
c := newCat("咪咪")

animalRun(d) // 小狗旺财在奔跑
animalRun(c) // 小猫咪咪在奔跑

d1 := Dog{name: "旺旺"}
animalRun(d1) // 报错 Cannot use 'd1' (type Dog) as the type Animal Type does not implement 'Animal' as the 'Eat' method has a pointer receiver

var d2 Animal = Dog{name: "汪汪"} // 报错 Cannot use 'Dog{name: "汪汪"}' (type Dog) as the type Animal Type does not implement 'Animal' as the 'Eat' method has a pointer receiver

d1 := &Dog{name: "旺旺"}
animalRun(d1) // 不报错
}

func animalRun(a Animal) {
a.Run()
}
  • 可以发现当接口实现类的方法是指针类型接收者的时候接口类型的变量并不能接收结构体类型的实例(任一个方法是指针类型接收者都不行),可以接收指针类型的实例;而如果将方法全部改为值类型接收者时,接口类型的变量既可以承载类实例,也可以承载类指针类型的实例
分析
  • 出现上述状况的原因在于(举例分析):
    • 当Animal接口的实现类Dog的方法接收者为值类型时,可以认为Dog类型是接口Animal的实现类,此时如果接口类型变量承接Dog类型的实例肯定没问题,如果承接的是&Dog类型的变量的话,因为前边介绍过得将结构体指针自动取值的语法糖,所以也不会有错
    • 当Dog方法的接受者时指针类型的时候,可以认为接口的实现类实际上是*Dog类型,此时为接口类型的变量赋值指针类型*Dog的实例的话不会报错,但是如果赋值Dog类型的实例的话,因为不存在自动转换的语法糖,所以肯定会有类型问题,所以不允许这样的操作
  • 分析下来会发现似乎使用值类型的接收者更具有兼容性,但是考虑到前边说的两种类型的接收者的区分,可能使用指针类型更常见些

类与接口的关系

  1. 一个类可以实现多个接口,只需要实现接口的全部方法即可
  2. 多个类当然也可以实现同一个接口
  3. 父类实现接口后,子类自动实现该接口(继承了父类的实现方法)
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
import in "github.com/JJLAAA/helloWorld/interface"

type RagDoll struct {
*Cat
}

func newRagDoll(name string) *RagDoll {
return &RagDoll{newCat(name)}
}

func (r *RagDoll) say() {
fmt.Println(r.name + " 布偶猫在喵喵喵")
}

type Animal = in.Animal

func main() {

r := newRagDoll("喵喵")
animalRun(r)
//catRun(r) // 类型错误,两相对比再次验证Go中只有接口类型提升,而没有类的提升

}

func animalRun(a Animal) {
a.Run()
}

func catRun(c Cat) {
c.Run()
}

空接口

  • 空接口就是没有定义任何方法的接口,因此可以说任何类型都实现了空接口,因此空接口类型是一个万能类型,空接口类型的变量可以存储任意类型的数据,因此用途也比较广泛
1
2
3
4
5
6
7
func main() {
var a interface{}

a = "13"
// string, 13
fmt.Printf("%T, %v\n", a, a)
}

作为函数的参数

  • 空接口作为参数类型表示函数可以接收任意类型的参数

    1
    2
    3
    func show(a interface{}) {
    fmt.Printf("%T %v", a, a)
    }

作为map的值类型

  • 实现可以保存任何类型值的map

    1
    2
    3
    4
    5
    6
    7
    func main() {
    m := make(map[string]interface{}, 12)
    m["name"] = "lee"
    m["age"] = 23
    // map[age:23 name:lee]
    fmt.Println(m)
    }

类型断言

  • 空接口类型的变量可以存储所有类型的值,那么在具体使用的时候,如何分辨其具体存储的数据类型是什么呢(这是必要的,因为对于不同的具体数据类型会有不同的操作行为)—-使用类型断言
接口类型底层详解
  • 前边数据类型部分说接口类型的变量占两个机器字,实际上就是说:一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的。这两部分分别称为接口的动态类型动态值

  • 举例说明

    1
    2
    3
    4
    var w io.Writer
    w = os.Stdout
    w = new(bytes.Buffer)
    w = nil
    • 对应的内存状态为:

      image-20211016105750222

      • 只声明类型之后,两个位置仍然都是nil
      • FileBufferWriter的实现类
类型断言
  • 想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:x.(T)

    • x:表示类型为interface{}的变量
    • T:表示断言x可能是的类型
      • 可以是原生类型也可以是自定义类型
  • 该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    func main() {
    var i interface{}
    m := make(map[int]int)
    m[1] = 1
    i = m
    // false
    v, r := i.(string)
    if r {
    fmt.Println(v)
    }
    // true
    v1, r1 := i.(map[int]int)
    if r1 {
    // map[1:1]
    fmt.Println(v1)
    }
    // false
    v2, r2 := i.(map[int]string)
    if r2 {
    fmt.Println(v2)
    }
    }
    • 只有接口类型才能使用类型断言,其中以空接口类型最常用
  • 可以使用switch语法实现更方便的类型断言

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    func main() {
    var i interface{}
    i = true
    // x is a bool is true
    justifyType(i)
    }

    func justifyType(i interface{}) {
    switch v := i.(type) {
    case string:
    fmt.Printf("x is a string,value is %v\n", v)
    case int:
    fmt.Printf("x is a int is %v\n", v)
    case bool:
    fmt.Printf("x is a bool is %v\n", v)
    default:
    fmt.Println("unsupported type!")
    }
    }
    • 注意type关键字的使用方式
  • 类型断言实际上是一种编译阶段获取空接口的值的类型的方法,一般只适用于已知类型的验证的场景,使用的更多的应该是使用反射在运行阶段判断空接口的值的类型

与其他语言进行交互

参考