变量与数据类型
变量与常量
变量(Variable)
变量即一段或多段内存空间,用于存储数据。
Go 语言中的变量声明格式为:
var 变量名 变量类型
例如:
var a int
var b string
var c bool
a = 1
b = "hello"
c = true
var d int = 2
// Go 具有类型推导,可以不指定类型
var d = 2 // int
var e string = "world"
var f bool = false
// 类型推导
var g = 3 // int
// 简短声明
h:= 4 // int
// 避免对已声明的变量进行重复声明
h:= 5 // 报错
输出结果:
fmt.Print("a=%v, b=%v, c=%v\n", a, b, c)
fmt.Print("d=%v, e=%v, f=%v\n", d, e, f)
fmt.Print("g=%v, h=%v\n", g, h)
此处使用了 fmt.Print
函数,%v
为占位符,表示输出变量的值。
当然,可以同时声明多个变量:
var a, b, c int
a, b, c = 1, 2, 3
var d, e, f int = 4, 5, 6
var g, h, i = 7, 8, 9
j, k, l := 10, 11, 12
对于不同类型的变量必须要分开声明,但是 var 关键字可以共享。
var (
a int = 1
b string = "hello"
c bool = true
)
在 Go 中,一个变量只进行了声明而没有赋值,那么它的值就是该类型的零值。以下是 Go 中的一些零值:
- 整型的零值是 0
- 浮点型的零值是 0.0
- 布尔型的零值是 false
- 字符串的零值是 ""
- 数组的零值是一个全是零值的数组
- 接口(interface)、切片(slice)、通道(channel)、字典(map)、指针(pointer)、函数(func)的零值是 nil(空指针)
- 结构体的零值是每个字段都是零值
常量(Constant)
常量即固定值,不可修改。
Go 语言中的常量声明格式为:
const 常量名 常量类型 = 常量值
例如:
const a int = 1
const b string = "hello"
const c bool = true
还有一种写法
const (
a int = 1
b string = "hello"
c bool = true
)
还可以使用 iota
来简化常量的定义:
const (
a = iota // 0
b // 1
c // 2
)
iota
是一个特殊的常量,它的值默认为 0,每次使用后自动加 1。第一行使用后,后面行可以省略 iota
。
iota
计数不会中断:
const (
a = iota // 0
b // 1
c = 100 // 100
d = iota // 3
e // 4
)
但是如果在中断后不再使用 iota
,则会重新计数:
const (
a = iota // 0
b // 1
c = 100 // 100
d // 100, 未显式赋值,将使用最近的赋值
e = 200 // 200
f // 200
)
iota
需要谨慎使用,因为如果插入一个值将会产生骨牌效应,例如数据库中一张表使用枚举值表示状态,如果插入一个状态,将会导致后面的状态值全部改变,这将与之前的业务表达不符。例如:
cosnt {
INIT = iota
START
STOP
PAUSE
FINISH
}
在数据库中,上述的状态值分别对应 0、1、2、3、4。当某天程序员需要在 START
和 STOP
之间插入一个状态,例如为 RUNNING
,那么 STOP
的值将变为 3,PAUSE
的值将变为 4,FINISH
的值将变为 5。这将导致数据库中的状态值全部改变,这是不可接受的。
最佳使用 iota
的场景是在枚举类型中,例如:
const (
MONDAY = iota
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
)
但是如果我们希望从 1 开始计数,可以使用占位符 _
:
const (
_ = iota
MONDAY
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
)
当然也可以使用 iota+1
:
const (
MONDAY = iota + 1
TUESDAY
WEDNESDAY
THURSDAY
FRIDAY
SATURDAY
SUNDAY
)
全局变量/常量
变量通常的作用域为当前代码块,如果想要在整个包中使用,可以使用全局变量。全局变量和函数具有相同的作用域。
全局变量的声明格式为:
var 变量名 变量类型
注意,全局变量的声明不能使用 :=
,只能使用 var
。
全局跨包变量/常量
如果想要在其他包中使用全局变量,需要将变量名首字母大写。
例如:
// demo/note/note.go
package note
var Note string = "hello"
// demo/main.go
import (
"demo/note"
"fmt"
)
func main() {
fmt.Println(note.Note)
}
没有多余的局部变量
Go 语言中,如果声明了一个局部变量,但是没有使用,编译器会报错。
func main() {
var a int
}
这里声明了一个变量 a
,但是没有使用,编译器会报错。这是因为 Go 力求保持代码整洁。
全局变量不会出现这种情况,未使用的全局变量不会报错。
常量也可以不使用,不会报错。这是因为常量在编译器就已经决定,导致的副作用较小。
基本数据类型
Go 中,所有的值类型变量常量都会在声明时被分配空间并赋予默认值。
bit: 位,计算机中最小的数据单位,只有 0 和 1 两种状态。 Byte:字节,计算机中基本的存储单位,1 Byte = 8 bit。
整型
整型分为有符号整型和无符号整型。有符号整型包括 int8、int16、int32、int64 和 int。无符号整型包括 uint8、uint16、uint32、uint64 和 uint。
类型 | 位数 | 取值范围 |
---|---|---|
int8 | 8 | -128 ~ 127 |
uint8 | 8 | 0 ~ 255 |
int16 | 16 | -32768 ~ 32767 |
uint16 | 16 | 0 ~ 65535 |
int32 | 32 | -2147483648 ~ 2147483647 |
uint32 | 32 | 0 ~ 4294967295 |
int64 | 64 | -9223372036854775808 ~ 9223372036854775807 |
uint64 | 64 | 0 ~ 18446744073709551615 |
int | 32/64 | 与操作系统有关,根据操作系统的位数决定 |
uint | 32/64 | 与操作系统有关 |
为什么有了这么多版本的 int 和 unit 之后,还要设计单独的 int 和 unit 类型呢?
- 能够避免选择,程序员通常并不关注数据长度的具体大小,如果 32 或者 64 都可以满足需求,那么就可以使用 int 或者 unit。
- 提升效率。CPU 按照自己的字长(word size)的整数倍来处理数据肯定是效率最高的,所以 int 和 unit 会根据 CPU 的字长来选择 32 或者 64 以提升效率。
var a int8 = 1
var b uint8 = 2
var c int16 = 3
var d int = 4
支持二进制、八进制、十进制、十六进制的表示方式:
var a int = 0b1010 // 二进制
var b int = 0O12 // 八进制
var c int = 10 // 十进制
var d int = 0xA // 十六进制
精度转换:
// 低精度转高精度
var a int8 = 1
var b int16 = int16(a)
var c int32 = int32(b)
var d int64 = int64(c)
var e int = int(d)
// 高精度转低精度
var f int = 1
var g int64 = int64(f)
var h int32 = int32(g)
var i int16 = int16(h)
var j int8 = int8(i)
浮点型
浮点型分为单精度浮点型和双精度浮点型。单精度浮点型为 float32,双精度浮点型为 float64。
类型 | 位数 |
---|---|
float32 | 32 |
float64 | 64 |
var a float32 = 1.1
var b float64 = 2.2
浮点型将导致精度丢失
在计算机中,整数是能够准确表达的,因为十进制整数转化为二进制是因为所有的整数的基石 1 是能够通过二进制准确表达的,但是浮点数的小数部分利用 2 的幂次方来表示,例如 2 的 -1 次方表示 0.5,2 的 -2 次方表示 0.25,但是 0.1 无法通过有限的二进制数来准确表示,这就导致了浮点数的精度丢失。
浮点数在计算机中表示示意如下:
- IEEE 32
符号位 | 指数位 | 尾数位 |
---|---|---|
1bit | 8bit | 23bit |
- IEEE 64
符号位 | 指数位 | 尾数位 |
---|---|---|
1bit | 11bit | 52bit |
不同于 Int 和 Uint 类型,Go 语言中没有 float 关键字来声明 32 位的浮点数或者与平台匹配的浮点数,只有 float32 和 float64。这可能是因为浮点数并不能准确表达一个数字,所以在这里程序员需要关注精度,所以需要明确声明。
由于精度缺失,浮点数不能使用 ==
>
<
来比较,而应使用 big 包中的函数。
import "math/big"
func main() {
a := 3.14
b := 3.14
result := big.NewFloat(a).Cmp(big.NewFloat(b))
if result == 0 {
fmt.Println("a == b")
} else if result == 1 {
fmt.Println("a > b")
} else {
fmt.Println("a < b")
}
}
字符型
类型 | 位数 | 取值范围 |
---|---|---|
byte | 8 | |
rune | 32 |
var a byte = 'a'
var b rune = 'b'
// 格式化打印字符
fmt.Printf("%c\n", a)
// 直接打印字符
fmt.Println(b)
// 强制转换为字符串
fmt.Println(string(a))
这里的 byte
和 rune
都是别名,实际上是 uint8
和 int32
来存储的。
布尔型
类型 | 位数 |
---|---|
bool | 1 |
var a bool = true
var b bool = false
字符串
字符串是一串固定长度的字符连接起来的字符序列。
var a string = "hello"
var b string = "world"
字符串在内存中存储时,实际上是一个字节数组 []byte
,每个字符占用一个字节。字符串的长度是字符的个数,而不是字节数。
字符串的长度可以通过 len
函数获取,但是其用于获得变量的实际的存储空间,即底层字节数组的长度。
package main
import {
"fmt"
}
func main{
s:= "字符串"
len:= len(s)
fmt.Println(len) // 9
for i:= 0; i < len; i++ {
fmt.Printf("%c\n", s[i])
}
}
执行结果:
9
E5 AD 97 E7 AC A6 E4 B8 B2
在这里打印的“字符串”的长度为 9,这对应每个汉字使用 3 个字节来存储。打印出来的内容也不是字符本身,而是字符的 UTF-8 编码。
rune 类型
如果我们希望以自然语言的视角来处理字符串,我们需要使用 rune
类型。
package main
import {
"fmt"
}
func main{
s:= "字符串"
fmt.Println(utf8.RuneCountInString(s)) // 3
for _, r:= range s {
fmt.Printf("%T, %X\n", r, r)
}
}
打印结果:
3
int32, 5B57
int32, 7B26
int32, 4E32
复数
类型 | 位数 | 取值范围 |
---|---|---|
complex64 | 64 | |
complex128 | 128 |
var a complex64 = 1 + 2i
var b complex128 = 3 + 4i
辨认数据类型
Go 语言中可以使用 reflect
包来辨认数据类型。
import "reflect"
func main() {
var a int = 1
var b string = "hello"
var c bool = true
var d float32 = 1.1
var e float64 = 2.2
var f byte = 'a'
var g rune = 'b'
var h complex64 = 1 + 2i
var i complex128 = 3 + 4i
fmt.Println(reflect.TypeOf(a))
fmt.Println(reflect.TypeOf(b))
fmt.Println(reflect.TypeOf(c))
fmt.Println(reflect.TypeOf(d))
fmt.Println(reflect.TypeOf(e))
fmt.Println(reflect.TypeOf(f))
fmt.Println(reflect.TypeOf(g))
fmt.Println(reflect.TypeOf(h))
fmt.Println(reflect.TypeOf(i))
}