Go语言从入门到实战(二)

Go语言从入门到实战(二)

一、变量与常量

1.1 变量

我们先来看一下Go语言当中的标准变量声明方法:

1
var name string = "tom"

其中,var是声明变量的关键字,而name就是我们的变量名。需要特别注意的是,作为类型的string被放在了变量名的后面。这样设计主要是为了引导程序员使用类型自动推导,从而省略变量类型:

1
2
// 使用类型推导
var name = "tom"

拓展:这种类型后置的设计在现代编程语言当中已经非常常见了,如ScalaKotlinSwiftRust

而当我们需要一次性声明多个变量的时候,则可以使用代码块来做:

1
2
3
4
5
var (
name string
age int
address string
)

当我们的变量是局部变量的时候,我们还可以使用简短声明的方法来声明变量:

1
2
3
func main() {
name := "tom"
}

在这里,我们有几个注意点需要强调:

  1. 简短声明仅可使用于局部,全局变量必须使用var关键字声明
  2. 局部变量一经声明必须被使用,否则将会编译错误;如果需要丢弃该变量,则使用下划线_命名变量
  3. 全局变量是级别的变量,而不是以单个文件作为作用域;全局变量可以声明但不使用
  4. 局部变量如果是不同类型(包括int32与int64也不是相同类型),无论如何都不能直接运算,而需要显式类型转换

1.2 常量

Go语言的常量使用const关键字进行声明:

1
const name string = "tom"

同样,变量类型是可以省略的:

1
const name = "tom"

当我们需要一次性声明多个常量的时候,同样可以使用代码块来做:

1
2
3
4
const (
name = "tom"
age = 11
)

关于常量,我们也有几点需要强调的:

  1. 常量声明必须使用关键字的方式,不能使用简短声明
  2. 常量可以声明后不使用
  3. 常量如果是以省略变量类型的方式声明的,整型浮点型变量之间可以直接运算

1.3 基于常量的枚举类型

Go并不提供专门的枚举类型关键字,如其他语言当中常见的enum。而是通过常量的关键字const + iota的方式,实现枚举

我们先来看一段示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
const (
a = 9 // iota值为0,虽然没有被使用,iota依然存在
b = iota // iota值为1,所以b为1
c // iota值为2,c继续使用b的表达式,所以c为2
d = 10 // iota值为3,虽然没有被使用,iota依然存在
e = iota // iota值为4,所以e为4
f = "abc" // iota值为5,f为"abc"
g // iota值为6,g继续使用f的表达式,所以g为"abc"
h = iota // iota值为7,所以h为7
)
const i = iota // 新的const,iota重制为0,所以i为0

fmt.Printf("a = %v, b = %v, c = %v, ", a, b, c)
fmt.Printf("d = %v, e = %v, f = %v, ", d, e, f)
fmt.Printf("g = %v, h = %v, i = %v", g, h, i)
}

输出

1
a = 9, b = 1, c = 2, d = 10, e = 4, f = abc, g = abc, h = 7, i = 0

通过上述例子,可以总结为:

  1. iota不管是否被使用,在同一个const代码块内,都会从0开始不断自增;如果遇到新的const,则重制为0
  2. 如果枚举项没有声明表达式,则继承上一个枚举项的表达式

二、Go语言的数据类型

2.1 布尔与字符串类型

类型 描述
bool 布尔类型(true、flase)
string 字符串

2.2 无符号整数类型

类型 描述
uint 长度由系统架构类型决定
uint8 长度为8位,即1字节(0~255)
uint16 长度为16位,即2字节(0 到 65535)
uint32 长度为32位,即4字节(0 到 4294967295)
uint64 长度为64位,即8字节(0 到 18446744073709551615)
uintptr 长度为8位,即1字节(0~255)

2.3 有符号整数类型

类型 描述
int 长度由系统架构类型决定
int8 长度为8位,即1字节(-128 到 127)
int16 长度为16位,即2字节(-32768 到 32767)
int32 长度为32位,即4字节(-2147483648 到 2147483647)
int64 长度为64位,即8字节(-9223372036854775808 到 9223372036854775807)

注意:虽然int类型和uint类型长度没有指定,但是不代表它们可以像Python这种语言一样不限制int的大小。例如在64位操作系统上,intuint的最大值就是int64uint64的最大值

我们也可以在代码中轻松获得每个数值类型的最大最小值,例如:

1
2
3
4
func main() {
fmt.Println(math.MaxInt8)
fmt.Println(math.MinInt8)
}

那如果我们需要操作的数字的大小,int64已经无法满足了,要怎么办呢?

我们可以使用math/big包下的函数进行处理:

1
2
3
4
func main() {
a, _ := new(big.Int).SetString("999999999999999999999999999999999999", 10)
fmt.Println(a)
}

关于这一点,此处就不再多说了,毕竟这不是目前的重点。如果需要更多内容,可以直接查找相关API文档

2.4 浮点数类型

类型 描述
float32 长度为4个字节,IEEE-754 32位浮点型数
float64 长度为8个字节,IEEE-754 64位浮点型数

不同于整型当中的int类型,浮点数类型要么是float32要么就是float64,不存在float这个类型

在使用简短声明的时候,编译器会根据当前计算机的架构自动进行推断,从而决定使用float32还是float64

2.4.1 浮点数与字符串类型的转换

浮点数转换为字符串:

1
2
3
4
5
6
7
func main() {
var f64 float64 = 1.12
str := strconv.FormatFloat(f64, 'E', -1, 64)
str2 := fmt.Sprintf("%v", f64)
fmt.Println(str) // 1.12E+00
fmt.Println(str2) // 1.12
}

字符串转换为浮点数:

1
2
3
4
5
func main() {
s := "3.1415926"
float, _ := strconv.ParseFloat(s, 64)
fmt.Println(float)
}

2.5 复数类型

类型 描述
complex64 32 位复数(实部与虚部分别32位)
complex128 64 位实数和虚数(实部与虚部分别64位)

2.6 别名类型

2.6.1 byte和rune

类型 描述
byte 字节类型,uint8类型的别名
rune 字符类型,int32类型的别名

因为Go语言的rune和其他语言的char还是有较大差异,所以我们在这里针对rune类型,多做一些介绍

我们先看一下Go语言源码中,对于rune类型的注释:

1
2
3
4
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
// 符文是int32的别名,在所有方面都等同于int32。按照惯例,用于区分字符值和整数值。
type rune = int32

因此,rune其实并不是一个独立的类型,它只是int32类型的另一个称呼罢了。所以,rune类型可以和int32类型直接做计算,而不需要任何转换。

如果我们直接将rune类型的值打印出来,那么它就是一个数字:

1
2
3
4
5
func main() {
c := 'a'
fmt.Println(c) // 97
fmt.Printf("%c\n", c) // a
}

对于rune类型的类型转换,下面给出两个示例:

例1:

rune类型转换为字符串:

1
2
3
4
5
6
7
8
9
func main() {
c := 'a'
// 方法1
fmt.Println(string(c))

// 方法2
str := fmt.Sprintf("%c", c)
fmt.Println(str)
}

例2:

rune类型所代表的数字,转换为字符串:

这个问题其实可以等价为,将整型转换为字符串。在其他语言当中,如Python,我们是直接使用str()函数进行转换的,但是因为Go语言的rune类型的存在,如果直接使用string()函数,就会得到该数字对应的字符,而不是数字本身的字符串格式。这显然不是我们想要的。

正确的做法应为:

1
2
3
4
5
6
7
8
func main() {
i := 97
// 方法1
fmt.Println(strconv.Itoa(i)) // 97

// 方法2 可以同时进行进制转换
fmt.Println(strconv.FormatInt(int64(i), 10)) // 97
}

2.6.2 类型定义与类型别名

这两个概念我们需要重点区分:类型定义类型别名

什么是类型定义?举个例子:

1
2
3
4
5
6
type myInt int

func main() {
var i myInt = 100
fmt.Println(i)
}

在上面的例子当中,myInt这个类型就是我们新定义的。那为什么说是新定义的呢?因为,它和int类型其实并不是一个类型。他只是与int类型有共同的实现、共同的能力罢了。

我们知道Go是一个强类型语言,且不存在类型隐式转换。只要不是同一个类型,就不能直接运算,如下面的代码是不能编译成功的:

1
2
3
4
5
6
7
type myInt int

func main() {
var i myInt = 100
var j = 100 // 默认为int类型
fmt.Println(i + j) // invalid operation: i + j (mismatched types myInt and int)
}

但是如果我们直接与字面量运算,还是没有问题的:

1
2
3
4
5
6
7
type myInt int

func main() {
var i myInt = 100
var j = i + 100
fmt.Println(j) // 200 并且此时的j也是myInt类型
}

我们可以通过使用类型定义这个特性来拓展原有类型的能力:

1
2
3
4
5
6
7
8
9
10
11
12
type myInt int

// 我们给新的整数类型myInt赋予了自动翻倍的能力
func (t myInt) double() int {
return int(t * 2)
}

func main() {
i := 100
newNum := myInt(i).double()
fmt.Println(newNum) // 200
}

类型别名像我们上述提到typerune时介绍的一样,并不是一个新的类型,只是叫法不同。这个特性是在Go1.9版本时引入的。

不同于类型定义,我们无法对别名类型的能力进行拓展,因为本来就是同一种类型

三、条件与循环语句

3.1 if条件语句

示例:

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

if a > 10 {
fmt.Println("大于10")
} else if a > 5 {
fmt.Println("a大于5")
} else {
fmt.Println("a小于5")
}
}

注意:Go语言的if语句,其条件不需要放在括号中

3.2 switch语句

3.2.1 基础switch

示例:

1
2
3
4
5
6
7
8
9
10
11
func main() {
var a = 5
switch a {
case 0, 2, 4, 6, 8:
fmt.Println("10以内的偶数")
case 1, 3, 5, 7, 9:
fmt.Println("10以内的奇数")
default:
fmt.Println("不是10以内的自然数")
}
}

输出:

1
10以内的奇数

这里应该能看出Go和其他主流编程语言的诸多不同,我们这里一一列举:

  1. Go语言的switch语句可以用来匹配任何类型,包括结构体
  2. 每个case都可以匹配多个值
  3. 每个case都默认自带break,如果需要实现switch穿透,则在需要穿透的case结尾加上fallthrough

除此之外,switch还可以作为if-else if结构的简写版本:

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

switch {
case a > 10:
fmt.Println("大于10")
case a > 5:
fmt.Println("a大于5")
default:
fmt.Println("a小于5")
}
}

3.2.2 type switch

type switch主要用于对变量类型进行判断的场景,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
var a interface{} = "123"

switch a.(type) {
case int:
fmt.Println("int")
case string:
fmt.Println("string")
default:
fmt.Println("other")
}
}

这里需要说明的是,type switch只能对interface{}类型使用,interface{}类型是一个接口Go当中所有类型都实现了该接口,所以type switch其实就是判断一个interface{}的实现到底是什么类型

提到了type switch,就不能不提type assertiontype assertion是一个类型断言,他能帮助我们先校验类型,后执行操作。

如下面这个s就可以顺利作为参数传递给println()

1
2
3
4
func main() {
var s interface{} = "123"
fmt.Println(s.(string))
}

3.3 循环语句

Go只有for循,但是存在多种不同的使用方式。比如其他语言当中的while循环,在Go语言中也是通过for来实现

3.3.1 经典for循环

示例:

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

其中注意点和if是一样的,即不需要使用括号来包围条件

3.3.2 for循环变体

Go中既然没有while循环,那么怎么实现while循环的效果呢?其实只要把for当作while就可以了:

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

for还可以轻松实现无限循环,相比其他语言的while(true)更加简洁:

1
2
3
4
5
func main() {
for {
fmt.Println("forever")
}
}

3.3.3 for…range

当我们需要遍历一个集合或者字符串的时候,使用经典for循环显然是太啰嗦了,我们可以使用for...range语句来简化语法,但是要注意,在某些场景下,for...range不仅仅是帮助我们简化了语法,还多做了一些人性化的事情,后面会详细讲到的。

经典for循环遍历数组:

1
2