Go语言基础之数据类型

Go 语言中有丰富的数据类型,除了基本的整型、浮点型、布尔型、字符串外,还有数组、切片、结构体、函数、map、通道(channel)等。Go 语言的基本类型和其他语言大同小异。

1. 整型

介绍

  1. Go语言同时提供了有符号和无符号的整数类型,内存对应 数字(bit)/8 大小的字节 :

    • 有符号:int8、 int16、 int32 、 int64
    • 无符号:uint8、uint16、uint32 、uint64
  2. 还有两种整数类型,它们分别对应特定 CPU 平台的字长(机器字大小),实际开发中由于编译器和计算机硬件的不同,他们所能表示的大小会在 32bit 或 64bit 之间变化:

    • 有符号:int
    • 无符号:uint
  3. uintptr 是无符号的整数类型,它没有指定具体的 bit 大小但是足以容纳指针。该类型只有在底层编程时才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

  4. rune 和 int32 是等价的,rune 通常用于表示一个 Unicode 码点。这两个名称可以互换使用。

  5. byte 和 uint8 是等价的,byte 通常用于强调数值是一个原始的数据而不是一个小的整数。

整型分为以下两个大类: 按长度分为:int8、int16、int32、int64 对应的无符号整型:uint8、uint16、uint32、uint64

其中,uint8就是我们熟知的byte型,int16对应 C 语言中的short型,int64对应 C 语言中的long型。

int32对应Java的intint64对应Java的long

解释这个符号的区别

int8uint8 举例,8 代表 8个bit,能表示的数值个数有 2^8 = 256。

uint8 是无符号,能表示的都是正数,0-255,刚好256个数。

int8 是有符号,既可以正数,也可以负数,那怎么办?对半分呗,-128-127,也刚好 256个数。

int8 int16 int32 int64 这几个类型的最后都有一个数值,这表明了它们能表示的数值个数是固定的。

而 int 并没有指定它的位数,说明它的大小,是可以变化的,那根据什么变化呢?

  • 当你在32位的系统下,int 和 uint 都占用 4个字节,也就是32位。
  • 若你在64位的系统下,int 和 uint 都占用 8个字节,也就是64位。

出于这个原因,在某些场景下,你应当避免使用 int 和 uint ,而使用更加精确的 int32 和 int64,比如在二进制传输、读写文件的结构描述(为了保持文件的结构不会受到不同编译目标平台字节长度的影响)

特殊整型

注意: 在使用intuint类型时,不能假定它是 32 位或 64 位的整型,而是考虑intuint可能在不同平台上的差异。

注意事项 获取对象的长度的内建len()函数返回的长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用int来表示。在涉及到二进制传输、读写文件的结构描述时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,不要使用intuint

数字字面量语法

Number literals syntax
Go1.13 版本之后引入了数字字面量语法,这样便于开发者以二进制、八进制或十六进制浮点数的格式定义数字,例如:

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45。 v := 0o377,代表八进制的 377,相当于十进制的 255。 v := 0x1p-2,代表十六进制的 1 除以 2²,也就是 0.25。

而且还允许我们用 _ 来分隔数字,比如说: v := 123_456 表示 v 的值等于 123456。

我们可以借助 fmt 函数来将一个整数以不同进制形式展示。

package main

import "fmt"

func main(){
// 定义十进制
var a int = 101
fmt.Printf("%d \n", a) // 101
fmt.Printf("%b \n", a) // 1100101 占位符%b表示二进制
fmt.Printf("%o \n", a) // 145 占位符%o表示八进制(一般用于文件权限)
fmt.Printf("%x \n", a) // 65 占位符%x表示十六进制(一般用于内存地址、指针)

// 定义八进制 以0开头
b := 077
fmt.Printf("%d \n", b) // 63

// 定义十六进制 以0x开头
var c int = 0xff
fmt.Printf("%x \n", c) // ff
fmt.Printf("%X \n", c) // FF
fmt.Printf("%d \n", c) // 255

// 声明int8类型变量
d := int8(9) // 明确指定int8类型,否则就是默认的int类型
fmt.Printf("%T \n", d) // int8
}

2. 浮点型

介绍

Go 语言支持两种浮点型数:float32float64。这两种浮点型数据格式遵循IEEE 754标准: float32 的浮点数的最大范围约为 3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

打印浮点数时,可以使用fmt包配合动词%f,代码如下:

package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) // 6位小数
fmt.Printf("%.2f\n", math.Pi) // 2位小数
f1 := 1.23456 // 默认定义的小数是float64类型
fmt.Printf("%T\n", f1) // float64
f2 := float32(1.23456) // 显示声明float32类型
fmt.Printf("%T\n", f2) // float32
}

总结

  1. Go语言提供了两种精度的浮点数 float32 和 float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。

  2. 浮点数类型的取值范围可以从很微小到很巨大,极限值可以在 math 包中找到:

    • float32:最小值:1.4e-45 最大值:常量 math.MaxFloat32,大约是 3.4e38,内存占用4字节。
    • float64:最小值:4.9e-324 最大值:常量 math.MaxFloat64,大约是 1.8e308,内存占用8字节。
  3. 小数位精度不同:

    • float32:6 个十进制数的精度
    • float64:15 个十进制数的精度

注意

  1. 通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!
  1. 浮点数在声明的时候可以只写整数部分或者小数部分,像下面这样:
const e = .71828 // 0.71828
const f = 1. // 1
  1. 很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:
const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数
  1. 用 Printf 函数打印浮点数时可以使用“%f”来控制保留几位小数:
package main
import (
"fmt"
"math"
)
func main() {
fmt.Printf("%f\n", math.Pi) // 3.141593
fmt.Printf("%.2f\n", math.Pi) // 3.14
}

复数(实部和虚部)

complex64:实部和虚部都是 float32 类型的的复数。
complex128:实部和虚部都是 float64 类型的的复数。

内建函数 complex 用于创建一个包含实部和虚部的复数。complex 函数的定义如下:

var c1 complex64
c1 = 1 + 2i
var c2 complex128
c2 = 2 + 3i
fmt.Println(c1)
fmt.Println(c2)

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

3. 布尔值

介绍

  1. 布尔类型的值只有两种:true 或 false。不可以用数字代替。

  2. if 和 for 语句的条件部分都是布尔类型的值,并且==<等比较操作也会产生布尔型的值。

  3. 一元操作符!对应逻辑非操作,因此!true的值为 false。

  4. 在内存中占用1个字节。

注意

  1. Go语言对于值之间的比较有非常严格的限制,如果以下条件都不满足,则必须将其中一个值的类型转换为和另外一个值的类型相同之后才可以进行比较:

    • 只有两个相同类型的值才可以进行比较。
    • 如果值的类型是接口(interface),那么它们也必须都实现了相同的接口。
    • 如果其中一个值是常量,那么另外一个值可以不是常量,但是类型必须和该常量类型相同。
  2. 布尔值可以和 &&(AND)和 ||(OR)操作符结合(&& 优先级比 || 高),并且有短路行为,如果运算符左边的值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值。

s != "" && s[0] == 'x'
  1. 布尔型无法参与数值运算,也无法与其他类型进行转换。
var n bool
fmt.Println(int(n) * 2)

会编译错误:cannot convert n (type bool) to type int

4. 字符串

介绍

  1. 字符串可以包含任意数据,通常是用来包含可读的文本,使用””或``来定义。

  2. 字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,更深入地讲,字符串是字节的定长数组。

  3. 字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)

UTF-8

  1. UTF-8 是一种被广泛使用的编码格式,是文本文件的标准编码,包括 XML 和 JSON 在内也都使用该编码。

  2. 由于该编码对占用字节长度的不定性:

    • Go语言中字符串根据需要占用 1 至 4 个字节。
    • 其它编程语言如 C++Java 或者 Python 不同(Java 始终使用 2 个字节)。
  3. Go语言这样做不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。

单行字符串

(字符串字面量 string literal)

使用双引号书写字符串的方式是字符串常见表达方式之一,双引号字面量不能跨行。

s := "hello word!"

多行字符串

  1. 使用反引号书写字符串的方式也是字符串常见表达方式之一,反引号字面量可以跨行。

  2. 反引号间换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。

  3. 多行字符串一般用于内嵌源码和内嵌数据等,立马所有内容均不会被编译器识别,而只是作为字符串的一部分。

const str = `第一行
第二行
第三行
\r\n
`
fmt.Println(str)

字符串转义符

Go 语言的字符串常见转义符包含回车、换行、单双引号、制表符等,如下表所示。

举个例子,我们要打印一个 Windows 平台下的一个文件路径:

package main
import (
"fmt"
)
func main() {
fmt.Println("str := \"c:\\Code\\lesson1\\go.exe\"")
}

字符串的常用操作

字符串类型在业务中的应用可以说是最广泛的,读者需要详细了解字符串的常见用法,请猛击下面的文章:

5. 字符类型 byte 和 rune

介绍

组成每个字符串的元素叫做 “字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号(’)包裹起来,如:

var a := '中'
var b := 'x'
  1. 字符串中的每一个元素叫做“字符”,在遍历或者单个获取字符串元素时可以叫做字符。字符使用单引号括起来。

  2. Go语言的字符有以下两种:

    • byte 类型(或者叫 uint8 类型),代表了 ASCII 码的一个字符。
    • rune 类型(等价于 int32 类型),代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。

byte

ASCII 码表中,下面的写法是等效的:

var ch byte = 65
var ch byte = '\377' //(\ 后面紧跟着长度为 3 的 8 进制数)
var ch byte = '\x41' //(\x 后面紧跟着长度为 2 的 16 进制数)

rune

  1. Go语言同样支持 Unicode(UTF-8),因此字符同样称为 Unicode 代码点或者 runes,并在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。

  2. 在书写 Unicode 字符时,需要在 16 进制数之前加上前缀\u或者\U。因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果需要使用到 4 字节,则使用\u前缀,如果需要使用到 8 个字节,则使用\U前缀。

  3. Unicode 包中内置了一些用于测试字符的函数,这些函数的返回值都是一个布尔值:

    • 判断是否为字母:unicode.IsLetter(c)
    • 判断是否为数字:unicode.IsDigit(c)
    • 判断是否为空白符号:unicode.IsSpace(c)
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

// log
65 - 946 - 1053236
A - β - r
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

UTF-8 和 Unicode 有何区别?

广义的 Unicode 指的是一个标准,定义字符集及编码规则,即 Unicode 字符集和 UTF-8、UTF-16 编码等。

Unicode

  1. Unicode 与 ASCII 类似,都是一种字符集。
  2. 字符集为每个字符分配一个唯一的 ID,我们使用到的所有字符在 Unicode 字符集中都有一个唯一的 ID,例如上面例子中的 a 在 Unicode 与 ASCII 中的编码都是 97。汉字“你”在 Unicode 中的编码为 20320,在不同国家的字符集中,字符所对应的 ID 也会不同。而无论任何情况下,Unicode 中的字符的 ID 都是不会变化的。

UTF-8

  1. UTF-8 是编码规则,将 Unicode 中字符的 ID 以某种方式进行编码,UTF-8 的是一种变长编码规则,从 1 到 4 个字节不等。编码规则如下:

    • 0xxxxxx 表示文字符号 0~127,兼容 ASCII 字符集。
    • 从 128 到 0x10ffff 表示其他字符。
  2. 根据这个规则,拉丁文语系的字符编码一般情况下每个字符占用一个字节,而中文每个字符占用 3 个字节。

6. 类型转换

介绍

  1. 在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明。

  2. 类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将 int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 float32 转换为 int),会发生精度丢失(截断)的情况。

  3. 只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)。

  4. 浮点数在转换为整型时,会将小数部分去掉,只保留整数部分。

使用

Go 有着非常严格的强类型特征。Go 没有自动类型提升或类型转换。我们通过一个例子说明这意味着什么。

package main

import (
"fmt"
)

func main() {
i := 55 //int
j := 67.8 //float64
sum := i + j //不允许 int + float64
fmt.Println(sum)
}

上面的代码在 C 语言中是完全合法的,然而在 Go 中,却是行不通的。i 的类型是 int ,而 j 的类型是 float64 ,我们正试图把两个不同类型的数相加,Go 不允许这样的操作。如果运行程序,你会得到 main.go:10: invalid operation: i + j (mismatched types int and float64)。

要修复这个错误,i 和 j 应该是相同的类型。在这里,我们把 j 转换为 int 类型。把 v 转换为 T 类型的语法是 T(v)。

package main

import (
"fmt"
)

func main() {
i := 55 //int
j := 67.8 //float64
sum := i + int(j) //j is converted to int
fmt.Println(sum)
}

现在,当你运行上面的程序时,会看见输出 122

赋值的情况也是如此。把一个变量赋值给另一个不同类型的变量,需要显式的类型转换。下面程序说明了这一点。

package main

import (
"fmt"
)

func main() {
i := 10
var j float64 = float64(i) // 若没有显式转换,该语句会报错
fmt.Println("j", j)
}

在第 9 行,i 转换为 float64 类型,接下来赋值给 j。如果不进行类型转换,当你试图把 i 赋值给 j 时,编译器会抛出错误。

7. 零值zero Value

零值并非是空值,而是一种“变量未填充前”的默认值。

var i int
var a float32
var b float64
var isMarried bool
var name string
fmt.Printf("i=%v,a=%v,b=%v,isMarried=%v,name=%v",i,a,b,isMarried,name)

参考感谢
1.4 数据类型:byte、rune与字符串 — Go编程时光 1.0.0 documentation (iswbm.com)
数据类型 · 语雀 (yuque.com)
数据类型 · 语雀 (yuque.com)