Go语言基础之变量、常量、内置类型以及设计技巧

上一篇Hello, Go已经接触了一个最简单的Go输出程序,这一篇通过例子来学习一下Go语言中的变量、常量、内置类型以及程序设计中的一些技巧。

变量

Go语言定义变量的方式有很多,使用var关键字是最基本的一种,与大部分语言不同的是Go把变量类型放在变量名后面

1
2
// 定义一个名为"variableName",类型为"type"的变量
var variableName type

定义多个变量

1
2
// 定义三个类型都为"type"的变量
var vname1, vname2, vname3 type

定义变量并初始化值

1
2
// 初始化名为"variableName",类型为"type"的变量,值为"value"
var variableName type = value

同时初始化多个变量

1
2
// 定义三个类型都是"type"的变量,并分别初始化相应的值
var vname1, vname2, vname3 type = v1, v2, v3

可以把上面的写法变得更简单一点

1
2
// 定义三个变量,分别初始化相应的值,Go会根据相应值的其类型自动初始化它们
var vname1, vname2, vname3 = v1, v2, v3

Go还支持一种简短声明,用:=符号取代var,效果和上面的代码一样。只是简短声明有一个限制,只能在函数内部使用;在函数外部无法编译通过,所以一般用var来定义全局变量

1
2
// 和上面代码的效果是一样的
vname1, vname2, vname3 := v1, v2, v3

_(下划线)在Go中是特殊的变量名,任何赋予它的值都会被丢弃。比如将35赋予b,同时丢弃34

1
_, b := 34, 35

Go是一种强类型语言,任何声明过但未使用的变量在编译时都会报错,比如下面这个例子,抛出错误:声明了变量i但未使用

1
2
3
4
5
package main

func main() {
var i int
}

常量

所谓常量,就是程序编译时就确定下来的值,程序运行时无法改变。常量使用const关键字,可以定义为数值、布尔值或字符串等类型。

语法如下:

1
2
// 定义名为"constname",值为"value"的常量
const constname = value

下面是一些例子:

1
2
3
4
const Pi = 3.1415926
// 如果需要的话,也可以明确指定常量的类型:
// const Pi float32 = 3.1415926
const prefix = "_prefix"

Go中的常量和一般语言不同的是,可以指定相当多的小数位数(比如200位),若指定了常量类型为float32则自动缩短为32bit,若指定了常量类型为float64则自动缩短为64bit。

内置类型

布尔类型

在Go中,布尔值的类型为bool,值为truefalse,默认为false。看一下示例

1
2
var isvalid bool //声明,变量isvalid默认值为false
isvalid = true //赋值,此时isvalid变量值为true
数值类型

在Go中,整数类型分为无符号uint以及有符号int两种。这两种类型长度相同,但具体长度取决于不同编译器的实现。同样,Go支持定义好位数的类型:runeint8int16int32int64以及byteuint8uint16uint32uint64。其中runeint32的别称,byteuint8的别称。

需要注意的是,不同整数类型之间是不允许互相赋值或操作的,不然在编译时会引起报错。比如下面的代码,就会引起编译报错,报错内容:mismatched types int8 and int32

1
2
3
var a int8
var b int32
c := a + b

虽然int类型长度为32位,但是和int32类型也不能互用,这点要注意。下面的代码,同样会引起编译器报错

1
2
3
var a int
var b int32
c := a + b

浮点数类型分为float32以及float64两种,默认为float64

Go还支持复数类型,默认为complex128,即64位实数+64虚数。由于不常用,这里就不一一展开了。

字符串类型

在Go中,字符串都是采用UTF-8字符集编码,用一对""双引号或``反单引号扩起来定义,类型为string。看一下示例

1
2
3
4
// 定义了一个值为空的字符串
var empty string = ""
// 自动匹配到字符串类型
var h = "hello"

需要注意的是,Go中的字符串是不可变的。比如下面的代码编辑时报错:cannot assign to h[0]

1
2
var h string = "hello"
h[0] = "w"

那如果真要修改应该怎么做呢?

1
2
3
4
5
6
7
h := "hello"
// 将字符串变量h转换成[]byte类型
w := []byte(h)
w[0] = 'w'
// 再转换回string类型
w2 := string(w)
fmt.Printf("%s\n", w2)

和大部分语言相同的是,Go使用+操作符来连接字符串

1
2
3
4
h := "hello"
w := "world"
hw := h + w
fmt.Printf("%s\n", hw)

虽然字符串不支持修改,但是可以进行切片操作

1
2
3
4
h := "hello"
// 切片操作这种写法有点类似于Python
h = "w" + h[1:]
fmt.Printf("%s\n", h)

Go通过反单引号`可以声明跨行字符串。反单引号`扩起来的字符串为Raw字符串,即不转义,原样输出。

1
2
hw := ` hello
world `
错误类型

在Go中,内置一个error类型,用来处理错误信息。基础的package中还有一个专门的errors包用来处理错误

1
2
3
4
err := errors.New("emit error")
if err != nil {
fmt.Print(err)
}
数据底层内存分配

可以用一张图来概括基础类型的数据底层都是如何分配内存的

一些技巧

分组声明

在Go中,同时导入多个包、常量或变量,可以采用分组的方式声明

下面的代码

1
2
3
4
5
6
7
8
9
10
import "fmt"
import "os"

const i = 100
const pi = 3.1415
const prefix = "Go_"

var i int
var pi float32
var prefix string

可以用如下形式分组声明

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

const(
i = 100
pi = 3.1415
prefix = "Go_"
)

var(
i int
pi float32
prefix string
)
iota枚举

在Go中,iota关键字用来声明枚举,默认值为0,在常量中每增加一行值加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
"fmt"
)

const (
x = iota // x == 0
y = iota // y == 1
z = iota // z == 2
w // 常量声明省略值时,默认和之前一个值的字面相同。这里隐式地说w = iota,因此w == 3。其实上面y和z可同样不用"= iota"
)

const v = iota // 每遇到一个const关键字,iota就会重置,此时v == 0

const (
h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
)

const (
a = iota //a=0
b = "B"
c = iota //c=2
d, e, f = iota, iota, iota //d=3,e=3,f=3
g = iota //g = 4
)

func main() {
fmt.Println(a, b, c, d, e, f, g, h, i, j, x, y, z, w, v)
}

数组、动态数组以及字典

数组array

在Go中,数组用array表示,定义方式如下

1
2
// 定义一个名为"arr",类型为"type",长度为"n"的数组
var arr [n]type

和大部分语言相同的是,在Go中操作数组通过[]方括号来进行读取或赋值,需要注意的是,并不存在关键字array

1
2
3
4
5
6
7
8
// 声明了一个长度为10,类型为int的数组arr
var arr [10]int
// 数组下标从0开始
arr[0] = 2
// 赋值操作
arr[9] = 11
// 越界访问,编译时报错
arr[10] = 0

数组还支持简单声明

1
2
3
4
5
6
// 声明了长度为3,类型为int的数组a
a := [3]int{1, 2, 3}
// 声明了长度为10,类型为int,前三个元素初始值为1、2、3,其它默认为0的数组b
b := [10]int{1, 2, 3}
// ...会根据元素个数自动计算数组长度
c := [...]int{4, 5, 6}

Go同样支持嵌套数组,即多维数组,例如

1
2
3
4
// 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素
doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}}
// 上面的声明可以简化,直接忽略内部的类型
easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

数组分配如下图所示

Go语言中,数组作为参数传递时,属于值传递,也就是说传的是数组的副本,不是指针。如果要使用引用传递的话,就需要介绍下面的动态数组slice类型了。

动态数组slice

在很多场景下,我们在定义数组的时候,并不知道初始长度,此时就需要用上动态数组slice。类似于Java中的List。

slice并不是真正意义上的动态数组,其本质是一个引用类型。slice总是指向一个底层数组,因此它的声明可以和数组一样,只是不需要长度。注意,程序中不存在slice关键字。

1
2
3
4
// 声明一个名为"fslice"的数组,只是少了长度,所以就是一个动态数组了
var fslice []int
// 声明并初始化一个名为"vslice"的动态数组,类型为[]byte
vslice = []byte {'a', 'b', 'c'}

slice动态数组和array数组在声明时的区别在于,声明数组时,[]方括号内写明了数组的长度或者使用...来自动计算数组长度,而声明动态数组时,[]方括号内没有任何字符。

下面是一些例子

1
2
3
4
5
6
7
8
9
10
11
12
13
// 声明一个含有10个元素元素类型为byte的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

// 声明两个含有byte的slice
var a, b []byte

// a指向数组的第3个元素开始,并到第五个元素结束,
a = ar[2:5]
//现在a含有的元素: ar[2]、ar[3]和ar[4]

// b是数组ar的另一个slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]

它们的数据结构如下图所示

slice在声明的时候有一些简便操作

  • slice的默认开始位置是0,ar[:n]等价于ar[0:n]
  • slice的第二个序列默认是数组的长度,ar[n:]等价于ar[n:len(ar)]
  • 如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于ar[0:len(ar)]

slice动态数组属于引用类型,如果要改变数组中的值时,推荐使用slice。下面是slice作为引用类型的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte

// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素

// 从slice中获取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g

从概念上来说,slice像一个结构体,它包含了三个元素

  • 一个指针,指向数组中slice指定的开始位置
  • 长度,即slice的长度
  • 最大长度,也就是slice开始位置到数组的最后位置的长度

举个例子

1
2
Array_a := [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
Slice_a := Array_a[2:5]

这段代码的真正存储结构如下图所示

对于slice有几个有用的内置函数:

  • len 获取slice的长度
  • cap 获取slice的最大容量
  • appendslice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
  • copy 函数copy从源slicesrc中复制元素到目标dst,并且返回复制的元素的个数

特别注意:append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响。

从Go1.2开始slice支持了三个参数的slice,之前我们一直采用这种方式在slice或者array基础上来获取一个slice

1
2
var array [10]int
slice := array[2:4]

这个例子里面slice的容量是8,新版本里面可以指定这个容量

1
slice = array[2:4:7]

上面这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。

如果slice是这样的形式array[:i:j],即第一个参数为空,默认值就是0。

字典map

map类似于Python中字典的概念,它的格式为map[keyType]valueType

我们看下面的代码,map的读取和赋值也和slice类似,通过key来操作,只是sliceindex只能是int类型,而map多了很多类型,可以是int,可以是string及所有完全定义了==!=操作的类型。

1
2
3
4
5
6
7
// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
var numbers map[string]int
// 另一种map的声明方式
numbers = make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3

使用map过程中需要注意的几点:

  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  • map的长度是不固定的,也就是和slice一样,也是一种引用类型
  • 内置的len函数同样适用于map,返回map拥有的key的数量
  • map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字典值改为11
  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制(这点在后面章节还会提到,先做个了解)

map的初始化可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式,通过delete删除map的元素

1
2
3
4
5
6
7
8
9
10
11
// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
fmt.Println("We have no rating associated with C# in the map")
}
// 删除key为C的元素
delete(rating, "C")

上面说过了,map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应的改变

1
2
3
4
m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut" // 现在m["hello"]的值已经是Salut了
make和new操作

写程序中少不了内存的分配。Go使用make和new关键字进行内存分配。需要记住的是,make用于内建类型(mapslicechannel)的内存分配。new用于各种类型的内存分配

new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。

new返回指针

make(T, args)new(T)有着不同的功能,make只能创建slicemapchannel(这一章没有介绍channel,但是不影响),并且返回一个有初始值(非零)的T类型,而不是*T本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于slicemapchannel来说,make初始化了内部的数据结构,填充适当的值。

make返回初始化后的(非零)值。

下图是make和new对应底层的内存分配示例

零值

关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”。

1
2
3
4
5
6
7
8
9
10
11
int     0
int8 0
int32 0
int64 0
uint 0x0
rune 0 //rune的实际类型是 int32
byte 0x0 // byte的实际类型是 uint8
float32 0 //长度为 4 byte
float64 0 //长度为 8 byte
bool false
string ""

总结

好了,到此为止,我们了解了Go语言中的基础部分,变量、常量、内置基本类型、数组、动态数组、字典以及内存分配操作等。老实说,如果没有看过这一章,直接上手Go的话,数组以及内存分配这一块,可能会影响你对代码的阅读。所以,这些部分是必须要消化的。

avatar

chilihotpot

You Are The JavaScript In My HTML