叩町

叩町

【Go】基本数据类型

go
6
2025-12-19
【Go】基本数据类型

布尔类型

用于存储逻辑值,只有true​和false两个取值。布尔类型的值作为布尔表达式的求值结果,可以与其他布尔表达式进行比较和操作:

  • &&:逻辑与运算符,两个操作数都为真时结果为真
  • ||:逻辑或运算符,至少有一个操作数为真时结果为真
  • !:逻辑非运算符,用于取反一个布尔值

数值类型

基本数值类型中,使用最多的是数值类型。

整型

Go中的整型分为平台无关整型平台相关整型两种,主要区别在不同CPU架构或操作系统中的长度是否一致。

平台无关整型,在任何CPU架构或操作系统中的长度都是固定的,Go提供的平台无关整型如下:

类型
长度 取值范围
有符号整型
int8 1字节 [-128, 127]
int16 2字节 [-37268, 32767]
int32 4字节 [-2147483648, 2147483647]
int64 8字节 [-9223372036854775808, 9223372036854775809]
无符号整型
uint8 1字节 [0, 255]
uint16 2字节 [0, 65535]
uint32 4字节 [0, 4294967295]
uint64 8字节 [0, 18446744073709551615]

有符号和无符号的本质差别在于二进制位是否被解释为符号位,直接影响到取值范围不同。

无符号整型与有符号整型

与平台相关的整型的长度会根据运行平台的不同而变化,Go原生提供了3中平台相关整型

类型
32位长度 64位长度
默认的有符号整型 int 32位(4字节) 64位(8字节)
默认的无符号整型 uint 32位(4字节) 64位(8字节)
无符号整型 uintptr 大到足以存储任意一个指针的值

需要特别注意,由于3中类型的长度与平台相关,因此在编写有移植性要求的代码时,不应假设这些类型的固定长度。如果不知道这些类型在目标运行平台上的长度,可以使用unsafe​包中的SizeOf函数获取:

package main

import (
	"fmt"
	"unsafe"
)

func main() {
	var a, b = int(5), uint(6)
	var p uintptr = 0x12345678

	fmt.Println("有符号整型a的长度为:", unsafe.Sizeof(a))
	fmt.Println("无符号整型b的长度为", unsafe.Sizeof(b))
	fmt.Println("指针类型p的长度为", unsafe.Sizeof(p))

}

整型溢出问题

无论哪种取值,一旦运算时结果超出取值范围,就会发生整型溢出,最终得到的值最终会落在取值范围内,但结果往往和预期不符:

var s int8 = 127
s += 1 // 预期128,实际结果变为-128
var u uint8 = 1
u -= 2 // 预期-1,实际结果变为255

字面值与格式化输出

Go在设计之初就继承了C语言关于数值字面值(number literal) 的语法形式,早期版本的Go支持以下形式:

a := 53        // 十进制
b := 0700      // 八进制,以“0”为前缀
c1 := 0xaabbcc // 十六进制,以“0x”为前缀
c2 := 0Xddeeff // 十六进制,以“0X”为前缀

自Go1.13起,引入了二进制字面值的支持

d1 := 0b10000001 // 二进制,以“0b”为前缀
d2 := 0B10000001 // 二进制,以“0B”为前缀
e1 := 0o700      // 八进制,以“0o”为前缀
e2 := 0O700      // 八进制,以“0O”为前缀

此外,还支持使用数字分隔符_,可以用来将数字分组或用来分隔前缀与字面值中的第一个数字,例如:

a := 5_3_7   // 十进制:537
b := 0b_1000_0111  // 二进制:10000111
c1 := 0_700  // 八进制:0700
c2 := 0o_700 // 八进制:0700
d1 := 0x_5c_6d // 十六进制:0x5c6d

还可以通过标准库fmt包的格式化输出函数,将整型变量以不同进制的形式输出:

var a int8 = 59
fmt.Printf("%b\n", a) // 输出二进制:111011
fmt.Printf("%d\n", a) // 输出十进制:59
fmt.Printf("%o\n", a) // 输出八进制:73
fmt.Printf("%O\n", a) // 输出八进制(带0o前缀):0o73
fmt.Printf("%x\n", a) // 输出十六进制(小写):3b
fmt.Printf("%X\n", a) // 输出十六进制(大写):3B

浮点类型

浮点类型的使用场景更为聚焦,主要集中在科学数值计算、图形图像处理和仿真、多媒体游戏以及人工智能等领域。

二进制表示

Go中浮点类型的二进制表示遵循IEEE754标准。

IEEE 754规定了4种表示浮点数值的方式:单精度(32位)、双精度(64位)、扩展单精度(43位以上)与扩展双精度(79位以上,通常实现为80位)。后两种较少使用,我们重点关注前两种。Go提供了float32与float64两种浮点类型,它们分别对应IEEE 754中的单精度与双精度浮点数值类型。

双精度浮点类型在阶码与尾数上使用的比特位更多,可以表示更高的精度,在日常开发中更常用,float64也是Go中浮点常量或字面值的默认类型。

字面值格式化与输出

浮点类型的字面值可以分为两类,一类是十进制表示的浮点值形式,另一类是科学计数法形式。

十进制表示:

3.1415
.15  // 如果整数部分为0,可以省略不写
81.80
82. // 如果小数部分为0,小数点后的0可以省略不写

科学计数法(十进制)

6674.28e-2 // 6674.28 * 10^(-2) = 66.742800
.12345E+5  // 0.12345 * 10^5 = 12345.000000

科学计数法(十六进制)

0x2.p10  // 2.0 * 2^10 = 2048.000000
0x1.Fp+0 // 1.9375 * 2^0 = 1.937500

整数部分和小数部分使用十六进制表示,但指数部分依然采用十进制,并且运算的底数为2

与整型类似,使用fmt同样可以对浮点类型进行格式化输出:

var f float64 = 123.45678
fmt.Printf("%f\n", f) // 输出:123.456780

fmt.Printf("%e\n", f) // 输出:1.234568e+02
fmt.Printf("%x\n", f) // 输出:0x1.edd3be22e5de1p+06

其中%e​用于输出十进制科学计数法形式,%x用于输出十六进制科学计数法形式。

复数类型

在数学中,形如z=a+bi(a、b均为实数,a称为实部,b称为虚部)的数被称为复数。

Go提供了两种复数类型—complex64和complex128。complex64的实部与虚部都是float32类型,而complex128的实部与虚部都是float64类型。如果一个复数没有显式赋予类型,则默认类型为complex128。

复数的初始化方式有三种,分别是直接使用复数字面值:

var c = 5 + 6i
var d = 0o123 + .12345E+5i // 83+12345i

使用complex函数创建复数:

var c = complex(5, 6) // 5 + 6i
var d = complex(0o123, .12345E+5) // 83+12345i

使用预定义函数real​和imag获取复数的实部与虚部

var c = complex(5, 6) // 5 + 6i
r := real(c) // 5.000000
i := imag(c) // 6.000000

字符串类型

Go原生支持字符串类型,带来了以下好处:

  1. string类型的数据不可变,提高了字符串的并发安全性。
  2. 没有结尾'\0'​,获取字符串长度的时间复杂度为常数时间,不再是C语言的o(n)
  3. 原生支持“所见即所得”的原始字符串,大大降低构造多行字符串时的负担,可以使用"`"符号包裹一个原生字符串,其中的转义字符都不会进行转义,而是原样输出,唯一不能直接包含的字符就是反引号本身。
  4. 对非ASCII字符提供原生支持,消除了源码在不同环境下显示乱码的可能。

Go字符串的组成

  • 字节视角:字符串是一个可空的字节序列,字节序列中字节数量即字符串的长度。单独的字节只是鼓励数据,无法表达具体的含义。

    var s = "中国人"
    fmt.Printf("the length of s = %d\n", len(s)) // 输出:9
    for i := 0; i < len(s); i++ {
        fmt.Printf("0x%x ", s[i]) // 输出:0xe4 0xb8 0xad 0xe5 0x9b 0xbd 0xe4 0xba 0xba
    }
    fmt.Printf("\n")
    
  • 字符视角:字符串由可控的字符序列构成

    // ch7/charactercount.go
    var s = "中国人"
    fmt.Println("the character count in s is", utf8.RuneCountInString(s)) // 输出:3
    for _, c := range s {
        fmt.Printf("0x%x ", c) // 输出:0x4e2d 0x56fd 0x4eba
    }
    fmt.Printf("\n")
    
    

    由于Go采用的时Unicode字符集,每个字符都是一个Unicode字符,因此输出的值分别是字符串三个字符在Unicode字符集中的码点(code point)

rune类型与字符字面值

Go使用rune​类型标识一个Unicode码点。由于rune​本质上是int32​的别名,因此与int32类型是完全等价的。

一个rune​实例就是一个Unicode字符,一个Go字符串也可以被视为一系列rune实例的集合。字符字面量最常见的写法是通过单引号括起的字符字面值:

'a'  // ASCII字符
'中' // Unicode字符集中的中文字符
'\n' // 换行字符
'\" // 单引号字符

此外,还可以使用Unicode专用的转义字符或\u​、\U来表示一个Unicode字符:

'\u4e2d'     // 字符:中
'\U00004e2d' // 字符:中
'\u0027'     // 单引号字符

\u​后跟的是4个十六进制数,而如果需要表示超过4个十六进制数范围的Unicode字符,则可以使用\U​,其后跟8个十六进制数,以表示一个Unicode字符。此外还可以直接用整型值作为字符字面值给rune变量赋值:

'\x27'  // 使用十六进制表示的单引号字符
'\047'  // 使用八进制表示的单引号字符

字符串字面值

字符串字面值需要使用双引号:

"abc\n"   // 包含换行字符的字符串
"中国人"  // 直接包含中文字符的字符串
"\u4e2d\u56fd\u4eba" // 使用Unicode转义序列表示的“中国人”
"\U00004e2d\U000056fd\U00004eba" // 同样使用Unicode转义序列表示的“中国人”
"中\u56fd\u4eba" // 混合的字符串字面值
"\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba" // 使用十六进制表示的字符串字面值“中国人”

十六进制形式字符串字面值与Unicode码点的值没有对应,原因是这个字节序列实际上是Unicode字符串的UTF-8编码值,UTF-8的介绍可以参考文章:

详细介绍 UTF-8 编码

Go字符串类型的内部表示

通过查看value.go​和string.go源码得到Go字符串的内部表示:

// $GOROOT/src/reflect/value.go
// StringHeader代表字符串的运行时表示形式
type StringHeader struct {
    Data uintptr
    Len  int
}
// $GOROOT/src/runtime/string.go
type stringStruct struct {
  str unsafe.Pointer
  len int
}

字符串类型实际上是一个“描述符”,它并不直接存储字符串数据,而是由一个指向底层存储的指针和表示字符串长度的字段组成。

Go编译器将源码中的字符串类型转换为运行时的二元数组(Data, len),实际的数据保存在Data指向的底层数组中。

字符串的内部表示

因此将字符串类型直接作为函数或方法的参数传递并不会带来显著的开销,因为传递的只是一个描述符。

字符串类型的常见操作

下标操作

字符串支持通过下标的方式进行访问:

var s = "中国人"
fmt.Printf("0x%x\n", s[0]) // 输出:0xe4,即字符“中” UTF-8编码的第一个字节

通过下标操作,获取到的是字符串中特定位置上的字节值,而不是字符本身。

字符迭代

Go提供了两种迭代字符串的方式。

常规for循环,从字节的视角出发,每轮迭代得到一个构成字符串的一个字节及其对应的索引标值,等价于遍历字符串底层数组的过程:

// ch7/stringiterate.go
var s = "中国人"
for i := 0; i < len(s); i++ {
    fmt.Printf("index: %d, value: 0x%x\n", i, s[i])
}

输出:

index: 0, value: 0xe4
index: 1, value: 0xb8
index: 2, value: 0xad
index: 3, value: 0xe5
index: 4, value: 0x9b
index: 5, value: 0xbd
index: 6, value: 0xe4
index: 7, value: 0xba
index: 8, value: 0xba

相比之下,使用for range​循环迭代时,得到的是字符串中每个Unicode字符的码点值,以及其在字符串中的偏移位置。这种方式可以用于计算字符串的字符数量,而通过Go内置的函数len只能获取字符串的长度(字节数量)。

// ch7/stringiterate.go
var s = "中国人"
for i, v := range s {
    fmt.Printf("index: %d, value: 0x%x\n", i, v)
}

输出:

index: 0, value: 0x4e2d
index: 3, value: 0x56fd
index: 6, value: 0x4eba

对于获取字符数量,更推荐的方式是使用标准库中UTF-8包的RuneCountlnString函数。

字符串连接

虽然Go中字符串的内容不可变,但并不妨碍基于已有的字符串创建新的字符串。Go原生支持通过+​和+=操作符进行字符串连接:

s := "Rob Pike, "
s = s + "Robert Griesemer, "
s += " Ken Thompson"
fmt.Println(s) // Rob Pike, Robert Griesemer, Ken Thompson

这种方式虽然可行,但在性能上不是最优的,Go提供了strings.Builder​、strings.Join​、fmt.Springf等函数,适用于不同的场景。

字符串比较

Go字符串类型支持多种比较操作符,包括==​、!=​、>=​、<=​、>​和<。在字符串比较时,Go采用字典序(按字符编码顺序)进行逐字符对比。

// ch7/stringcompare.go
func main() {
    // ==
    s1 := "世界和平"
    s2 := "世界" + "和平"
    fmt.Println(s1 == s2) // 输出:true
    // !=
    s1 = "Go"
    s2 = "C"
    fmt.Println(s1 != s2) // 输出:true
    // < and <=
    s1 = "12345"
    s2 = "23456"
    fmt.Println(s1 < s2)  // 输出:true
    fmt.Println(s1 <= s2) // 输出:true
    // > and >=
    s1 = "12345"
    s2 = "123"
    fmt.Println(s1 > s2)  // 输出:true
    fmt.Println(s1 >= s2) // 输出:true
}

由于Go字符串内容的不可变性,如果两个字符串长度不同,则不需要比较具体字符串数据即可断定它们不同。但如果两个字符串长度相同,则需要进一步判断数据指针是否指向同一块底层存储的数据。如果相同,才能认为两个字符串是等价的,否则,还需要进一步对比实际的内容。

字符串转换

Go支持字符串与字节切片、字符串与rune切片之间的双向转换。这种转换无需调用额外的函数,只需通过显式的类型转换即可实现。

// ch7/stringconvert.go
var s string = "中国人"
// string -> []rune
rs := []rune(s) 
fmt.Printf("%x\n", rs) // 输出:[4e2d 56fd 4eba]
// string -> []byte
bs := []byte(s) 
fmt.Printf("%x\n", bs) // 输出:e4b8ade59bbde4baba
// []rune -> string
s1 := string(rs)
fmt.Println(s1) // 输出:中国人
// []byte -> string
s2 := string(bs)
fmt.Println(s2) // 输出:中国人

  • 0