Go 学习笔记(4)— Go 标识符、数据类型之间转换、布尔型、整型、浮点型、interface 类型

725 阅读9分钟

1. 标识符整体分类

Go 标识符整体分类如下图所示:

在这里插入图片描述

2. 数据类型分类

Go 语言按类别有以下几种数据类型:

类型描述
布尔型布尔型的值只可以是常量 true 或者 false
数字类型整型 int 和浮点型 float32、float64,Go语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码
字符串类型字符串就是一串固定长度的字符连接起来的字符序列。 Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本
派生类型(a) 指针类型(Pointer)
(b) 数组类型
(c) 结构化类型(struct)
(d) Channel 类型
(e) 函数类型
(f) 切片类型
(g) 接口类型(interface)
(h) Map 类型

注意:

  1. 布尔类型数据和整型数据不能直接进行转换;
var a bool = true
a = 10 // cannot use 10 (type int) as type bool in assignment
a := 100
if a {
    // Error: non-bool a (type int) used as if condition
    println("true")
}
  1. Go 语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明,不同类型的不能直接转化,必须强制类型转换;

转换方式:

valueOfTypeB = typeB(valueOfTypeA)
a := 5.0
b := int(a)
package main

func main() {
	var a int = 10
	var b int32 = 10
	b = a	// cannot use a (type int) as type int32 in assignment
	if a == b { // invalid operation: a == b (mismatched types int and int32)
		println("a is equal b ")
	}
}
  1. 对于整数类型值、整数常量之间的类型转换,原则上只要源值在目标类型的可表示范围内就是合法的。

比如,之所以 uint8(255) 可以把无类型的常量 255 转换为 uint8 类型的值,是因为 255 在 [0, 255] 的范围内。但需要特别注意的是,源整数类型的可表示范围较大,而目标类型的可表示范围较小的情况,比如把值的类型从 int16 转换为 int8 。请看下面这段代码:

	var srcInt = int16(-255)
	dstInt := int8(srcInt)
	fmt.Println(dstInt)	// 1

一定要记住,当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。

类似的快刀斩乱麻规则还有:当把一个浮点数类型的值转换为整数类型值时,前者的小数部分会被全部截掉

  1. 虽然直接把一个整数值转换为一个 string 类型的值是可行的,但值得关注的是,被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果将会是"�"(仅由高亮的问号组成的字符串值)。

字符'�'的 Unicode 代码点是U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。

string(-1)	// "�"

总结:

package main

import (
	"fmt"
)

func main() {
	// 重点1的示例。
	var srcInt = int16(-255)
	// 请注意,之所以要执行uint16(srcInt),是因为只有这样才能得到全二进制的表示。
	// 例如,fmt.Printf("%b", srcInt)将打印出"-11111111",后者是负数符号再加上srcInt的绝对值的补码。
	// 而fmt.Printf("%b", uint16(srcInt))才会打印出srcInt原值的补码"1111111100000001"。
	fmt.Printf("The complement of srcInt: %b (%b)\n", uint16(srcInt), srcInt)
	// The complement of srcInt: 1111111100000001 (-11111111)

	dstInt := int8(srcInt)
	fmt.Printf("The complement of dstInt: %b (%b)\n", uint8(dstInt), dstInt)
	// The complement of dstInt: 1 (1)
	fmt.Printf("The value of dstInt: %d\n", dstInt)
	//	The value of dstInt: 1
	fmt.Println()

	// 重点2的示例。
	fmt.Printf("The Replacement Character: %s\n", string(-1))
	// The Replacement Character: �
	fmt.Printf("The Unicode codepoint of Replacement Character: %U\n", '�')
	// The Unicode codepoint of Replacement Character: U+FFFD
	fmt.Println()
}

2.1 数字类型详细划分

类型描述
uint8无符号 8 位整型 (0 到 255)
uint16无符号 16 位整型 (0 到 65535)
uint32无符号 32 位整型 (0 到 4294967295)
uint64无符号 64 位整型 (0 到 18446744073709551615)
int8有符号 8 位整型 (-128 到 127)
int16有符号 16 位整型 (-32768 到 32767)
int32有符号 32 位整型 (-2147483648 到 2147483647)
int64有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

尽管在某些特定的运行环境下 intuintuintptr 的大小可能相等,但是它们依然是不同的类型,比如 intint32 ,虽然 int 类型的大小也可能是 32 bit,但是在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换,反之亦然。

Golang 中,整数类型长度与操作系统有关,32 位系统中,整数类型长度是 4 个字节,在 64 位系统中,整数类型长度是 8 个字节。

Go 语言中有符号整数采用 2 的补码形式表示,也就是最高 bit 位用来表示符号位,一个 n-bit 的有符号数的取值范围是从 -2 到 2-1。

无符号整数的所有 bit 位都用于表示非负数,取值范围是 0 到 2。例如,int8 类型整数的取值范围是从 -128 到 127,而 uint8 类型整数的取值范围是从 0 到 255。

哪些情况下使用 intuint ?

程序逻辑对整型范围没有特殊需求。例如,对象的长度使用内建 len() 函数返回,这个长度可以根据不同平台的字节长度进行变化。实际使用中,切片或 map 的元素数量等都可以用 int 来表示。

事实上,内置的 len 函数返回一个有符号的 int ,我们可以像下面例子那样处理逆序循环。

medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

如果 len 函数返回一个无符号数,那么 i 也将是无符号的 uint 类型,然后条件i >= 0则永远为真。在三次迭代之后,也就是i == 0时, i-- 语句将不会产生 -1,而是变成一个 uint 类型的最大值(可能是2^64-1),然后 medals[i] 表达式运行时将发生 panic 异常,也就是试图访问一个 slice 范围以外的元素。

反之,在二进制传输、读写文件的结构描述、位运算、哈希和加密操作等时,为了保持文件的结构不会受到不同编译目标平台字节长度的影响,通常使用 uint

Unicode 字符 rune 类型是和 int32 等价的类型,通常用于表示一个 Unicode 码点。这两个名称可以互换使用。同样 byte 也是 uint8 类型的等价类型, byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数。

一个算术运算的结果,不管是有符号或者是无符号的,如果需要更多的 bit 位才能正确表示的话,就说明计算结果是溢出了。超出的高位的 bit 位部分将被丢弃。如果原始的数值是有符号类型,而且最左边的 bit 位是1的话,那么最终结果可能是负的,例如 int8 的例子:

var u uint8 = 255
fmt.Println(u, u+1, u*u) // "255 0 1"
var i int8 = 127
fmt.Println(i, i+1, i*i) // "127 -128 1"

2.2 浮点类型详细划分

类型描述
float32IEEE-754 32位浮点型数
float64IEEE-754 64位浮点型数
complex6432 位实数和虚数
complex12864 位实数和虚数
  • float32 的浮点数的最大范围约为 3.4e38, 可以使用常量定义: math.MaxFloat32

  • float64 的浮点数的最大范围约为 1.8e308,可以使用常量定义: math.MaxFloat64

  • float32 类型的浮点数可以提供大约 6 个十进制数的精度,

  • float64 则可以提供约 15 个十进制数的精度;

通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大(译注:因为 float32 的有效 bit 位只有23个,其它的 bit 位用于指数和符号;当整数大于 23 bit 能表达的范围时, float32 的表示将出现误差):

var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1)    // "true"!

Printf 函数的 %g 参数打印浮点数,将采用更紧凑的表示形式打印,并提供足够的精度,但是对应表格的数据,使用 %e (带指数)或 %f 来控制保留几位小数的形式打印可能更合适。所有的这三个打印形式都可以指定打印的宽度和控制打印精度。

for x := 0; x < 8; x++ {
    fmt.Printf("x = %d e^x = %8.3f\n", x, math.Exp(float64(x)))
}

上面代码打印 e 的幂,打印精度是小数点后三个小数精度和 8 个字符宽度:

x = 0       e^x =    1.000
x = 1       e^x =    2.718
x = 2       e^x =    7.389
x = 3       e^x =   20.086
x = 4       e^x =   54.598
x = 5       e^x =  148.413
x = 6       e^x =  403.429
x = 7       e^x = 1096.633

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

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 ,例如 3.141592i或 2i,它将构成一个复数的虚部,复数的实部是 0:

fmt.Println(1i * 1i) // "(-1+0i)", i^2 = -1

在常量算术规则下,一个复数常量可以加到另一个普通数值常量(整数或浮点数、实部或虚部),我们可以用自然的方式书写复数,就像 1+2i 或与之等价的写法 2i+1 。上面 x 和 y 的声明语句还可以简化:

x := 1 + 2i
y := 3 + 4i

复数也可以用 ==!= 进行相等比较。只有两个复数的实部和虚部都相等的时候它们才是相等的(译注:浮点数的相等比较是危险的,需要特别小心处理精度问题)。

参考:Go 语言圣经

2.3 其它数字类型划分

类型描述
byte等同 uint8
rune等同 int32
uint32 或 64 位
int与 uint 一样大小
uintptr无符号整型,用于存放一个指针

Go 语言内建的基本类型中就存在两个别名类型。byteuint8 的别名类型,而 runeint32 的别名类型。

3. interface 类型

由于 Go 语言中任何对象实例都满足空接口 interface{} ,所以 interface{} 看起来像是可以指向任何对象的 Any 类型,如下:

var v1 interface{} = 1 // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}

当函数可以接受任意的对象实例时,我们会将其声明为 interface{} ,最典型的例子是标准库 fmtPrintXXX 系列的函数,例如:

func Printf(fmt string, args ...interface{})
func Println(args ...interface{})

Go 语言中,interface{} 代表空接口,任何类型都是它的实现类型。任何类型的值都可以很方便地被转换成空接口的值就行了,这里的具体语法是 interface{}(x)

你可能会对这里的 {} 产生疑惑,为什么在关键字 interface 的右边还要加上这个东西?请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

而空接口 interface{} 则代表了不包含任何方法定义的、空的接口类型。当然了,对于一些集合类的数据类型来说,{} 还可以用来表示其值不包含任何元素,比如空的切片值 []string{} ,以及空的字典值 map[int]string{}