1、Go 语言的数据类型与表达式

1.1、Go 语言的基本数据类型

Go 语言提供了多种基本数据类型,主要包括布尔型、数值型和字符型。

布尔类型(Boolean):bool 是布尔类型,用于表示真或假,取值为 truefalse。布尔类型主要用于条件判断和逻辑运算。

数值类型(Numeric Types):数值类型包括整型、浮点型和复数类型

  • 整型分为有符号和无符号整型:
    • 有符号整型:int8int16int32int64int
    • 无符号整型:uint8uint16uint32uint64uint
  • 浮点型用于表示小数:float32float64
  • 复数类型用于表示复数:complex64complex128

字符类型(Character):字符类型用于表示单个字符

  • byteuint8 的别名,通常用于表示 ASCII 字符;
  • runeint32 的别名,用于表示 Unicode 码点。

这些基本数据类型为 Go 语言提供了强大的数据处理能力,使得编写高效、类型安全的代码变得更加容易。

1.2、Go 语言的表达式和运算符

表达式是一种特定的类型的值,它可以由其它的值以及运算符组合而成。每个类型都定义了可以和自己结合的运算符集合,如果你使用了不在这个集合中的运算符,则会在编译时获得编译错误。

一元运算符只可以用于一个值的操作(作为后缀),而二元运算符则可以和两个值或者操作数结合(作为中缀)。

只有两个类型相同的值才可以和二元运算符结合,另外要注意的是,Go 是强类型语言,因此不会进行隐式转换,任何不同类型之间的转换都必须显式说明。Go 不存在像 C 那样的运算符重载,表达式的解析顺序是从左至右。


2、布尔类型

布尔型的值只能是常量 truefalse

两个相同类型的值可以使用相等 == 或不等 != 运算符进行比较,并返回一个布尔型的结果。

2.1、相等运算符

当相等运算符两边的值完全相同时,返回 true,否则返回 false。只有在两个值的类型相同时才可以使用相等运算符。

示例:

var aVar = 10
aVar == 5   // false
aVar == 10  // true
2.2、不等运算符

当不等运算符两边的值不同时,返回 true,否则返回 false

示例:

var aVar = 10
aVar != 5   // true
aVar != 10  // false

Go 对于值之间的比较有严格的限制。只有两个相同类型的值才可以进行比较。如果值的类型是接口(interface),它们必须都实现相同的接口。如果其中一个值是常量,那么另一个值的类型必须与该常量类型兼容。如果以上条件都不满足,必须显式转换类型后才能进行比较。

布尔型常量和变量可以通过逻辑运算符(非 !、与 &&、或 ||)结合来产生新的布尔值。逻辑语句本身并不是完整的 Go 语句。

逻辑值通常用于条件结构中的条件语句(例如 ifforswitch 结构)以测试某个条件是否满足。与 &&、或 ||、相等 == 和不等 != 运算符属于二元运算符,而非 ! 运算符属于一元运算符。在接下来的内容中,我们将使用 T 表示条件满足的语句,用 F 表示条件不满足的语句。

2.3、非运算符:!
!T -> false
!F -> true

非运算符用于取得布尔值的相反结果。

2.4、与运算符:&&
T && T -> true
T && F -> false
F && T -> false
F && F -> false

只有当两边的值都为 true 时,结果才是 true

2.5、或运算符:||
T || T -> true
T || F -> true
F || T -> true
F || F -> false

只有当两边的值都为 false 时,结果才是 false,只要有一边的值为 true,表达式的结果就是 true

在 Go 语言中,&&|| 是具有短路特性的运算符。当运算符左边的表达式值已经能够决定整个表达式的结果时(&& 左边的值为 false|| 左边的值为 true),运算符右边的表达式将不会被执行。利用这个特性,如果有多个条件判断,应将计算过程较为复杂的表达式放在运算符的右侧,以减少不必要的计算。

利用括号也可以提升某个表达式的运算优先级。

在格式化输出时,可以使用 %t 表示布尔型值。

布尔值(以及任何结果为布尔值的表达式)最常用于条件结构的条件语句中,例如:ifforswitch 结构。

良好的布尔值命名能够提升代码的可读性,例如以 isIs 开头的 isSortedisFinishedisVisible。这样的命名能够使代码更加清晰易读,例如标准库中的 unicode.IsDigit(ch)


3、数字类型

3.1、整型和浮点型

Go 语言支持整型和浮点型数字,并且原生支持复数,采用二的补码进行位运算。

Go 也有基于架构的类型,例如:intuintuintptr

这些类型的长度由运行程序所在的操作系统类型决定:

  • intuint 在 32 位操作系统上使用 32 位(4 个字节),在 64 位操作系统上使用 64 位(8 个字节)。
  • uintptr 的长度足以存放一个指针。

Go 语言中没有 float 类型,只有 float32float64,没有 double 类型。

与操作系统架构无关的类型都有固定大小,并在类型名称中标明:

整数类型:

  • int8(-128 到 127)
  • int16(-32768 到 32767)
  • int32(-2,147,483,648 到 2,147,483,647)
  • int64(-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807)

无符号整数类型:

  • uint8(0 到 255)
  • uint16(0 到 65,535)
  • uint32(0 到 4,294,967,295)
  • uint64(0 到 18,446,744,073,709,551,615)

浮点类型(符合 IEEE-754 标准):

  • float32(± 1e-45 到 ± 3.4 * 1e38)
  • float64(± 5 * 1e-324 到 107 * 1e308)

在整型中,int 类型的计算速度最快。

整型的零值为 0,浮点型的零值为 0.0。

float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。由于精确度的缘故,使用 ==!= 来比较浮点数时应当非常小心。建议在正式使用前测试精确度要求较高的运算。

尽量使用 float64,因为 math 包中所有的数学运算函数都要求使用这个类型。

可以通过增加前缀 0 来表示八进制数(如:077),增加前缀 0x 来表示十六进制数(如:0xFF),以及使用 e 表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。

可以使用 a := uint64(0) 来同时完成类型转换和赋值操作,这样 a 的类型就是 uint64

Go 中不允许不同类型之间的混合使用,但对于常量的类型限制非常少,允许常量之间的混合使用。以下示例很好地解释了这个现象(该程序无法通过编译):

示例:

package main

func main() {
	var a int
	var b int32
	a = 15
	b = a + a   // 编译错误
	b = b + 5   // 因为 5 是常量,所以可以通过编译
}

如果尝试编译该程序,会得到编译错误 cannot use a + a (type int) as type int32 in assignment

同样地,int16 不能隐式转换为 int32

下面示例展示了通过显式转换避免这个问题:

示例:

package main

import "fmt"

func main() {
	var n int16 = 34
	var m int32
	// 编译错误:cannot use n (type int16) as type int32 in assignment
	//

m = n
	m = int32(n)

	fmt.Printf("32 bit int is: %d\n", m)
	fmt.Printf("16 bit int is: %d\n", n)
}

输出:

32 bit int is: 34
16 bit int is: 34
3.2、格式化说明符

在格式化字符串中,%d 用于格式化整数(%x%X 用于格式化十六进制表示的数字),%g 用于格式化浮点型(%f 输出浮点数,%e 输出科学计数表示法),%0nd 用于规定输出长度为 n 的整数,其中开头的数字 0 是必须的。

%n.mg 用于表示数字 n 并精确到小数点后 m 位,除了使用 g 外,还可以使用 ef,例如:使用格式化字符串 %5.2e 来输出 3.4 的结果为 3.40e+00

3.3、数字值转换

当进行类似 a32bitInt = int32(a32Float) 的转换时,小数点后的数字将被丢弃。这种情况一般发生在从取值范围较大的类型转换为取值范围较小的类型时,或者可以编写专门的函数来确保没有精度损失。下面示例展示如何安全地从 int 型转换为 int8

func Uint8FromInt(n int) (uint8, error) {
	if 0 <= n && n <= math.MaxUint8 { // 转换是安全的
		return uint8(n), nil
	}
	return 0, fmt.Errorf("%d is out of the uint8 range", n)
}

或者安全地从 float64 转换为 int

func IntFromFloat64(x float64) int {
	if math.MinInt32 <= x && x <= math.MaxInt32 { // x 在整数范围内
		whole, fraction := math.Modf(x)
		if fraction >= 0.5 {
			whole++
		}
		return int(whole)
	}
	panic(fmt.Sprintf("%g is out of the int32 range", x))
}

如果实际存储的数字超出要转换到的类型的取值范围,会引发 panic。

3.4、复数

Go 拥有以下复数类型:

  • complex64 (32 位实数和虚数)
  • complex128 (64 位实数和虚数)

复数使用 re+imI 表示,其中 re 代表实数部分,im 代表虚数部分,I 代表 √-1

示例:

var c1 complex64 = 5 + 10i
fmt.Printf("The value is: %v", c1)
// 输出: 5 + 10i

如果 reim 的类型均为 float32,那么类型为 complex64 的复数 c 可以通过以下方式获得:

c = complex(re, im)

函数 real(c)imag(c) 可以分别获得相应的实数和虚数部分。

在使用格式化说明符时,可以使用 %v 表示复数,但如果只表示其中的一个部分,需要使用 %f

复数支持与其他数字类型相同的运算。当使用等号 == 或不等号 != 对复数进行比较时,需注意精确度。cmath 包中包含了一些操作复数的公共方法。如果对内存要求不高,建议使用 complex128 进行计算,因为相关函数都使用这个类型的参数。


4、运算符

4、位运算

位运算只能用于整数类型的变量,并且需要它们具有相同的位长度。

%b 是用于表示位的格式化标识符。

4.1、二元运算符

按位与 &

1 & 1 -> 1
1 & 0 -> 0
0 & 1 -> 0
0 & 0 -> 0

按位或 |

1 | 1 -> 1
1 | 0 -> 1
0 | 1 -> 1
0 | 0 -> 0

按位异或 ^

1 ^ 1 -> 0
1 ^ 0 -> 1
0 ^ 1 -> 1
0 ^ 0 -> 0

位清除 &^:将指定位置上的值设置为 0

示例:

package main
import "fmt"
func main() {
	var x uint8 = 15
	var y uint8 = 4
	fmt.Printf("%08b\n", x &^ y)  // 00001011
}
4.2、一元运算符

按位补足 ^

该运算符与异或运算符一起使用,即 m^x。对于无符号 x 使用“全部位设置为 1” 的规则,对于有符号 x 时使用 m=-1。例如:

^10 = -01 ^ 10 = -11

位左移 <<

用法:bitP << n

bitP 的位向左移动 n 位,右侧空白部分使用 0 填充。如果 n 等于 2,则结果是 2 的相应倍数,即 2n 次方。例如:

1 << 10 // 等于 1 KB
1 << 20 // 等于 1 MB
1 << 30 // 等于 1 GB

位右移 >>

用法:bitP >> n

bitP 的位向右移动 n 位,左侧空白部分使用 0 填充。如果 n 等于 2,则结果是当前值除以 2n 次方。

当希望将结果赋值给第一个操作数时,可以简写为 a <<= 2b ^= a & 0xffffffff

4.3、示例

位左移常见实现存储单位的用例:

使用位左移与 iota 计数配合可优雅地实现存储单位的常量枚举:

type ByteSize float64
const (
	_ = iota // 通过赋值给空白标识符来忽略值
	KB ByteSize = 1<<(10*iota)
	MB
	GB
	TB
	PB
	EB
	ZB
	YB
)

在通讯中使用位左移表示标识的用例:

type BitFlag int
const (
	Active BitFlag = 1 << iota // 1 << 0 == 1
	Send // 1 << 1 == 2
	Receive // 1 << 2 == 4
)

flag := Active | Send // == 3

5、逻辑运算符

Go 中的逻辑运算符包括:==!=<<=>>=

它们的运算结果总是布尔值 bool。例如:

b3 := 10 > 5 // b3 为 true

6、算术运算符

常见可用于整数和浮点数的二元运算符有 +-*/

Go 在进行字符串拼接时允许使用运算符 +,但不允许自定义运算符重载。

对于整数运算,除法 / 的结果依然为整数。例如:9 / 4 -> 2

取余运算符 % 只能作用于整数:9 % 4 -> 1

整数除以 0 可能导致程序崩溃,会引发运行时恐慌。如果除以 0 的行为在编译时能被捕捉到,则会引发编译错误。第 13 章将详细讲解如何正确处理此类情况。

浮点数除以 0.0 会返回一个无穷大的结果,用 +Inf 表示。

练习:尝试编译 divby0.go

你可以将语句 b = b + a 简写为 b += a,同样的写法也适用于 -=*=/=%=

对于整数和浮点数,可以使用一元运算符 ++(递增)和 --(递减),但只能用于后缀:

i++ -> i += 1 -> i = i + 1
i-- -> i -= 1 -> i = i - 1


带有 ++-- 的只能作为语句,而非表达式。因此 n = i++ 这种写法无效,其它像 f(i++)a[i]=b[i++] 的写法在 Go 中也不允许。

运算时溢出不会产生错误,Go 会简单地将超出位数的部分抛弃。如果需要范围无限大的整数或有理数(仅受限于计算机内存),可以使用标准库中的 big 包,该包提供了 big.Intbig.Rat 类型。


7、随机数

一些像游戏或统计学类的应用需要用到随机数。rand 包实现了伪随机数的生成。

下面的示例演示了如何生成 10 个非负随机数:

package main
import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	for i := 0; i < 10; i++ {
		a := rand.Int()
		fmt.Printf("%d / ", a)
	}
	for i := 0; i < 5; i++ {
		r := rand.Intn(8)
		fmt.Printf("%d / ", r)
	}
	fmt.Println()
	timens := int64(time.Now().Nanosecond())
	rand.Seed(timens)
	for i := 0; i < 10; i++ {
		fmt.Printf("%2.2f / ", 100*rand.Float32())
	}
}

可能的输出:

816681689 / 1325201247 / 623951027 / 478285186 / 1654146165 /
1951252986 / 2029250107 / 762911244 / 1372544545 / 591415086 / / 3 / 0 / 6 / 4 / 2 /22.10
/ 65.77 / 65.89 / 16.85 / 75.56 / 46.90 / 55.24 / 55.95 / 25.58 / 70.61 /

函数 rand.Float32rand.Float64 返回介于 0 和 1 之间的伪随机数,包括 0.0 但不包括 1.0。函数 rand.Intn 返回介于 0 和 n 之间的伪随机数。

可以使用 rand.Seed(value) 函数来提供伪随机数的生成种子,一般情况下使用当前时间的纳秒级数字。


8、运算符与优先级

有些运算符具有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符及其优先级,由上至下代表优先级由高到低:

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


9、类型别名

在使用某个类型时,可以为它起另一个名字,然后在代码中使用新的名字(用于简化名称或解决名称冲突)。

type TZ int 中,TZint 类型的新名称(用于表示程序中的时区)。然后可以使用 TZ 来操作 int 类型的数据。

示例:

package main
import "fmt"

type TZ int

func main() {
	var a, b TZ = 3, 4
	c := a + b
	fmt.Printf("c has the value: %d", c) // 输出:c has the value: 7
}

实际上,类型别名得到的新类型与原类型并非完全相同,新类型不会继承原类型的方法。TZ 可以自定义方法以输出更加人性化的时区信息。

练习:定义一个 string 的类型别名 Rope,并声明一个该类型的变量。

10、字符类型

严格来说,字符并不是 Go 语言的一个独立类型,它们只是整数的特殊用例。byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符,这种类型完全适用。例如:

var ch byte = 'A'  // 字符使用单引号括起来

在 ASCII 码表中,'A' 的值是 65,使用十六进制表示则为 41。因此,以下写法是等效的:

var ch byte = 65
var ch byte = '\x41'

\x 总是紧跟着长度为 2 的十六进制数)

另一种可能的写法是 \ 后面紧跟着长度为 3 的八进制数,例如:\377

Go 同样支持 Unicode(UTF-8),因此字符也称为 Unicode 代码点或者 runes,在内存中使用 int 表示。在文档中,通常使用格式 U+hhhh 表示,其中 h 表示一个十六进制数。其实 rune 也是 Go 中的一个类型,是 int32 的别名。

书写 Unicode 字符时,需要在十六进制数之前加上前缀 \u\U

因为 Unicode 至少占用 2 个字节,所以我们使用 int16int 类型表示。如果需要使用 4 字节,则会加上 \U 前缀;前缀 \u 总是紧跟着长度为 4 的十六进制数,前缀 \U 紧跟着长度为 8 的十六进制数。

示例:

var ch int = '\u0041'
var ch2 int = '\u03B2'
var ch3 int = '\U00101234'
fmt.Printf("%d - %d - %d\n", ch, ch2, ch3) // integer
fmt.Printf("%c - %c - %c\n", ch, ch2, ch3) // character
fmt.Printf("%X - %X - %X\n", ch, ch2, ch3) // UTF-8 bytes
fmt.Printf("%U - %U - %U", ch, ch2, ch3)   // UTF-8 code point

输出:

65 - 946 - 1053236
A - β - 𐄴
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

格式化说明符 %c 用于表示字符;当与字符配合使用时,%v%d 会输出用于表示该字符的整数;%U 输出格式为 U+hhhh 的字符串。

unicode 包含了一些用于测试字符的有用函数(其中 ch 表示字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

这些函数返回布尔值。utf8 包含更多与 rune 类型相关的函数。

06-02 09:20