一点Golang的学习笔记

在这篇文章中,我将会陆续分享一下我学习golang的时候的一些笔记,同时借助整理笔记来复习一下golang基础。因为整理笔记有点繁琐费时间,加上学业繁忙,所以将会是不定期的上传。

笔记的内容主要来自于 千锋Golang基础教学视频2019版 戳我直达视频地址 和 李文周的博客戳我直达博客地址

编码规范

命名规范

Go的命名规则涉及变量、常量、全局函数、结构、接口、方法等的命名。

区分大小写

Go在命名时以字母a到z或A到Z或下划线开头,后面跟着字母、下划线和数字(0到9)。Go不允许在命名时中使用@、$和%等标点符号。Go是一种区分大小写的编程语言。因此,Manpower和manpower是两个不同的命名。

私有和公开

Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则应该以小写字母开头。

  1. 当命名(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public)
  2. 命名如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )
标识符

在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123。

关键字

关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。

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

此外,Go语言中还有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
包命名

保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,尽量和标准库不要冲突。包名应该为小写单词,不要使用下划线或者混合大小写。

1
2
package demo
package main
文件命名

尽量采取有意义的文件名,简短,有意义,应该为小写单词,使用下划线分隔各个单词。

1
my_test.go
结构体命名
  • 采用驼峰命名法,首字母根据访问控制大写或者小写

  • struct 申明和初始化格式采用多行,例如下面:

1
2
3
4
5
6
7
8
9
10
11
// 多行申明
type User struct{
Username string
Email string
}

// 多行初始化
u := User{
Username: "astaxie",
Email: "astaxie@gmail.com",
}
接口命名
  • 命名规则基本和上面的结构体类型
  • 单个函数的结构名以 “er” 作为后缀,例如 Reader , Writer 。
1
2
3
type Reader interface {
Read(p []byte) (n int, err error)
}
变量命名
  • 和结构体类似,变量名称一般遵循驼峰法,首字母根据访问控制原则大写或者小写,但遇到特有名词时,需要遵循以下规则: [总的来说,特有名次的所有字母都应该和该特有名词的首字母状态保持一致,该首字母大小写状态根据私有/公有/位置灵活确定]
    • 如果变量为私有,且特有名词为首个单词,则使用小写,如 apiClient
    • 其它情况都应当使用该名词原有的写法,如 APIClient、repoID、UserID
    • 错误示例:UrlArray,应该写成 urlArray 或者 URLArray
  • 若变量类型为 bool 类型,则名称应以 Has, Is, Can 或 Allow 开头
1
2
3
4
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
常量命名

常量均需使用全部大写字母组成,并使用下划线分词

1
const APP_VER = "1.0"

如果是枚举类型的常量,需要先创建相应类型:

1
2
3
4
5
6
type Scheme string

const (
HTTP Scheme = "http"
HTTPS Scheme = "https"
)

注释

Go提供C风格的/* */块注释和C ++风格的//行注释。

  • 你可以在任何地方使用以 // 开头的单行注释
  • 多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段

go 语言自带的 godoc 工具可以根据注释生成文档,生成可以自动生成对应的网站( golang.org 就是使用 godoc 工具直接生成的),注释的质量决定了生成的文档的质量。每个包都应该有一个包注释,在package子句之前有一个块注释。对于多文件包,包注释只需要存在于一个文件中,任何一个都可以。包评论应该介绍包,并提供与整个包相关的信息。它将首先出现在godoc页面上,并应设置下面的详细文档。

详细的如何写注释可以
参考:http://golang.org/doc/effective_go.html#commentary

代码风格

语句的结尾

Go语言中是不需要类似于Java需要冒号结尾,默认一行就是一条数据

如果你打算将多个语句写在同一行,它们则必须使用 ;

括号和空格

括号和空格方面,也可以直接使用 gofmt 工具格式化。go 会强制左大括号不换行,换行会报语法错误,所有的运算符和操作数之间要留空格(非强制,但建议)。

1
2
3
4
5
6
7
8
9
10
// 正确的方式
if a > 0 {

}

// 错误的方式
if a>0 // a ,0 和 > 之间应该空格
{ // 左大括号不可以换行,会报语法错误

}
import 规范

import在多行的情况下,goimports会自动帮你格式化,但如果你在一个文件里面引入了一个package,还是建议采用如下格式:

1
2
3
import (
"fmt"
)

如果你的包引入了三种类型的包,标准库包,程序内部包,第三方包,建议采用如下方式进行组织你的包:

1
2
3
4
5
6
7
8
9
10
11
import (
"encoding/json"
"strings"

"myproject/models"
"myproject/controller"
"myproject/utils"

"github.com/astaxie/beego"
"github.com/go-sql-driver/mysql"
)

有顺序的引入包,不同的类型采用空格分离,第一种实标准库,第二是项目包,第三是第三方包。

在项目中不要使用相对路径引入第三方包:

1
2
3
4
5
// 这是不好的导入
import “../net”

// 这是正确的做法
import “github.com/repo/proj/src/net”

但是如果是引入本项目中的其他包,最好使用相对路径。

错误处理
  • 错误处理的原则就是不能丢弃任何有返回err的调用,不要使用 _ 丢弃,必须全部处理。接收到错误,要么返回err,或者使用log记录下来
  • 尽早return:一旦有错误发生,马上返回
  • 尽量不要使用panic,除非你知道你在做什么
  • 错误描述如果是英文必须为小写,不需要标点结尾
  • 采用独立的错误流进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
// 错误写法
if err != nil {
// error handling
} else {
// normal code
}

// 正确写法
if err != nil {
// error handling
return // or continue, etc.
}
// normal code

– 不过说实话,习惯了try catch语法的我初学go时对它的错误处理真的很不习惯。

测试

单元测试文件名命名规范为 example_test.go,必须以 _test 结尾,这样go才能识别这个源文件为单元测试文件名。
测试用例的函数名称必须以 Test 开头,例如:TestExample
每个重要的函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试

待测函数

1
2
3
func Add(a, b int) int {
return a + b
}

测试函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "testing"

func TestAdd(t *testing.T) {
cases := []struct {
first int
second int
excepted int
}{
{1, 2, 3},
{1, 2, 4},
}

for _, c := range cases {
result := add(c.first, c.second)
if result != c.excepted {
t.Fatalf("add function failed, first: %d, second:%d, execpted:%d, result:%d", c.first, c.second, c.excepted, result)
}
}
}

变量常量

变量

什么是变量

变量是为存储特定类型的值而提供给内存位置的名称。在go中声明变量有多种语法。

所以变量的本质就是一小块内存,用于存储数据,在程序运行过程中数值可以改变。

变量的声明

单变量声明

Go语言的变量声明格式为:

1
var 变量名 变量类型

第一种,指定变量类型,声明后若不赋值,使用默认值

以关键字var开头,变量类型放在变量的后面,行尾无需分号。例如

1
2
3
var name string
var age int
var isOk bool

第二种,根据值自行判定变量类型(类型推断Type inference)

如果一个变量有一个初始值,编译器会根据等号右边的值来推导变量的类型完成初始化。因此,如果变量具有初始值,则可以省略变量声明中的类型。

注意,虽然go可以省略变量类型来声明变量,但实际上go还是一个强类型的语言.

1
var name = value

第三种,省略var,(简短声明)

:=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误

注意,这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值

1
2
3
4
5
6
name := value

// 例如
var a int = 10
var b = 10
c : = 10

多变量声明

每声明一个变量就需要写var关键字会比较繁琐,go语言中还支持批量变量声明:

第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值

前面的几个变量的type都相同时,可以直接在最后一个变量后面写一个type类型,表示这几个变量的类型都是这个type

1
2
var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3

第二种,直接赋值,下面的变量类型可以是不同的类型

1
var name1, name2, name3 = v1, v2, v3

第三种,集合类型

1
2
3
4
var (
name1 type1
name2 type2
)

例如:

1
2
3
4
5
6
var (
a string
b int
c bool
d float32
)
匿名变量

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线_表示,例如:

1
2
3
4
5
6
7
8
9
func foo() (int, string) {
return 10, "Q1mi"
}
func main() {
x, _ := foo()
_, y := foo()
fmt.Println("x=", x)
fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 在某些编程语言里,匿名变量也被叫做哑元变量。

注意事项
  • 变量必须先定义才能使用
  • go语言是强类型静态语言,要求变量的类型和赋值的类型必须一致。
  • 变量名不能冲突。(同一个作用于域内不能冲突)
  • 简短定义方式,左边的变量名至少有一个是新的
  • 简短定义方式:=不能使用在函数外,即不能定义全局变量。
  • 变量的零值。也叫默认值。
  • 变量定义了就要使用,否则无法通过编译。
  • 函数外的每个语句都必须以关键字开始(var、const、func等)
  • _多用于占位,表示忽略值。

已经被初始化声明的变量不可再次被初始化声明,只能对该变量进行赋值操作。否则编译器报错。

如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,此外,单纯地给 a 赋值也是不够的,这个值必须被使用。

在同一个作用域中,已存在同名的变量,则之后的声明初始化,则退化为赋值操作。但这个前提是,最少要有一个新的变量被定义,且在同一作用域,例如,下面的y就是新定义的变量

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

import (
"fmt"
)

func main() {
x := 140
fmt.Println(&x)
x, y := 200, "abc"
fmt.Println(&x, x)
fmt.Print(y)
}

运行结果:

1
2
3
0xc04200a2b0
0xc04200a2b0 200
abc

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。

常量的声明

常量的声明和变量声明非常类似,只是把var换成了const

常量在定义的时候必须赋值,且在运行过程中不能也不可能被改变,如果常量初始化赋值后尝试对其再次赋值,编译器会报错不通过。

1
const identifier [type] = value

当然,常量也是可以类型推导的

1
2
const b string = "abc" //显式类型定义
const b = "abc" //类型推导,隐式类型定义

例:

1
2
const pi = 3.1415
const e = 2.7182

声明了pie这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了。

多个常量也可以一起声明,组成常量组

1
2
3
4
const (
pi = 3.1415
e = 2.7182
)

const同时声明多个常量时,如不指定类型和初始化值,则与上一行非空常量右值相同

1
2
3
4
5
6
const (
x uint16 = 16
y
s = "abc"
z
)

运行结果:

1
2
uint16,16
string,abc

常量的注意事项:

  • 常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型

  • 不曾使用的常量,在编译的时候,是不会报错的(变量声明了就必须使用)

iota

iota,特殊常量,可以认为是一个可以被编译器修改的常量.

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

iota 可以被用作枚举值:

1
2
3
4
5
6
const (
n1 = iota //0
n2 //1
n3 //2
n4 //3
)

等同于,虽然每项都等于iota,但是iota的值在上一个常量被初始化声明后变化了,导致该常量的值与上一个不同

1
2
3
4
5
6
const (
n1 = iota //0
n2 = iota //1
n3 = iota //2
n4 = iota //3
)

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:

1
2
3
4
5
const (
a = iota
b
c
)
iota 用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}

运行结果:

1
0 1 2 ha ha 100 100 7 8

其他示例:

使用_跳过某些值

1
2
3
4
5
6
const (
n1 = iota //0
n2 //1
_
n4 //3
)

iota声明中间插队

1
2
3
4
5
6
7
const (
n1 = iota //0
n2 = 100 //100
n3 = iota //2
n4 //3
)
const n5 = iota //0

定义数量级 (这里的<<表示左移操作,1<<10表示将1的二进制表示向左移10位,也就是由1变成了10000000000,也就是十进制的1024。同理2<<2表示将2的二进制表示向左移2位,也就是由10变成了1000,也就是十进制的8。)

1
2
3
4
5
6
7
8
const (
_ = iota
KB = 1 << (10 * iota)
MB = 1 << (10 * iota)
GB = 1 << (10 * iota)
TB = 1 << (10 * iota)
PB = 1 << (10 * iota)
)

多个iota定义在一行

1
2
3
4
5
const (
a, b = iota + 1, iota + 2 //1,2
c, d //2,3
e, f //3,4
)

如果中断iota自增,则必须显式恢复。且后续自增值按行序递增

自增默认是int类型,可以自行进行显示指定类型

数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址【数字常量是获取某一固定内存块】

零值

Go任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是””,而指针,函数,interface,slice,channel和map的零值都是nil。

基本数据类型和运算符

基本数据类型

go中可用的基本数据类型:

类型 描述
布尔bool bool
数值型 int,int8/16/32/64,uint,uint8/16/32/64,float32/64,complex64/128,byte/rune
字符串类型 string
布尔型bool

布尔型的值只可以是常量 true 或者 false。

数值型

整型

基本整型

整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

类型 描述
uint8 无符号 8位整型 (0 到 255)
uint16 无符号 16位整型 (0 到 65535)
uint32 无符号 32位整型 (0 到 4294967295)
uint64 无符号 64位整型 (0 到 18446744073709551615)
int8 有符号 8位整型 (-128 到 127)
int16 有符号 16位整型 (-32768 到 32767)
int32 有符号 32位整型 (-2147483648 到 2147483647)
int64 有符号 64位整型 (-9223372036854775808 到 9223372036854775807)

特殊整型

类型 描述
uint 32位操作系统上就是uint32,64位操作系统上就是uint64
int 32位操作系统上就是int32,64位操作系统上就是int64
uintptr 无符号整型,用于存放一个指针

注意:在使用intuint类型时,不能假定它是32位或64位的整型,而是考虑intuint可能在不同平台上的差异。

获取对象的长度的内置函数len()返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

数字字面量语法

Go1.13版本之后引入了数字字面量语法(Number literals syntax),这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。 v := 0o377,代表八进制的 377,相当于十进制的 255。 v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。 而且还允许我们用 _ 来分隔数字,比如说:

v := 123_456 等于 123456。

我们可以借助fmt函数来将一个整数以不同进制形式展示。

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

import "fmt"

func main(){
// 十进制
var 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
}

浮点型

Go语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

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

复数

complex64和complex128

1
2
3
4
5
6
var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

byte和rune类型

组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

1
2
var a := '中'
var b := 'x'

Go 语言的字符有以下两种:

  1. uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
  2. rune类型,代表一个 UTF-8字符

当需要处理中文、日文或者其他复合字符时,则需要用到rune类型。rune类型实际是一个int32

Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾。

1
2
3
4
5
6
7
8
9
10
11
12
// 遍历字符串
func traversalString() {
s := "hello沙河"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}

输出:

1
2
104(h) 101(e) 108(l) 108(l) 111(o) 230(æ) 178(²) 153() 230(æ) 178(²) 179(³) 
104(h) 101(e) 108(l) 108(l) 111(o) 27801(沙) 27827(河)

因为UTF8编码下一个中文汉字由3~4个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果。

字符串底层是一个byte切片,所以可以和[]byte类型相互转换。字符串是不能修改的 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。 rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。

字符串

字符串底层是一个byte切片,我们可以通过下标来访问字符串中的字符,但是我们不能修改字符,修改必须将字符串转换成字节数组后再修改,修改后再转换成字符串。具体可见下文。

下标访问字符示例:

1
2
3
4
5
6
7
8
name := "Hello World"
for i:= 0; i < len(s); i++ {
fmt.Printf("%d ", s[i])
}
fmt.Printf("\n")
for i:= 0; i < len(s); i++ {
fmt.Printf("%c ",s[i])
}

Go语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用UTF-8编码。 字符串的值为双引号(")中的内容,可以在Go语言的源码中直接添加非ASCII码字符,例如:

1
2
s1 := "hello"
s2 := "你好"

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,和其他语言类似

转义符 含义
\r 回车符(返回行首)
\n 换行符(直接跳到下一行的同列位置)
\t 制表符
\' 单引号
\" 双引号
\\ 反斜杠

多行字符串

Go语言中要定义一个多行字符串时,就必须使用反引号字符:

1
2
3
4
5
s1 := `第一行
第二行
第三行
`
fmt.Println(s1)

反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

字符串的常用操作

方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

修改字符串

要修改字符串,需要先将其转换成[]rune[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。

1
2
3
4
5
6
7
8
9
10
11
12
func changeString() {
s1 := "big"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'p'
fmt.Println(string(byteS1))

s2 := "白萝卜"
runeS2 := []rune(s2)
runeS2[0] = '红'
fmt.Println(string(runeS2))
}
Go的类型转换

Go语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用。

语法格式:Type(Value)

常数:在有需要的时候,会自动转型

变量:需要手动转型 T(V)

复合类型(派生类型)

1、指针类型(Pointer)
2、数组类型
3、结构化类型(struct)
4、Channel 类型
5、函数类型 (func)
6、切片类型(slice)
7、接口类型(interface)
8、Map 类型(map)

运算符

go语言的运算符规则同C

算术运算符
1
+ - * / %(求余)

注意*: ++(自增)和 –(自减)在Go语言中是单独的语句,并不是运算符。

关系运算符
1
== != > < >= <=
逻辑运算符
1
2
3
&& 
||
!
位运算符
1
2
3
4
5
6
&   且
| 或
^ 异或
&^ 二进制位清空
<< 左移位
>> 右移位
赋值运算符

和其他语言相同

1
2
3
4
5
6
7
8
9
10
11
=
+=
-=
*=
/=
%=
<<= 左移位并赋值运算符
>>= 右移位并赋值运算符
&= 按位与赋值运算符
^= 按位异或并赋值运算符
|= 按位或并赋值运算符
运算符优先级
优先级 运算符
1 ~ ! ++ –
2 * / % << >> & &^
3 + - ^
4 == != < <= >= >
5 <-
6 &&
7 ||

当然,你可以通过使用括号来临时提升某个表达式的整体运算优先级。

流程控制

go语言的流程控制语句总共只有以下几种:

  1. if else (条件判断)
  2. for、for range (键值循环)
  3. switch case (选择)
  4. goto (跳转到指定标签)
  5. break (跳出循环)
  6. continue (结束当前循环,继续下次循环)

注意:go语言中是没有 while 和 do…while 语句的,但是使用 for 或 for+if 是可以模拟出这两个循环语句的

条件分支语句

if else条件判断语句

单if

1
2
3
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}

if else

1
2
3
4
5
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}

if - else if - else

1
2
3
4
5
6
7
if 布尔表达式1 {
/* 在布尔表达式1为 true 时执行 */
} else if 布尔表达式2{
/* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
} else{
/* 在上面两个布尔表达式都为false时,执行*/
}

Go语言规定与if匹配的左括号{必须与if和表达式放在同一行,{放在其他位置会触发编译错误。 同理,与else匹配的{也必须与else写在同一行,else也必须与上一个ifelse if右边的大括号在同一行。func、struct 的 { 也是。

if 条件判断特殊写法

if条件判断还有一种特殊的写法,可以在 if 表达式之前添加一个执行语句,再根据变量值进行判断

1
2
3
4
5
6
if statement; condition {  
}

if condition{

}

示例代码:

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

import (
"fmt"
)

func main() {
if num := 10; num % 2 == 0 { //checks if number is even
fmt.Println(num,"is even")
} else {
fmt.Println(num,"is odd")
}
}

注意,num的定义在if里,那么只能够在该if..else语句块中使用。

switch语句

switch是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较,并根据匹配执行代码块。它可以被认为是一种惯用的方式来写多个if else子句,使用switch语句可方便地对大量的值进行条件判断。

语法

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。

switch 语句每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止,如果都不匹配则会执行default语句块。

Go语言规定每个switch只能有一个default分支,case后的常量值不能重复。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func switchDemo1(finger int) {
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("无效的输入!")
}
}

和其他语言(C++/Java)不同的,Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用 fallthrough 强制执行后面的case代码。

  • 注意:fallthrough应该是某个case的最后一行。如果它出现在中间的某个地方,编译器就会抛出错误。

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func switchDemo5() {
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("...")
}
}

输出

1
2
a
b

分支还可以使用表达式,这时候switch语句后面不需要再跟判断变量。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
func switchDemo4() {
age := 30
switch {
case age < 25:
fmt.Println("好好学习吧")
case age > 25 && age < 35:
fmt.Println("好好工作吧")
case age > 60:
fmt.Println("好好享受吧")
default:
fmt.Println("活着真好")
}
}

同时,一个分支可以有多个值,多个case值中间使用英文逗号分隔。如:

1
2
3
4
5
6
7
8
9
10
func testSwitch3() {
switch n := 7; n {
case 1, 3, 5, 7, 9:
fmt.Println("奇数")
case 2, 4, 6, 8:
fmt.Println("偶数")
default:
fmt.Println(n)
}
}
Type Switch

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

1
2
3
4
5
6
7
8
9
switch x.(type){
case type:
statement(s);
case type:
statement(s);
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s);
}

例:

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

import "fmt"

func main() {
var x interface{}

switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}

运行结果:

1
x 的类型 :<nil>

循环语句

Go 语言中的所有循环类型均可以使用for关键字来完成。

基本语法的for

for循环的基本格式如下,和C++,Java中的for语句类似(但是少了括号):

1
2
3
for 初始语句;条件表达式;结束语句{
循环体语句
}
省略语句的for

所有的三个组成部分,即初始化、条件和post都是可选的。

for循环的初始语句可以被忽略,但是初始语句后的分号必须要写,例如:

1
2
3
4
5
6
func forDemo() {
i := 0
for ; i < 10; i++ {
fmt.Println(i)
}
}

for循环的初始语句和结束语句都可以省略,效果与其他语言中的while相似,注意go中是没有while的

1
2
3
4
5
6
7
func forDemo() {
i := 0
for i < 10 {
fmt.Println(i)
i++
}
}

无限循环

1
2
3
for {
循环体语句
}

for循环可以通过breakgotoreturnpanic语句强制退出循环。

for range(键值循环)

go语言中可以使用for range遍历数组array、切片slice、字符串string、map 及通道(channel)。 通过for range遍历的返回值有以下规律:

  1. 数组、切片、字符串返回索引和值。

    1
    for i,v:= range array
  2. map返回键和值。

    1
    for k,v:= range map
  3. 通道(channel)只返回通道内的值。

    1
    for v := range channel

    注意:在需要时,可以使用匿名变量对 for range 的变量进行选取。

其他语句

goto

goto语句通过标签进行代码间的无条件跳转。goto语句可以在快速跳出循环、避免重复退出上有一定的帮助。类似汇编语言中的goto

break

break语句可以结束forswitchselect的代码块。

break语句还可以在语句后面添加标签,表示退出某个标签对应的代码块,标签要求必须定义在对应的forswitchselect的代码块上。 如:

1
2
3
4
5
6
7
8
9
10
11
12
func breakDemo1() {
BREAKDEMO1:
for i := 0; i < 10; i++ {
for j := 0; j < 10; j++ {
if j == 2 {
break BREAKDEMO1
}
fmt.Printf("%v-%v\n", i, j)
}
}
fmt.Println("...")
}
continue

continue语句可以结束当前循环,开始下一次的循环迭代过程,仅限在for循环内使用,作用和其他语言continue类似。

但是如果是嵌套循环时,在 continue语句后添加标签时,表示开始标签对应的循环。如

1
2
3
4
5
6
7
8
9
10
11
12
func continueDemo() {
forloop1:
for i := 0; i < 5; i++ {
// forloop2:
for j := 0; j < 5; j++ {
if i == 2 && j == 2 {
continue forloop1
}
fmt.Printf("%v-%v\n", i, j)
}
}
}

数组

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。

1
2
// 定义一个长度为3元素类型为int的数组a
var a [3]int

数组定义

声明需要指明数组的大小和存储的数据类型。如果忽略 [ ] 中的数字不设置数组大小,Go 语言会根据数组初始化时元素的个数来设置数组的大小。

1
var 数组变量名 [元素数量]T

比如:var a [5]int, 数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变。

注意:[5]int[10]int是不同的类型。

1
2
3
var a [3]int
var b [4]int
a = b //不可以这样做,因为此时a和b是不同的类型

数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1,访问越界(下标在合法范围之外),则触发访问越界,会panic。

数组初始化

数组可以在声明数组的时候初始化,如果声明时没有手动初始化,那么编译器将自动初始化该数组,值为数组数据类型类型的零值

1
var arr [4] float32 // 等价于:var arr = [4]float32{}
初始化列表

使用初始化列表来设置数组元素的值。注意:初始化数组中 { } 中的元素个数不能大于 [ ] 中的数字。

1
2
3
var array1= [3]int{}                        //数组会初始化为int类型的零值
var array2 = [3]int{1, 2} //使用指定的初始值完成初始化, 未指定到的默认为零值
var array3 = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
忽略数组长度

如果忽略 [ ] 中的数组的长度并用 ... 代替,Go 语言编译器会为你自动推导数组的长度

1
2
3
//例
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var cityArray = [...]string{"北京", "上海", "深圳"}
指定索引值

我们还可以使用指定索引值的方式来初始化数组,未指定的元素将会被初始化为零值,注意:索引是从0开始的,如:

1
2
var array = [5] int{4: 100} // [0 0 0 0 100]
var array = [...] int{0: 1, 4: 1, 9: 1} // [1 0 0 0 1 0 0 0 0 1]

数组的语法

访问数组元素

和其他语言一样,go语言中的数组可以通过其下标来访问对应的元素

1
2
var array = [3]string{"北京", "上海", "深圳"}
fmt.Println(array[2]) //输出 深圳
数组的长度

Go语言获取数组的长度语法和C语言类似,是通过将数组作为参数传递给len函数,可以获得数组的长度。

1
len(array)
数组的遍历

遍历数组a有以下两种方法:

for循环遍历

1
2
3
4
var a = [...]string{"北京", "上海", "深圳"}
for i := 0; i < len(a); i++ {
fmt.Println(a[i])
}

for range遍历

1
2
3
4
var a = [...]string{"北京", "上海", "深圳"}
for index, value := range a {
fmt.Println(index, value)
}

如果只需要值并希望忽略索引,那么可以通过使用_ 标识符替换索引来实现这一点,否则你必须使用索引index,如果不适用编译器将会报错。[因为go语言规定变量声明后必须使用]

多维数组

Go语言支持多维数组(数组中又嵌套数组)。

多维数组的声明

1
var 数组名 [第一层大小][第二层大小]...[第N层大小] 数据类型

二维数组的声明和初始化,更高维度类似

1
2
3
4
5
6
7
8
9
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]
fmt.Println(a[2][1]) //支持索引取值,输出 重庆
}

二维数组的遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
a := [3][2]string{
{"北京", "上海"},
{"广州", "深圳"},
{"成都", "重庆"},
}
for _, v1 := range a {
for _, v2 := range v1 {
fmt.Printf("%s\t", v2)
}
fmt.Println()
}
}

注意: 多维数组只有第一层可以使用...或 [ ] 来让编译器推导数组长度

数组是值类型

数组是值类型,复制和传参是值传递,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

什么是值传递

1
2
3
4
5
num := 10
num2:=num
fmt.Println("num:",num,",num2:",num2)
num=20
fmt.Println("num:",num,",num2:",num2)

num2:=num时,是将num的值复制一份传递给num2,这就是值传递

其他

  1. 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的(数组声明的时候就已经分配了内存)。
  2. [n]*T表示指针数组,*[n]T表示数组指针 。

切片

切片的本质

数组的长度是固定的并且数组长度属于类型的一部分,所以数组有很多的局限性,比如不能动态添加元素。

Go中提供了一种灵活,功能强悍的内置类型 切片slice (“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

切片Slice是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装,它非常灵活,支持自动扩容。切片本身没有任何数据。它们只是对现有数组的引用。与数组相比,切片不需要设定长度,即声明时在[ ]中不用设定值。

slice从底层来说,其实就是一个数据结构(struct结构体)。可以看出,切片是对数组的抽象。

1
2
3
4
5
type slice struct{
ptr *[]int //指针,指向数组中slice指定的开始位置
len int //长度,随元素数量的变化而变化
cap int //容量,也就是slice开始位置到数组的最后位置的长度,最大长度(可扩容的)
}

注意,切片是引用类型,数组是值类型

切片的定义和初始化

声明语法
1
var 变量名 []类型

例如

1
2
var a []string              //声明一个字符串切片
fmt.Println(a) //输出 []
初始化
1
2
3
4
5
6
7
8
9
10
var b = []int{}             //声明一个整型切片并初始化
var c = []bool{false, true} //声明一个布尔切片并初始化
var d = []bool{false, true} //声明一个布尔切片并初始化
fmt.Println(a) //[]
fmt.Println(b) //[]
fmt.Println(c) //[false true]
fmt.Println(a == nil) //true
fmt.Println(b == nil) //false
fmt.Println(c == nil) //false
// fmt.Println(c == d) //这样比较是错误的,切片是引用类型,不支持直接比较,只能和nil比较

注意:

1
2
var b = [...]int{123}	//这是声明一个数组,并使用编译器自动推断数组的长度
var b = []int{123} //这才是声明一个切片
切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式。

简单切片表达式

切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片。 切片表达式中的lowhigh表示一个索引范围(左包含,又不包含),也就是下面代码中从数组a中选出1<=索引值<4的元素组成切片s,得到的切片长度=high-low,容量等于得到的切片的底层数组的容量。

根据缺省条件可分为下面几种:

将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片(左闭右开),长度为 high-low

1
var s = arr[low:high]

例如

1
2
a := [5]int{1, 2, 3, 4, 5}
s := a[1:3] // s := a[low:high]

缺省endIndex时将表示一直到arr的最后一个元素

1
s := arr[low:]

缺省startIndex时将表示从arr的第一个元素开始

1
s := arr[:high]

全部缺省,表示直接从该数组的全部元素构建

1
s := arr[:]

注意:对于数组或字符串,必须保证索引不越界

对切片再执行切片表达式时(切片再切片),high的上限边界是切片的容量cap(a),而不是长度。常量索引必须是非负的,并且可以用int类型的值表示。对于数组或常量字符串,常量索引也必须在有效范围内。如果lowhigh两个指标都是常数,它们必须满足low <= high。如果索引在运行时超出范围,就会发生运行时panic

完整切片表达式

对于数组,指向数组的指针,或切片a(注意不能是字符串)支持完整切片表达式:

1
a[low : high : max]

上面的代码会构造与简单切片表达式a[low: high]相同类型、相同长度和元素的切片。另外,它会将得到的结果切片的容量设置为max-low。在完整切片表达式中只有第一个索引值(low)可以省略,但是冒号:不能省略,它默认为0。

1
2
3
a := [5]int{1, 2, 3, 4, 5}
t := a[1:3:5]
fmt.Printf("t:%v len(t):%v cap(t):%v\n", t, len(t), cap(t))

输出结果:

1
t:[2 3] len(t):2 cap(t):4

完整切片表达式需要满足的条件是0 <= low <= high <= max <= cap(a),其他条件和简单切片表达式相同。

动态定义

上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

make是内建函数,你可以在 http://docscn.studygolang.com/pkg/builtin/#make 这儿看到它,简单的说就是为slice、map和channel分配内存和初始化,返回一个引用类型的对象

1
make([]T, len, cap) //T:切片的元素类型,size:切片中元素的数量,cap:切片的容量

当然,你也可以不指定cap,这是cap默认等于len。返回的切片的数组元素全部为零值

切片的访问和修改

同数组一样,切片也可以通过下标来访问对应的元素,并修改它。

slice没有自己的任何数据。它只是底层数组的一个表示。对slice所做的任何修改都将反映在底层数组中。

也就是说,如果一个切片是根据一个数组创建的,那么如果我们对这个切片进行修改,其对应的底层数组也会改变

1
2
3
4
5
6
7
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)

输出

1
2
array before [57 89 90 82 100 78 67 69 59]  
array after [57 89 91 83 101 78 67 69 59]

先添加元素后再修改

1
2
3
4
5
6
7
8
darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}
dslice := darr[2:5]
fmt.Println("array before",darr)
dslice=append(dslice, 1000)
for i := range dslice {
dslice[i]++
}
fmt.Println("array after",darr)

输出

1
2
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 1001 67 69 59]

当多个片共享相同的底层数组时,每个元素所做的更改将在数组中反映出来。

1
2
3
4
5
6
7
8
numa := [3]int{78, 79 ,80}//创建一个数组
nums1 := numa[:] //根据数组创建一个切片
nums2 := numa[:]
fmt.Println("array before change 1",numa)
nums1[0] = 100
fmt.Println("array after modification to slice nums1", numa)
nums2[1] = 101
fmt.Println("array after modification to slice nums2", numa)

结果

1
2
3
array before change 1 [78 79 80]  
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]

切片的赋值拷贝

1
2
s1 := make([]int, 3) //[0 0 0]
s2 := s1 //将s1直接赋值给s2,s1和s2共用一个底层数组

对一个切片的修改会影响另一个切片的内容,如果不想有该特性,可以使用内建的copy函数,具体可见下文

切片的长度和容量

切片的长度是切片中元素的数量。切片的容量是从创建切片的索引开始的底层数组中元素的数量。

长度

切片的长度可以通过内建函数 len() 函数来获取

容量

切片的容量可以通过内建函数 cap() 函数来获取

注意,切片的长度是可以随着切片的增删来改变的。切片的容量也不是固定的,当我们在向切片slice append添加数据的时候,Golang 会检查底层的数组的长度是否已经不够,如果长度不够,Golang 则会新建一个数组,把原数组的数据拷贝过去,再将 slice 中的指向数组的指针指向新的数组,也就是扩容,这时候容量cap也会随之改变。

切片的遍历

切片的遍历方式和数组是一致的,支持索引遍历和for range遍历。

1
2
3
4
5
6
7
8
s := []int{1, 3, 5}
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}

for index, value := range s {
fmt.Println(index, value)
}

空切片

一个切片在未初始化之前默认为 nil,长度为 0

要检查切片是否为空,请始终使用len(s) == 0来判断,而不应该使用s == nil来判断。

切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个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来判断。

nil是指底层数组没有分配内存空间,len=0是指分配了空间但是没有填充数据元素。

切片的添加和复制

append添加

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

1
2
3
4
5
var s []int
s = append(s, 1) // [1]
s = append(s, 2, 3, 4) // [1 2 3 4]
s2 := []int{5, 6, 7}
s = append(s, s2...) // [1 2 3 4 5 6 7]

注意:通过var声明的零值切片可以在append()函数直接使用,无需初始化。

1
2
var s []int
s = append(s, 1, 2, 3)
切片的扩容

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值。

关于切片扩容的具体详情可以看这里 https://h3l.github.io/posts/slice-append-grow-analyze/

总结一下:slice 在 cap 长度小于 1024 之前容量是翻倍增长,在 cap 长度大于 1024 之后,因为存在着内存对齐,slice 的容量增长方式是最小增加 25%。

需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如intstring类型的处理方式就不一样。

copy复制

在上文的 切片的访问和修改 中提到过:当多个片共享相同的底层数组时,每个元素所做的更改将在数组中反映出来。

由于切片是引用类型,所以nums1和nums2其实都指向了同一块内存地址(数组numa)。修改nums2的同时nums1的值也会发生变化。

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用格式如下:

1
copy(destSlice, srcSlice []T)

其中:

  • srcSlice: 数据来源切片
  • destSlice: 目标切片

这样目标切片修改后源切片并不会受到影响

切片的删除

go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

1
2
3
4
5
// 从切片中删除元素
a := []int{30, 31, 32, 33, 34, 35, 36, 37}
// 要删除索引为2的元素
a = append(a[:2], a[3:]...)
fmt.Println(a) //[30 31 33 34 35 36 37]

总结一下就是:要从切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

Map

map是什么

Go语言中提供的映射关系容器为map,map 是一种无序的基于key-value键值对的集合,是Go中的内置类型。它将一个值与一个键关联起来,可以通过 key 来快速检索数据,key 类似于索引,指向数据的值。其内部使用散列表(hash)实现,因此,map 是无序的。

Go语言中的map是引用类型,必须初始化才能使用。

map的声明和初始化

Go语言中 map的声明语法如下:

1
2
/* 声明变量,默认 map 是 nil */
var map_variable map[keyType]valueType //KeyType:键的类型,ValueType:键对应的值的类型

map类型的变量默认初始值为nil,也就是还没有分配内存空间,需要使用make()函数来分配内存。语法为:

1
2
/* 使用 make 函数 */
map_variable = make(make(map[KeyType]ValueType, [cap]))

其中cap表示map的容量,该参数不是必须的。

也可以声明的时候直接初始化分配内存并填充键值对:

1
rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }

如果不初始化 map,那么就会创建一个 nil map。nil map 是不能用来存放键值对的。

map基本使用

map中的数据都是成对出现的

我们可以通过key获取map中对应的value值。语法为:

1
map[key]

但是当key如果不存在的时候,我们会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0。但是程序不会报错。

map的基本使用示例代码如下:

1
2
3
4
5
6
scoreMap := make(map[string]int, 8) //初始化
scoreMap["张三"] = 90 //添加键值对
scoreMap["小明"] = 100
fmt.Println(scoreMap) //打印所有的键值对 map[小明:100 张三:90]
fmt.Println(scoreMap["小明"]) //取出key对应的value 100
fmt.Printf("type of a:%T\n", scoreMap) //map的数据类型 type of a:map[string]int

map也支持在声明的时候填充元素,例如:

1
2
3
4
userInfo := map[string]string{
"username": "沙河小王子",
"password": "123456",
}

判断某个键是否存在

Go语言中有个判断map中键是否存在的特殊写法,这样通过判断ok获取key对应的value更加合理,格式如下:

1
value, ok := map[key] //如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值

map的遍历

Go语言中使用for range遍历map。

1
2
3
4
5
6
7
scoreMap := make(map[string]int)
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
fmt.Println(k, v)
}

但我们只想遍历key的时候,可以按下面的写法:

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

注意:遍历map时的元素顺序与添加键值对的顺序无关,因为map是基于hash表的,是无序的

按照指定顺序遍历map

正常使用for range对map进行遍历是,得到的是一个无序的结果,如果我们需要指定顺序的来遍历map,我们可以通过取出map所有的key,添加到切片slice中,对该slice进行排序,然后使用key来输出指定顺序的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
rand.Seed(time.Now().UnixNano()) //初始化随机数种子

var scoreMap = make(map[string]int, 200) //声明map

for i := 0; i < 100; i++ {
key := fmt.Sprintf("stu%02d", i) //生成stu开头的字符串
value := rand.Intn(100) //生成0~99的随机整数
scoreMap[key] = value
}
//取出map中的所有key存入切片keys
var keys = make([]string, 0, 200)
for key := range scoreMap {
keys = append(keys, key)
}
//对切片进行排序
sort.Strings(keys)
//按照排序后的key遍历map
for _, key := range keys {
fmt.Println(key, scoreMap[key])
}
}

map键值对的删除

我们可以使用delete()内建函数从map中删除一组键值对,参数为 map 和其对应的 key,删除函数不返回任何值。格式如下:

1
delete(map, key)   //map:要删除键值对的map,key:要删除的键值对的键

map的长度

使用len函数可以确定map的长度。

1
len(map)  // 可以得到map的长度

map是引用类型的

与切片相似,map映射是引用类型。当将映射分配给一个新变量时,它们都指向相同的内部数据结构。因此,改变其中一个变量时,另一个变量的值也会随之改变。

示例代码:

1
2
3
4
5
6
7
8
9
personSalary := map[string]int{
"steve": 12000,
"jamie": 15000,
}
personSalary["mike"] = 9000
fmt.Println("Original person salary", personSalary) //Original person salary map[steve:12000 jamie:15000 mike:9000]
newPersonSalary := personSalary
newPersonSalary["mike"] = 18000
fmt.Println("Person salary changed", personSalary) //Person salary changed map[steve:12000 jamie:15000 mike:18000]

注意:map不能使用==操作符进行比较。==只能用来检查map是否为空。否则会报错:invalid operation: map1 == map2 (map can only be comparedto nil)

和切片相互作用

切片中的元素为map类型
1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}
map中值为切片类型
1
2
3
4
5
6
7
8
9
10
11
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)
文章作者: Oxywen
文章链接: https://oxywen.cn/post/go/hello/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 不闻星河须臾