基础数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。

整形

Go语言同时提供了有符号和无符号类型的整数运算。这里有int8、int16、int32和int64四种截然不同大小的有符号整数类型,分别对应8、16、32、64bit大小的有符号整数,与此对应的是uint8、uint16、uint32和uint64四种无符号整数类型。

除此之外,还提供了对应特地CPU平台机器字大小的类型int和uint,这些类型的大小依赖于具体的实现,和C语言一致。

Unicode字符rune类型(卢恩字符,古代北欧文字,Lancer使用的魔术!<–月批的丑态)是和int32等价的类型,通常用于表示一个Unicode码点。这两个名称可以互换使用。同样byte也是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是一个小的整数。

uintptr是无符号整数类型,没有指定具体的大小但是足以容纳指针,只有底层编程时会用到,一般是Go语言和C语言函数库或操作系统接口交互的地方。

这些类型都是不同类型的兄弟类型,相互转换需要显式的类型转换操作。

它们的底层都是用不同大小的bit位去储存数据,也就是二进制的形式。因此有符号数需要最高位作为符号位,无符号数则不需要符号位,故比相同bit位的有符号数表示范围多一位。

这样的储存性质决定了他们的溢出现象。在Go中,不管结果是有符号还是没符号,只要需要更多bit位来表示结果,那个超出部分的高位bit部分将被丢弃,也就是向左截断。特别的对于有符号数,如果最高位是1的话上溢会产生负数。

浮点数

Go的浮点数和C有部分类似,比如在控制宽度和精度方面都可以用类似于%8.3f之类的声明。除此之外,还定义了有IEEE754规定的正负无穷大+Inf -Inf和非数NaN,一般表示无效的除法操作会出现。

var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"

可以用math.IsNaN来判断一个数是否是非数,也可以用math.NaN来返回一个NaN。使用NaN时务必要小心,因为NaN和任何数都不相等,包括它自己。

复数

Go语言提供了两种精度的复数类型:complex64和complex128,分别对应float32和float64两种浮点数精度。内置的complex函数用于构建复数,内建的real和imag函数分别返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(-5+10i)"
fmt.Println(real(x*y)) // "-5"
fmt.Println(imag(x*y)) // "10"

如果一个浮点数或者十进制整数后跟着一个i,则被视为是一个复数的虚部,实部为0。

布尔值

布尔值只有两种取值,即truefalse,具有短路行为,即运算符左边的式子可以确定整个布尔表达式的值,就不会对右边的式子求值

布尔值不会显示转换为数字值0或1,需要一个显式转换。

字符串

文本字符串通常被解释为采用UTF8编码的Unicode码点(rune)序列。

内置的len函数可以返回一个字符串中的字节数目(不是rune字符数目),索引操作s[i]返回第i个字节的字节值,i必须满足0 ≤ i< len(s)条件约束。

s := "hello, world"
fmt.Println(len(s)) // "12"
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

fmt.Println(len("你好")) // 6
//函数返回的是字符串中的字节长度,而不是字符数量。因为 "你好" 中的每个字符在 UTF-8 编码下都占用 3 个字节,所以 len("你好") 的结果是 6
fmt.Println(utf8.RuneCountInString("你好")) //2 获取字符串中的字符数量

第i个字节并不一定是字符串的第i个字符,因为对于非ASCII字符的UTF8编码会要两个或多个字节。

子字符串操作s[i:j]基于原始的s字符串的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串。生成的新字符串将包含j-i个字节。

这一点和Python的切片操作很像,但是也有所不同。Python是以字符下标来切片,而Go是基于字节来生成新字符串。但可以通过将字符串转换为rune切片(rune在Go中是一个字符类型,可以处理任何Unicode字符)来按字符进行操作。这是一个例子:

s := "你好,世界"
r := []rune(s)

// 取第一个字符到第三个字符(不包括第三个字符)
sub := string(r[0:2]) // 输出:"你好"

在这个例子中,[]rune(s)将字符串s转换为一个rune切片,然后我们可以按照字符的索引进行切片操作。注意,这里的索引是按照字符,而不是字节,所以即使"你"和"好"每个都占用3个字节,它们仍然被视为一个字符。

rune 是一个整数类型。它是 int32 的别名,用于表示 Unicode 码点。当尝试打印一个 rune 类型的值时,你实际上是在打印一个整数,这个整数表示的是字符的 Unicode 码点。所以需要把rune类型再转回string类型。这点有点类似C中的char。

可以通过+来拼接字符串,也可以通过=>等来比较字符串。比较字符串是通过比较逐个字节完成的,因此比较的结果是字符串自然编码的顺序。

字符串的值是不可变的,也就是说字符串的内部属性不可更改,这使得复制字符串时变得快捷且低廉,不需要像C语言那样单独开辟一份新的空间,两份字符串可以共享一个内存地址。同理切片也可以直接引用字符串的地址,而这一切都是安全的。(这里划重点,后面要考)

值得注意的是,Go在处理字符串时表现的像Java(Java的字符串也不可改变),但处理切片时和Python虽然很像又有不同。在Python中,字符串和切片都被视为序列,当做列表处理,所以可以更改字符串的值。但是Go中字符串和序列是两种不同地点数据结构,在 Go 语言中,切片是一个包含三个字段的数据结构:

  1. 一个指向底层数组的指针,这个指针指向的是切片第一个元素在底层数组中的位置。
  2. 切片的长度(len),即切片中元素的数量。
  3. 切片的容量(cap),即从切片的开始位置到底层数组的结束位置的元素数量。

切片本身有自己的内存地址,这个地址是存储切片数据结构的地址。而切片存储的数据实际上是底层数组的一个引用,通过切片的指针字段可以访问到底层数组。

这种设计使得切片非常灵活,可以轻松地对底层数组进行子集选择,而无需复制整个数组。同时,由于切片存储的是底层数组的引用,所以通过切片对底层数组进行的修改会直接反映在底层数组上。

还记得我们说过这么一句话吗?“切片也可以直接引用字符串的地址,而这一切都是安全的”。这句话其实有一点问题,因为字符串本身不可更改,而切片却可以通过对底层数组的引用修改底层数组的值,那么如果对字符串s,修改s[7:]的切片是否可以更改字符串呢?答案是否,因为s[7:]看上去像是一个切片,实际上这只是对原字符串的子字符串划分,它的类型还是字符串而不是切片。

func main() {
s := "adam ben"
s1 := []rune(s)
fmt.Println("type:", reflect.TypeOf(s))
fmt.Println("type:", reflect.TypeOf(s[2:]))
fmt.Println("type:", reflect.TypeOf(s1))
fmt.Println("type:", s1)
}
/*
type: string
type: string
type: []int32
type: [97 100 97 109 32 98 101 110]
*/

如果对象是数组,那么的确可以通过直接对数组切片的引用来修改数组的值;但如果对象是字符串,那么这个操作看上去是在切片实际上则还是字符串。

因为Go语言源文件总是用UTF8编码,且Go语言的文本字符串也以UTF8编码的方式处理。

一个原生的字符串面值形式是`…`,使用反引号代替双引号。在原生的字符串面值中,没有转义操作;全部的内容都是字面的意思,包含退格和换行,因此一个程序中的原生字符串面值可能跨越多行。这点和PHP中的单引号字符串类似。

原生字符串面值用于编写正则表达式会很方便,因为正则表达式往往会包含很多反斜杠。原生字符串面值同时被广泛应用于HTML模板、JSON面值、命令行提示信息以及那些需要扩展到多行的场景。

常量

常量的值在编译期进行计算而不是运行期,一般来说常量的基础数据类型有:boolean、string、数字。

常量的值无法修改。

const pai:=3.14159 //const在C中是老朋友了

如果是批量声明的常量,除了第一个外其它的常量右边的初始化表达式都可以省略,如果省略初始化表达式则表示使用前面常量的初始化表达式写法,对应的常量类型也一样的。例如:

const (
a = 1
b
c = 2
d )
fmt.Println(a, b, c, d) // "1 1 2 2"

itoa常量生成器

常量声明可以使用iota常量生成器初始化,它用于生成一组以相似规则初始化的常量,但是不用每行都写一遍初始化表达式。在一个const声明语句中,在第一个声明的常量所在的行,iota将会被置为0,然后在每一个有常量声明的行加一。

ype Weekday int
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

无类型常量

虽然一个常量可以有任意一个确定的基础类型,例如int或 float64,或者是类似time.Duration这样命名的基础类型。但是许多常量并没有一个明确的基础类型。编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算。

这里有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。

通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
//可以直接使用常量而不需要显式转换

那么本章内容到此为止,Go中所有基本的数据结构都差不多介绍了一遍。下一章则介绍如何通过这些基础的数据结构去组合成数组或结构体等复杂数据类型,然后构建用于解决实际编程问题的数据结构。


a7a21eb76137fbe20b311c8a2c3ce9bc