类型
变量或表达式的类型定义了对应存储值的属性特征(例如数值在内存的存储大小), 它们在内部是如何表达的, 是否支持一些操作符, 以及它们自己关联的方法等.
一个类型声明语句(type
)创建了一个新的类型名称, 和现有类型具有相同的底层结构. 新命名的类型提供了一个方法, 用来分隔不同概念的类型, 这样即使它们底层类型相同也是不兼容的.
type 类型名字 底层类型
对于中文汉字, Unicode 标志都作为小写字母处理, 因此它是不能导出的.
Go 语言将数据类型分为四类: 基础类型, 复合类型, 引用类型和接口类型.
类型转换
对于每一个类型 T, 都有一个对应的类型转换操作 T(x)
, 用于将 x 转为 T 类型(指针转换需要多一个括号: (*T)(x)
). 类型转换不会改变值本身, 但是会使它们的语义发生变化.
在任何情况下, 运行时不会发生转换失败的错误(错误只会发生在编译阶段).
自定义类型可以与其底层类型直接进行比较.
var i = 10
var f = float64(i)
var u = uint(f)
类型断言
// 如果是指定类型则返回 ture
_, ok = var.(Type)
运算符
在 Go 语言中, %
取模运算符的符号和被取模数的符号总是一致的, 因此 -5 % 3
和 -5 % -3
的结果都是 -2.
除法运算符 /
的行为依赖于操作数. 比如 5.0 / 4.0
的结果是 1.25, 而 5 / 4
的结果是 1. 整数除法会向着 0 方向截断余数.
如果计算结果溢出, 超出的高位 bit 位部分会被丢弃.
位操作运算符 ^
作为二元运算符时是按位异或(XOR), 当用作一元运算符时表示按位取反.
位操作运算符 &^
用于按位置零(AND NOT). 对于表达式 z = x &^ y
, 如果 y 的 bit 位为 1 的话, 则 z 的 bit 位为 0; 否则对应的 bit 位等于 x 相应的 bit 位的值.
var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2
fmt.Printf("%08b\n", x) // 00100010
fmt.Printf("%08b\n", y) // 00000110
fmt.Printf("%08b\n", x&y) // 位运算 AND, 00000010
fmt.Printf("%08b\n", x|y) // 位运算 OR, 00100110
fmt.Printf("%08b\n", x^y) // 位运算 XOR, 00100100
fmt.Printf("%08b\n", x&^y) // 位运算 AND NOT, 00100000
fmt.Printf("%08b\n", x<<1) // 位运算左移, 01000100
fmt.Printf("%08b\n", x>>1) // 位运算右移, 00010001
位运算操作数最好使用无符号数, 这样你可以将整数完全当作一个 bit 位模式处理(不需要考虑符号位).
无符号数往往只有在位运算或其它特殊的运算场景才会使用, 像 bit 集合, 分析二进制文件格式或者是哈希和加密操作等. 它们通常并不用于仅仅是表达非负数量的场合.
八进制数以 0
开始, 十六进制以 0x
开始.
字符使用 ''
定义:
ascii := 'a'
unicode := '国'
newline := '\n'
fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'"
fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 国 '国'"
fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"
常量
常量表达式的值在编译期计算, 而不是运行时. 每种常量的潜在类型都是基础类型: boolean, string 或数值.
常量使用 const
定义, 其值不可修改.
一个未指定类型的常量由上下文来决定其类型.
常量间的所有算术运算, 逻辑运算和比较运算的结果也是常量.
批量声明的常量可使用前一个常量的类型和值:
const (
a = 1
b
c = 2
d
)
fmt.Println(a, b, c, d) // "1 1 2 2"
iota 常量生成器
常量声明可以使用 iota 常量生成器初始化, 它用于生成一组以相似规则初始化的常量, 但是不用每行都写一遍初始化表达式. 在一个 const
声明语句中, 在第一个声明的常量所在的行, iota 将会被置为 0, 然后在每一个有常量声明的行加一.
iota 可被用来实现枚举类型.
// 定义星期
type Weekday int
const (
Sunday Weekday = iota // 0
Monday // 1
Tuesday // 2
Wednesday
Thursday
Friday
Saturday // 6
)
// 定义字节单位, 每个常量都是 1024 的幂
const (
_ = 1 << (10 * iota) // _ 表示忽略
KiB // 1024
MiB // 1048576
GiB // 1073741824
TiB // 1099511627776 (exceeds 1 << 32)
PiB // 1125899906842624
EiB // 1152921504606846976
ZiB // 1180591620717411303424 (exceeds 1 << 64)
YiB // 1208925819614629174706176
)
无类型常量
数值类型的常量是可以不声明具体类型的.
编译器为那些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算. 你可以认为至少有 256bit 的运算精度.
只有常量是可以无类型的. 当一个无类型的常量被赋值给一个变量时, 无类型的常量会被隐式转换为对应的类型.
无类型的整数常量默认转换为 int
, 浮点数转换为 float64
, 复数转换为 complex128
.
整型
有符号: int
, int8
, int16
, int32
, int64
.
无符号: uint
, uint8
, uint16
, uint32
, uint64
.
Unicode 字符 rune 类型是和 int32 等价的类型, 通常用于表示一个 Unicode 码点.
byte 是 uint8 的等价类型, byte 类型一般用于强调数值是一个原始的数据而不是一个小的整数.
uintptr 是一个没有指定具体的 bit 大小但是足以容纳指针的类型.
int
, uint
和 uintptr
在 32 位系统上通常为 32 位宽, 在 64 位系统上则为 64 位宽. 当你需要一个整数值时应使用 int
类型, 除非你有特殊的理由使用固定大小或无符号的整数类型.
浮点数
float32
类型的浮点数可以提供大约 6 个十进制数的精度, 而 float64
则可以提供约 15 个十进制数的精度. 通常应该优先使用 float64
类型.
小数点前面或后面的数字都可以省略(如 .1
, 1.
). 很小或很大的数最好用科学计数法书写, 通过 e 或 E 来指定指数部分.
const a = 6.032e23
const b = 6.666e-34
math 包定义了一些特殊值: 正无穷大(+Inf
), 负无穷大(-Inf
)和非数(NaN
).
var z float64
fmt.Println(z, -z, 1/z, -1/z, z/z) // "0 -0 +Inf -Inf NaN"
测试一个结果是否为 NaN
是充满风险的, 因为 NaN
和任何数都是不相等的(包括它自己).
复数
Go 语言提供了两种精度的复数类型: complex64
和 complex128
, 分别对应 float32
和 float64
两种浮点数精度. 内置的 complex
函数用于构建复数, 内建的 real
和 imag
函数分别返回复数的实部和虚部.
布尔型
一个布尔类型的值只有两种: true
和 false
.
布尔值并不会隐式转换为数字值 0 和 1.
指针
从传统意义上说, 指针是一个指向某个确切的内存地址的值. 这个内存地址可以是任何数据或代码的起始地址. 比如某个变量, 字段或函数.
类型 *T
是指向 T 类型值的指针. 其零值为 nil
.
与 C 不同, Go 没有指针运算.
var p *int
// & 操作符会生成一个指向其操作数的指针
i := 42
p = &i
// * 操作符表示指针指向的底层值
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
一个指针的值是另一个变量的地址. 一个指针对应变量在内存中的存储位置. 并不是每一个值都会有一个内存地址, 但是对于每一个变量必然有对应的内存地址. 通过指针, 我们可以直接读或更新对应变量的值, 而不需要知道该变量的名字.
如果用 var x int
声明一个变量, 那么 &x
表达式(取 x 变量的内存地址)将产生一个指向该整数变量的指针, 指针对应的数据类型是 *int
, 指针被称之为 “指向 int 类型的指针”. 如果指针名字为 p, 那么可以说 “p 指针指向变量 x”, 或者说 “p 指针保存了 x 变量的内存地址”. 同时 *p
表达式对应 p 指针指向的变量的值. 一般 *p
表达式读取指针指向的变量的值, 同时因为 *p
对应一个变量, 所以该表达式也可以出现在赋值语句的左边, 表示更新指针所指向的变量的值:
var x int = 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // 1
*p = 2 // equivalent to x = 2
fmt.Println(x) // 2
变量又被称为可寻址的值. 即使变量由表达式临时生成, 那么表达式也必须能接受 &
取地址操作.
任何类型的指针的零值都是 nil. 如果 p != nil
为真, 那么 p 是指向某个有效变量的指针.
因为指针包含了一个变量的地址, 因此如果将指针作为参数调用函数, 那将可以在函数中通过该指针来更新变量的值.
指针特别有价值的地方在于我们可以不用名字而访问一个变量, 但这是一把双刃剑.
uintptr
类型是一个数值类型, 根据计算机的架构不同, 它可以存储 32 位或 64 位的无符号整数, 可以代表任何指针的位模式, 也就是原始的内存地址.
unsafe.Pointer
类型可以表示任意可寻址的指针. 同时它也是指针值和 uintptr
值之间的桥梁.
Go 语言中不可寻址的值:
不可变的值. 常量, 基本类型的值字面量, 字符串变量的值, 函数以及方法的字面量都是如此.
临时结果. 没有明确赋予变量的表达式都属于临时结果.
字典中的 “键/值” 都是不可寻址的. 因为字典中的 “键/值” 的存储位置是有可能变化的.不安全的. 函数和方法都是不可寻址的. 一个原因是函数就是代码, 是不可变的. 另一个原因是, 拿到指向一段代码的指针是不安全的.
不可变的, 临时结果和不安全的值都是不可寻址值. 不可寻址值的指针方法是无法被调用的.
func add(x, y int) int {
return x + y
}
// 错误, 函数返回的结果属性临时结果, 不可寻址, 不能进行 ++ 操作
i := add(1, 2)++
// 需要明确将函数返回结果赋给一个变量
i := add(1, 2)
i++