Go基础入门

91 阅读28分钟

68747470733a2f2f63646e2e6362642e696e742f616e7a686979752d61737365747340312e302e31312f696d6167652f636f6d6d6f6e2f6769746875622d696e666f2f706572736f6e616c2d686f6d65706167652d62616e6e65722e6a7067.jpg

开篇综述

语言介绍

Go 语言由谷歌(Google)公司于 2009 年正式对外发布,设计 Go 语言的初衷都是为了满足 Google 公司的需求。主要目标是“兼具 Python 等动态语言的开发速度和 C/C++等编译型语言的性能与安全性”,旨在不损失应用程序性能的情况下降低代码的复杂性,具有“部署简单、并发性好、执行性能好”等优势。最主要还是为了并发而生,并发是基于goroutine的,goroutine类似于线程,但并非线程,可以将goroutine理解为一种轻量级线程。Go 语言运行时会参与调度goroutine,并将goroutine合理地分配到每个 CPU 中,最大限度地使用 CPU 性能。

作者介绍

y2snviYUp6grnjX2KiAMnQ

从左到右分别是 :

  • 罗伯特·格瑞史莫(Robert Griesemer):  曾为谷歌的 V8 JavaScript 引擎和 Chubby 开发代码
  • 罗勃·派克(Rob Pike):  罗布·派克是 Unix 的先驱,是贝尔实验室最早和 Ken Thompson 以及 Dennis M. Ritche 开发 Unix 的猛人,UTF-8 的设计人。还是 1980 年奥运会射箭的银牌得主。
  • 汤普逊(Ken Thompson):  Ken Thompson 图灵奖得主,C 语言前身 B 语言的作者,Unix 的发明人之一, 操作系统 Plan 9 的主要作者。 共同开发了 UTF-8。

Go 语言特点

Go 语言,作为编程语言的后生,站在巨人的肩膀上,吸收了其他一些编程语言的特点。下面简单解释一下:

  1. 拥有编译器很方便

    自带编译器可以检测出你犯的所有低级错误,如:变量名拼错,不要小看这种问题,没有编译器情况下,很可能浪费掉很长时间去排查,并且非常不容易发现,而且Go语言也是跨平台编译的,你可以在 Mac 电脑上,编译出linux或者windows的目标程序。

  2. 开发速度

    Go 是一个非常简单的语言,上手容易,无论你是小白还是老鸟,都会比其他语言 C/C++ 和 Java 等语言要容易很多,这点在做项目中体现得尤其明显。很多人可能会说那 python 呢,php 呢?从语言上说 python 和 php 他们没有编译检查,同样也会像上面说的出一些小的低级错误,或者运行时错误,这都给 php 和 python 带来了隐患,所以 Go 语言虽然牺牲了一点点代码书写的时间,但是从项目运行安全角度来说,已经非常值得了。

  3. 天生高并发

    Go 语言就是为高并发而生的。当你需要使用并发场景,如果你有其他语言基础,第一反应是用到锁,但是 Go 语言提供了更加方便的方式协程+通道,在 Go 语言中代码不用修改就能直接多协程运行,只要在调用的时候加入 go 关键字,就可以了,非常方便。这与其他语言截然不同,你要考虑哪里开辟新的线程,哪里是代码执行逻辑。

  4. 部署简单

    Go 语言最终执行就是一个二进制文件,包括了它所依赖的程序包,这让开发者不用考虑部署环境的问题,例如,如果你是 java 程序,要考虑执行你的程序,对方机器是否安装了 java 的运行环境,其他语言同理,如果对方机器没有安装,无法运行你的程序,Go 语言可以在 Mac 和 Linux 上交叉编译你的代码,将其拷贝到远程服务器上,然后就可以任其运行了。

Go 语言能做什么

Go 的优点:实现快 + 资源占用低 + 任意环境随便跑,综合考虑在很多场景十分好用 。

  1. Go 适合造轮子,哪个库不好用就自己造。
  2. Go 适合写工具,比如 hugo 、hub,还有国人写的 linux 下的百度 pan client 都是 go 实现的。
  3. Go 适合实现 C/C++ 一部分业务,Java 的大部分业务。
  4. Go 适合做最外层的胶水,通过 RPC/REST/CGO 粘合不同语言的模块,而在这个胶水层还可以实现各种业务逻辑,又不用像 shell/python/node 有诸多顾虑和局限。
  5. Go 提供了协程、指针、 unsafe, cgo 加上 C/C++ 兼容的内存布局和跨平台的汇编,有了这些你能做很多事情了。

所以问题不是它适合做什么,是你有能力用它做什么。

哪些公司在使用 Go

这个问题之前 Go 的官方 wiki 里面有一个维护列表,大家可以点击下面地址看到:

github.com/golang/go/w…

在 stackshare 里面可能更加直接的看到一些使用 Go 的国外企业列表:

  • Uber
  • Google
  • Pinterest
  • Shopify
  • Slack

国内使用 Go 的企业也非常的多:

  • 腾讯
  • 阿里巴巴
  • 百度
  • 字节跳动
  • BiliBili
  • 小米
  • 163Cloud
  • PingCAP
  • 积梦智能

开发环境搭建

“工欲善其事,必先利其器”,做好一件事,准备工作非常重要。在开始学习 Go 技术之前,先介绍如何搭建 Go 开发环境是非常重要的一件事。

安装和配置 SDK

本文基于go version go1.18

下载

Go 源码包官网下载地址为:golang.org/dl/(因为你知道的原…

Go 官方镜像站(推荐):golang.google.cn/dl/

Go 语言支持以下系统,根据自己的系统,自行选择安装即可:

  • Linux
  • FreeBSD
  • Mac OS X(也称为 Darwin)
  • Windows

操作系统

包名

Windows
​
go1.18.windows-amd64.msi
​
Linux
​
go1.18.linux-amd64.tar.gz
​
Mac
​
go1.18.darwin-amd64.pkg
​
FreeBSD
​
go1.18.freebsd-amd64.tar.gz
​

Win & Mac

对于 Windows 平台和 Mac 平台,只要下载对应的安装包,然后双击,一路 next 点击,就可以安装完成

在 win 中用 cmd,执行下面的命令,查看是否是你安装的版本

PS C:> go version  
go version go1.18 windows/amd64

在 mac 中 Terminal 中执行

go version  
go version go1.18 darwin/arm64  

如果你在 mac 平台上,也可以使用 brew install go,这样的方式安装,只要这么一条命令就可以搞定。

Linux

使用go mod 管理项目,就不需要非得把项目放到GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,包含go.mod文件的目录也被称为模块根,也就是说,go.mod 文件的出现定义了它所在的目录为一个模块。

GoLand 选择你电脑上安装好的 GO 编程环境 创建一个 Demo 工程,点击如图所示运行按钮,若能成功执行,则环境搭建完毕!


第一个 Go 程序

Hello Go

在控制台输出“Hello Go!”非常简单,仅需要几行代码就可以搞定,如下所示:

复制成功

package main   // 声明 main 包  
  
import "fmt"   // 导入 fmt 包,打印字符串时需要用到  
  
func main(){   // 声明 main 主函数  
    fmt.Println("Hello, Go!")  // 打印 Hello Go!  
}  
​

大家也许不明白这些代码的含义,没关系,下面就来依次介绍。

注释

作用: 在代码中加一些说明和解释,方便自己或其他程序员程序员阅读代码,能够大大增强程序的可读性。

两种格式

  • 单行注释:通常放在一行代码的上方,或者一条语句的末尾,==对该行代码说明==
     fmt.Println("Hello, Go!") // 右边的所有东西当做说明,而不是真正要执行的程序,起辅助说明作用  S
  • 多行注释:通常放在一段代码的上方,==对该段代码做整体说明==
        func main() {  
        /*  
            以下两行代码都是将信息打印在屏幕上  
         */  
        fmt.Println("以下是我的微信公众号:")  
        fmt.Printf("《面向加薪学习》")  
    }  
  • 特别说明:

    • 编译器在编译代码时,会忽略注释的内容。如果你写的代码不想让计算机执行,那么也可以加上注释。加了注释后代码不会被编译执行,这就是对已有的代码进行注销。
    • 以后写程序要多加注释,这是我们程序猿的专业和职业道德,不加注释就是流氓。

package(创建包)

Go 语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包,所以我们会看到每个 Go 源文件的开头都是一个 package 声明,格式如下:

package name // 其中 package 是声明包名的关键字, name 为包的名字。

Go 语言的包与文件夹是一一对应的,它具有以下几点特性:

  • 一个目录下的同级文件属于同一个包。
  • 包名可以与其目录名不同。
  • main 包是 Go 语言程序的入口包,一个 Go 语言程序必须有且仅有一个 main 包。如果一个程序没有 main 包,那么编译时将会出错,无法生成可执行文件。

import(导入包)

在包声明之后,是 import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""包围,格式如下:

import "name" // 其中 import 是导入包的关键字,name 为所导入包的名字。

代码第 4 行导入了 fmt 包,这行代码会告诉 Go 编译器,我们需要用到 fmt 包中的函数或者变量等,fmt 包是 Go 语言标准库为我们提供的,用于格式化输入输出的内容,类似的还有 os 包、io 包等。

另外有一点需要注意,导入的包中不能含有代码中没有使用到的包,否则 Go 编译器会报编译错误,例如 imported and not used: "xxx",”xxx” 表示包名。

也可以使用一个 import 关键字导入多个包,此时需要用括号( )将包的名字包围起来,并且每个包名占用一行,也就是写成下面的样子:

import(  
    "name1"  
    "name2"  
)  

main 函数

第 5 行代码创建了一个 main 函数,它是 Go 语言程序的入口函数,也即程序启动后运行的第一个函数。main 函数只能声明在 main 包中,不能声明在其他包中,并且,一个 main 包中也必须有且仅有一个 main 函数。

main 函数是自定义函数的一种,在 Go 语言中,所有函数都以关键字 func 开头的,定义格式如下所示:

func 函数名 (参数列表) (返回值列表){  
    函数体  
}  
​
注意:Go 语言函数的左大括号{必须和函数名称在同一行,否则会报错。

打印输出

代码的第 6 行fmt.Println("Hello Go!")中,Println 是 fmt 包中的一个函数,它用来格式化输出数据,比如字符串、整数、小数等。这里我们使用 Println 函数来打印字符串。注意,Println 函数打印完成后会自动换行,ln 是 line 的缩写。另外,代码fmt.Println("Hello Go!")的结尾,不需要使用;来作为结束符,Go 编译器会自动帮我们添加,当然,在这里加上;也是可以的。


Go变量与常量

变量

作用: 给一段指定的内存空间起名,方便操作这段内存。

声明变量的一般形式是使用 var 关键字。

方法一:声明一个变量, 默认的值是 0

package main  
  
import "fmt"  
  
func main(){  
    // 方法一:声明一个变量, 默认的值是0  
    var a int  
    fmt.Println("a = ", a)  
    fmt.Printf("a的类型是: %T\n", a)  
}

方法二:声明一个变量, 并初始化一个值

package main  
  
import "fmt"  
  
func main(){  
    // 方法二:声明一个变量, 初始化一个值  
    var b int = 100  
    fmt.Printf("b = %d, type of b = %T\n", b, b)  
  
    var bb string = "从0到Go语言微服务架构师"  
    fmt.Printf("bb = %s, bb的类型是: %T\n", bb, bb)  
}

方法三:在初始化的时候,可以省去数据类型,通过值去自动匹配当前变量的数据类型

package main  
  
import "fmt"  
  
func main(){  
  
    // 方法三:在初始化的时候,可以省去数据类型,通过值去自动匹配当前变量的数据类型  
    var c = 100  
    fmt.Printf("c = %d, type of c = %T\n", c, c)  
  
    var cc = "Go语言微服务架构师核心22讲"  
    fmt.Printf("cc = %s, cc的类型是: %T\n", cc, cc)  
}

短声明,只能在函数内

package main  
  
import "fmt"  
  
func main(){  
  
    // 方法四:(常用的方法) 省去var关键字,使用:=,既推导数据类型又赋值  
    // 注: 短声明是在函数或方法内部使用, 不支持全局变量声明!!!!  
    e := 100  
    fmt.Printf("e = %d, e的类型是: %T\n", e, e)  
  
    f := "Go语言极简一本通"  
    fmt.Printf("f = %s, f的类型是: %T\n", f, f)  
}

多变量声明

package main  
  
import "fmt"  
  
func main(){  
	// 声明多个变量  
    var xx, yy int = 100, 200  
    var kk, wx = 300, "write_code_666(欢喜哥)"  
  
    var (  
        nn int = 100  
        mm bool = true  
    )  
}

常量

常量(constant)表示固定的值。在计算机程序运行时,不会被程序修改的。

定义一个常量,使用 const 关键字。常量定义的时候就要赋值。

package main  
  
import "fmt"  
  
func main(){  
    // 常量(只读属性)  
    const length int = 10  
    // length = 100  // 常量是不允许被修改的  
    fmt.Println("length = ", length)  
}

使用 const 来定义枚举类型

package main  
  
import "fmt"  
  
// const来定义枚举类型  
const (  
    BEIJING = 0  
    SHANGHAI = 1  
    SHENZHEN = 2  
)  
  
func main() {  
    fmt.Println("BEIJING = ", BEIJING)      // 0  
    fmt.Println("SHANGHAI = ", SHANGHAI)    // 1  
    fmt.Println("SHENZHEN = ", SHENZHEN)    // 2  
}

iota

iota 是 Go 语言的常量计数器,只能在常量的表达式中使用。 iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。使用 iota 能简化定义,在定义枚举时很有用

package main  
  
import "fmt"  
  
// const来定义枚举类型  
const (  
    // 可以在const()中添加一个关键字iota, 每行的iota都会累加1, 第一行的iota默认是0  
    BEIJING = 10 * iota   // iota = 0  
    SHANGHAI			  // iota = 1  
    SHENZHEN			  // iota = 2  
)  
  
func main() {  
    fmt.Println("BEIJING = ", BEIJING)      // 0  
    fmt.Println("SHANGHAI = ", SHANGHAI)    // 10  
    fmt.Println("SHENZHEN = ", SHENZHEN)    // 20  
}

关键字

关键字是 Go 语言中预先保留的单词,在程序中有特殊含义,不能用来定义变量或常量。

Go 语言中有 25 个关键字:


Go基础数据类型

在静态类型语言(C++/Java/Golang 等)中规定在创建一个变量或者常量时,必须要指定出相应的数据类型,否则无法给变量分配内存。

整形

整型分两大类

  • 有符号整型:int8、int16、int32、int64、int。
  • 无符号整型:uint8、uint16、uint32、uint64、uint。
package main  
  
import (  
   "fmt"  
   "math"   "unsafe")  
  
/*  
----------有符号整型----------  
*/  
func Integer() {  
   var num8 int8 = 127  
   var num16 int16 = 32767  
   var num32 int32 = math.MaxInt32  
   //var num32 int32 = math.MinInt32  看最小值  
   var num64 int64 = math.MaxInt64  
   var num int = math.MaxInt  
   fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",  
      num8, unsafe.Sizeof(num8), num8)  
   fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",  
      num16, unsafe.Sizeof(num16), num16)  
   fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",  
      num32, unsafe.Sizeof(num32), num32)  
   fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",  
      num64, unsafe.Sizeof(num64), num64)  
   fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",  
      num, unsafe.Sizeof(num), num)  
}  
  
// 无符号整型  
func unsignedInteger() {  
   var num8 uint8 = 128  
   var num16 uint16 = 32768  
   var num32 uint32 = math.MaxUint32  
   var num64 uint64 = math.MaxUint64  
   var num uint = math.MaxUint  
   fmt.Printf("num8的类型是 %T, num8的大小 %d, num8是 %d\n",  
      num8, unsafe.Sizeof(num8), num8)  
   fmt.Printf("num16的类型是 %T, num16的大小 %d, num16是 %d\n",  
      num16, unsafe.Sizeof(num16), num16)  
   fmt.Printf("num32的类型是 %T, num32的大小 %d, num32是 %d\n",  
      num32, unsafe.Sizeof(num32), num32)  
   fmt.Printf("num64的类型是 %T, num64的大小 %d, num64是 %d\n",  
      num64, unsafe.Sizeof(num64), num64)  
   fmt.Printf("num的类型是 %T, num的大小 %d, num是 %d\n",  
      num, unsafe.Sizeof(num), num)  
}  
  
func main() {  
   Integer()  
   println("---------------------------------------")  
   unsignedInteger()  
}

Tips:

  • 除非对整型的大小有特定的需求,否则你通常应该使用 int 表示整型宽度,在 32 位系统下是 32 位,而在 64 位系统下是 64 位。表示范围:在 32 位系统下是 -2147483648 ~ 2147483647 ,而在 64 位系统是 -9223372036854775808 ~ 9223372036854775807 。
  • 对于 int8 , int16 等这些类型后面有跟一个数值的类型来说,它们能表示的数值个数是固定的。所以,在有的时候:例如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)等情况下,使用更加精确的 int32 和 int64 是更好的。

浮点型

浮点型表示存储的数据是实数,如 3.145。关于浮点型的说明,如表所示。 Go不同于C语言,Go的float类型是支持8位的长度

package main // 声明 main 包  
  
import (  
   "fmt"  
   "math") // 导入 fmt 包,打印字符串时需要用到  
  
func showFloat() {  
   var num1 float32 = math.MaxFloat32  
   var num2 float64 = math.MaxFloat64  
   fmt.Printf("num1的类型是%T,num1是%g\n", num1, num1)  
   fmt.Printf("num2的类型是%T,num1是%g\n", num2, num2)  
}  
  
func main() {  
   showFloat()  
   /*  
      num1的类型是float32,num1是3.4028235e+38  
      num2的类型是float64,num1是1.7976931348623157e+308  
   */}

Tips:

  • 通过上面的程序,我们知道浮点数能表示的数值很大,但是浮点数的精度却没有那么大:

    • float32 的精度只能提供大约 6 个十进制数(表示小数点后 6 位)的精度。
    • float64 的精度能提供大约 15 个十进制数(表示小数点后 15 位)的精度。

字符

字符串中的每一个元素叫作“字符”,定义字符时使用单引号。Go 语言的字符有两种,如表所示。

package main  
  
import (  
   "fmt"  
   "unsafe")  
  
func showChar() {  
   var x byte = 65  
   var y uint8 = 65  
   fmt.Printf("x = %c\n", x) // x = A  
   fmt.Printf("y = %c\n", y) // y = A  
}  
  
func sizeOfChar() {  
   var x byte = 65  
   fmt.Printf("x = %c\n", x)  
   fmt.Printf("x 占用 %d 个字节\n", unsafe.Sizeof(x))  
  
   var y rune = 'A' //rune 是占用4个字节的  
   fmt.Printf("y = %c\n", y)  
   fmt.Printf("y 占用 %d 个字节\n", unsafe.Sizeof(y))  
}  
  
func main() {  
   showChar()  
   fmt.Println("-------------")  
   sizeOfChar()  
   /*  
      x = A     
      y = A    
      -------------   
      x = A      x 占用 1 个字节  
      y = A      y 占用 4 个字节  
  
   */}

由此可见, byte 类型只能表示 28个值,所以你想表示其他一些值,例如中文的话,就得使用 rune 类型

var y rune = '牛'

字符串

字符串在 Go 语言中是以基本数据类型出现的,使用字符串就像使用其他原生基本数据类型 int、float32、float64、bool 一样。

示例程序:

package main  
  
import "fmt"  
  
func main() {  
   var study string      // 定义名为str的字符串类型变量  
   study = "《Go语言极简一本通》" // 将变量赋值  
  
   study2 := "《从0到Go语言微服务架构师》" // 以自动推断方式初始化  
   fmt.Println(study)  
   fmt.Println(study2)  
}

有些字符串没有现成的文字代号,所以只能用转义字符来表示。常用的转义字符如表所示。 定义多行字符串的方法如下。

  • 双引号书写字符串被称为字符串字面量(string literal),这种字面量不能跨行。
  • 多行字符串需要使用反引号“`”,多用于内嵌源码和内嵌数据。
  • 在反引号中的所有代码不会被编译器识别,而只是作为字符串的一部分。

多行字符串定义方式如例所示:

package main  
import "fmt"  
  
func main() {  
  var s1 string  
	s1 = `  
    		study := 'Go语言微服务架构核心22讲'  
    		fmt.Println(study)  
			`  
	fmt.Println(s1)  
}

布尔类型

关于 布尔(bool)  类型,无非就是两个值:true 或者 false 。

示例程序:

func showBool(){  
	a := true  
	b := false  
	fmt.Println("a=", a)  
	fmt.Println("b=", b)  
	fmt.Println("true && false = ", a && b)  
	fmt.Println("true || false = ", a || b)  
}  
  
func main() {  
    showBool()  
}

Tip:

  • 如果你学过其他编程语言或许会发现,布尔型可以参与数值运算,也可以与其他类型进行转换。但是在 Go 中,真值是用 true 表示,并且 不与 1 相等;同样的,假值是用 false 表示,并且 不与 0 相等。

复数型

复数型用于表示数学中的复数,如 1+2j、1-2j、-1-2j 等。在 Go 语言中提供了两种精度的复数类型:complex64 和 complex128 ,分别对应 float32 和 float64 两种浮点数精度,如表所示。

package main  
  
import "fmt"  
  
func showComplex() {  
   // 内置的 complex 函数用于构建复数  
   var x complex64 = complex(1, 2)  
   var y complex128 = complex(3, 4)  
   var z complex128 = complex(5, 6)  
   fmt.Println("x = ", x)  
   fmt.Println("y = ", y)  
   fmt.Println("z = ", z)  
   fmt.Println("-------------------")  
   // 内建的 real 和 imag 函数分别返回复数的实部和虚部  
   fmt.Println("real(x) = ", real(x))  
   fmt.Println("imag(x) = ", imag(x))  
   fmt.Println("y * z = ", y*z)  
}  
  
func main() {  
   showComplex()  
}
  • 当然,我们可以对声明进行简化,使用自然的方式书写复数:
x := 1 + 2i  
y := 3 + 4i  
z := 5 + 6i

fmt 格式输出


Go容器类-数组

数组

数组 是一个由 固定长度 的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。因为数组的长度是固定的,因此在 Go 语言中很少直接使用数组。和数组对应的类型是 slice(切片)  ,它是可以动态的增长和收缩的序列, slice 功能也更灵活,下面我们再讨论 slice 。

数组声明

可以使用 [n]Type 来声明一个数组。其中 n 表示数组中元素的数量, Type 表示每个元素的类型。

package main  
  
import "fmt"  
  
func test01() {  
   // 声明时没有指定数组元素的值, 默认为零值  
   var arr [5]int  
   fmt.Println(arr)  
  
   arr[0] = 1  
   arr[1] = 2  
   arr[2] = 3  
   fmt.Println(arr)  
}  
  
func test02() {  
   // 直接在声明时对数组进行初始化  
   var arr1 = [5]int{15, 20, 25, 30, 35}  
   fmt.Println(arr1)  
  
   // 使用短声明  
   arr2 := [5]int{15, 20, 25, 30, 35}  
   fmt.Println(arr2)  
  
   // 部分初始化, 未初始化的为零值  
   arr3 := [5]int{15, 20} // [15 20 0 0 0]  
   fmt.Println(arr3)  
  
   // 可以通过指定索引,方便地对数组某几个元素赋值  
   arr4 := [5]int{1: 100, 4: 200}  
   fmt.Println(arr4) // [0 100 0 0 200]  
  
   // 直接使用 ... 让编译器为我们计算该数组的长度  
   arr5 := [...]int{15, 20, 25, 30, 35, 40}  
   fmt.Println(arr5)  
}  
  
func test03() {  
   // 特别注意数组的长度是类型的一部分,所以 [3]int 和 [5]int 是不同的类型  
   arr1 := [3]int{15, 20, 25}  
   arr2 := [5]int{15, 20, 25, 30, 35}  
   fmt.Printf("type of arr1 is %T\n", arr1)  
   fmt.Printf("type of arr2 is %T\n", arr2)  
}  
  
func test04() {  
   // 定义多维数组  
   arr := [3][2]string{  
      {"1", "Go语言极简一本通"},  
      {"2", "Go语言微服务架构核心22讲"},  
      {"3", "从0到Go语言微服务架构师"}}  
   fmt.Println(arr)  
}  
  
func main() {  
   test01()  
   fmt.Println("----------------")  
   test02()  
   fmt.Println("----------------")  
   test03()  
   fmt.Println("----------------")  
   test04()  
   fmt.Println("----------------")  
  
}

数组长度

使用内置的 len 函数将返回数组中元素的个数,即数组的长度。

package main  
  
import "fmt"  
  
func main() {  
   arrLength()  
}  
func arrLength() {  
	arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}  
	fmt.Println("数组的长度是:", len(arr)) //数组的长度是: 3  
}

数组遍历

使用 for range 循环可以获取数组每个索引以及索引上对应的元素。

package main  
  
import "fmt"  
  
func main() {  
   showArr()  
}  
func showArr() {  
   arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}  
   for index, value := range arr {  
      fmt.Printf("arr[%d]=%s\n", index, value)  
   }  
   fmt.Println("-----------")  
   for _, value := range arr {  
      fmt.Printf("value=%s\n", value)  
   }  
}

数组是值类型

Go 中的数组是值类型而不是引用类型。当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。如果对新变量进行更改,不会影响原始数组。

package main  
  
import "fmt"  
  
func main() {  
   arrByValue()  
}  
  
/*  
	Go 中的数组是值类型而不是引用类型。  
	当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本。  
	如果对新变量进行更改,不会影响原始数组。  
*/  
func arrByValue() {  
   arr := [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}  
   copy := arr  
   copy[0] = "Golang"  
   fmt.Println(arr)  
   fmt.Println(copy)  
}

Go容器类-切片

切片(Slice)

切片是对数组的一个连续片段的引用,所以切片是一个引用类型。切片 本身不拥有任何数据,它们只是对现有数组的引用,每个切片值都会将数组作为其底层的数据结构。slice 的语法和数组很像,只是没有固定长度而已。

创建切片

使用 []Type 可以创建一个带有 Type 类型元素的切片。

// 声明整型切片  
var numList []int  
  
// 声明一个空切片  
var numListEmpty = []int{}

你也可以使用 make 函数构造一个切片,格式为 make([]Type, size, cap) 。

//切片是用make的做的
numList := make([]int, 3, 5)

当然,我们可以通过对数组进行片段截取创建一个切片。

arr := [5]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师", "微服务", "分布式"}  
var s1 = arr[1:4]  
fmt.Println(arr) // [Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师 微服务 分布式]  
fmt.Println(s1)  // [Go语言微服务架构核心22讲 从0到Go语言微服务架构师 微服务]

切片的长度和容量

一个 slice 由三个部分构成:指针 、 长度 和 容量 。指针指向第一个 slice 元素对应的底层数组元素的地址,要注意的是 slice 的第一个元素并不一定就是数组的第一个元素。长度对应 slice 中元素的数目;长度不能超过容量,容量一般是从 slice 的开始位置到底层数据的结尾位置。简单的讲,容量就是从创建切片索引开始的底层数组中的元素个数,而长度是切片中的元素个数。

内置的 len 和 cap 函数分别返回 slice 的长度和容量。

s := make([]string, 3, 5)  
fmt.Println(len(s)) // 3  
fmt.Println(cap(s)) // 5

如果切片操作超出上限将导致一个 panic 异常。

s := make([]int, 3, 5)  
fmt.Println(s[10]) //panic: runtime error: index out of range [10] with length 3

Tips:

由于 slice 是引用类型,所以你不对它进行赋值的话,它的默认值是 nil

var numList []int  
fmt.Println(numList == nil) // true
  • 切片之间不能比较,因此我们不能使用 == 操作符来判断两个 slice 是否含有全部相等元素。特别注意,如果你需要测试一个 slice 是否是空的,使用 len(s) == 0 来判断,而不应该用 s == nil 来判断。

切片元素的修改

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。

func modifySlice() {  
	var arr = [...]string{"Go语言极简一本通", "Go语言微服务架构核心22讲", "从0到Go语言微服务架构师"}  
	s := arr[:] //[0:len(arr)]  
	fmt.Println(arr) //[Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]  
	fmt.Println(s) //[Go语言极简一本通 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]  
  
	s[0] = "Go语言"  
	fmt.Println(arr) //[Go语言 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]  
	fmt.Println(s) //[Go语言 Go语言微服务架构核心22讲 从0到Go语言微服务架构师]  
}

这个很容易理解切片是引用类型的数据,数组是值拷贝的数据 这里的 arr[:] 没有填入起始值和结束值,默认就是 0 和 len(arr)

追加切片元素

使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(slice []Type, elems ...Type) []Type 。其中 elems ...Type 在函数定义中表示该函数接受参数 elems 的个数是可变的。这些类型的函数被称为可变函数。

package main  
  
import "fmt"  
  
func appendSliceData() {  
   s := []string{"Go语言极简一本通"}  
   fmt.Println(s)  
   fmt.Println(cap(s))  
  
   s = append(s, "Go语言微服务架构核心22讲")  
   fmt.Println(s)  
   fmt.Println(cap(s))  
  
   s = append(s, "从0到Go语言微服务架构师", "分布式")  
   fmt.Println(s)  
   fmt.Println(cap(s))  
  
   s = append(s, []string{"微服务", "分布式锁"}...)  
   fmt.Println(s)  
   fmt.Println(cap(s))  
  
}  
func main() {  
   appendSliceData()  
}

当新的元素被添加到切片时,如果容量不足,会创建一个新的数组。现有数组的元素被复制到这个新数组中,并返回新的引用。现在新切片的容量是旧切片的两倍。

多维切片

类似于数组,切片也可以有多个维度。

package main  
  
import (  
   "fmt"  
   "unsafe")  
  
func main() {  
   mSlice()  
}  
func mSlice() {  
   numList := [][]string{  
      {"1", "Go语言极简一本通"},  
      {"2", "Go语言微服务架构核心22讲"},  
      {"3", "从0到Go语言微服务架构师"}, {"4", "Beego"},  
   }  
   fmt.Println(numList)  
   fmt.Println("s的类型%T", unsafe.Sizeof(numList)) //固定24  
}

Go容器类-Map

Map

在 Go 语言中,map 是散列表(哈希表)的引用。它是一个拥有键值对元素的无序集合,在这个集合中,键是唯一的,可以通过键来获取、更新或移除操作。无论这个散列表有多大,这些操作基本上是通过常量时间完成的。所有可比较的类型,如 整型 ,字符串 等,都可以作为 key 。

创建 Map

使用 make 函数传入键和值的类型,可以创建 map 。具体语法为 make(map[KeyType]ValueType) 。

// 创建一个键类型为 string 值类型为 int 名为 scores 的 map  
scores := make(map[string]int)  
steps := make(map[string]string)

我们也可以用 map 字面值的语法创建 map ,同时还可以指定一些最初的 key/value :

var steps2 map[string]string = map[string]string{  
		"第一步": "Go语言极简一本通",  
		"第二步": "Go语言微服务架构师核心22讲",  
		"第三步": "从0到Go语言微服务架构师",  
}  
fmt.Println(steps2)

或者

steps3 := map[string]string{  
		"第一步": "Go语言极简一本通",  
		"第二步": "Go语言微服务架构师核心22讲",  
		"第三步": "从0到Go语言微服务架构师",  
}  
fmt.Println(steps3)

Map 操作

  • 添加元素
// 可以使用 `map[key] = value`map 添加元素。  
steps3["第四步"] = "总监"

更新元素

// 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。  
steps3["第四步"] = "CTO"

获取元素

// 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。  
fmt.Println(steps3["第四步"] )

删除元素

//使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。  
delete(steps3, "第四步")

判断 key 是否存在

// 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]  
v3, ok := steps3["第三步"]  
fmt.Println(ok)  
fmt.Println(v3)  
  
v4, ok := steps3["第四步"]  
fmt.Println(ok)  
fmt.Println(v4)
  • 这个语句说明 map 的下标读取可以返回两个值,第一个值为当前 key 的 value 值,第二个值表示对应的 key 是否存在,若存在 ok 为 true ,若不存在,则 ok 为 false 。
  • 遍历 map
// 遍历 map 中所有的元素需要用 for range 循环。  
for key, value := range steps3 {  
    fmt.Printf("key: %s, value: %d\n", key, value)  
}

获取 map 长度

fmt.Println(len(steps3))

package main  
  
import "fmt"  
  
func main() {  
   MapOperate()  
}  
func MapOperate() {  
  
   steps3 := map[string]string{  
      "第一步": "Go语言极简一本通",  
      "第二步": "Go语言微服务架构师核心22讲",  
      "第三步": "从0到Go语言微服务架构师",  
   }  
   fmt.Println(steps3)  
   fmt.Println("向 map 添加元素-------------")  
   // 可以使用 `map[key] = value` 向 map 添加元素。  
   steps3["第四步"] = "总监"  
   fmt.Println(steps3)  
   fmt.Println("若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值-------------")  
   // 若 key 已存在,使用 map[key] = value 可以直接更新对应 key 的 value 值。  
   steps3["第四步"] = "CTO"  
   fmt.Println(steps3)  
   fmt.Println(" 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零-------------")  
   // 直接使用 map[key] 即可获取对应 key 的 value 值,如果 key不存在,会返回其 value 类型的零值。  
   fmt.Println(steps3["第四步"])  
   fmt.Println(steps3)  
   fmt.Println("使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错-------------")  
   //使用 delete(map, key)可以删除 map 中的对应 key 键值对,如果 key 不存在,delete 函数会静默处理,不会报错。  
   delete(steps3, "第四步")  
   fmt.Println(steps3)  
   // 如果我们想知道 map 中的某个 key 是否存在,可以使用下面的语法:value, ok := map[key]  
  
}

map 是引用类型

当 map 被赋值为一个新变量的时候,它们指向同一个内部数据结构。因此,改变其中一个变量,就会影响到另一变量。

func mapByReference() {  
		steps4 := map[string]string{  
		"第一步": "Go语言极简一本通",  
		"第二步": "Go语言微服务架构师核心22讲",  
		"第三步": "从0到Go语言微服务架构师",  
	}  
	fmt.Println("steps4: ", steps4)  
	// steps4:  map[第一步:Go语言极简一本通 第三步:从0到Go语言微服务架构师 第二步:Go语言微服务架构师核心22讲]  
	newSteps4 := steps4  
	newSteps4["第一步"] = "Go语言极简一本通-222"  
	newSteps4["第二步"] = "Go语言微服务架构师核心22讲-222"  
	newSteps4["第三步"] = "从0到Go语言微服务架构师-222"  
	fmt.Println("steps4: ", steps4)  
  // steps4:  map[第一步:Go语言极简一本通-222 第三步:从0到Go语言微服务架构师-222 第二步:Go语言微服务架构师核心22讲-222]  
	fmt.Println("newSteps4: ", newSteps4)  
  // newSteps4:  map[第一步:Go语言极简一本通-222 第三步:从0到Go语言微服务架构师-222 第二步:Go语言微服务架构师核心22讲-222]  
}

当 map 作为函数参数传递时也会发生同样的情况。