2.1 变量
从计算机系统实现角度来看,变量是一段或多段用来存储数据的内存。
作为静态类型语言,Go变量总是有固定的数据类型,类型决定了变量内存的长度和存储格式。我们只能修改变量值,无法改变类型。
通过类型转换或指针操作,我们可用不同方式修改变量值,但不能修改变量类型。
因为内存分配发生在运行期,所以在编码阶段我们给变量起名来表示这段内存。实际上,编译后的机器码压根不用变量名,而是直接用内存地址来访问目标数据。
定义
关键字var用于定义变量。运行时内存分配会确保变量自动初始化为二进制零值(zero value),如果显式提供初始化值则可省略变量类型,由编译器推断。
func main(){
var x,y int
var a,s = 100,"xyz"
}
简短模式
简短模式限制:
- 定义变量,同时显式初始化;
- 不能提供数据类型
- 只能用在函数内部
第三条尤其要注意,否则可能造成如下栗子,本想修改全局变量,结果变成重新定义一个同名的局部变量。
var m = 10
func main() {
println(&m,m)
m:=1000
println(&m,m)
}
输出:
0x4a4138 10
0xc04202ff68 1000
不同作用域,可以重新定义变量。
func main() {
m := 10
println(&m,m)
{
m:="xyz"
println(&m,m)
}
}
多变量赋值
进行多变量赋值时,先计算出所有右边的值,再一次完成赋值操作。
func main() {
x,y:=1,2
x,y=y+3,x+2 //先算y+3,x+2,再对x,y赋值
println(x,y)
}
输出:
go build && 2.1变量.exe
3, 5
C:\projects\gocode\src\learngo\golang_nodes\2.类型>go tool objdump -s "main\.main" 2.1变量.exe
TEXT main.main(SB) C:/projects/gocode/src/learngo/golang_nodes/2.类型/2.1变量.go
2.1变量.go:5 0x450180 65488b0c2528000000 MOVQ GS:0x28, CX
2.1变量.go:5 0x450189 488b8900000000 MOVQ 0(CX), CX
2.1变量.go:5 0x450190 483b6110 CMPQ 0x10(CX), SP
2.1变量.go:5 0x450194 7646 JBE 0x4501dc
2.1变量.go:5 0x450196 4883ec10 SUBQ $0x10, SP
2.1变量.go:5 0x45019a 48896c2408 MOVQ BP, 0x8(SP)
2.1变量.go:5 0x45019f 488d6c2408 LEAQ 0x8(SP), BP
2.1变量.go:8 0x4501a4 e8d756fdff CALL runtime.printlock(SB)
2.1变量.go:8 0x4501a9 48c7042405000000 MOVQ $0x5, 0(SP)
2.2 常量和枚举
const (
_=iota
KB = 1<<(10*iota) //1<< 10*1
MB //1<< 10*2
GB
)
const(
_,_=iota,iota*10 //0 , 0*10
a,b //1, 1*10
c,d //2, 2*10
x,y //同上
e,f = 100,200 // 100,200
g,h=iota,iota*10 // 4, 4*100 恢复iota
)
如果iota中断,那么后面要用枚举必须得显式恢复iota,而且后续的值按照行号递增。
自增的数据类型默认为int,可显式指定类型。
const (
a float32 = iota
在实际编码中,建议用自定义类型实现用途明确的枚举类型。但这并不能将取值范围限定在预定义的枚举值内。
常量和变量的不同
变量在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。
数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址。
2.3 基本类型
清晰完备的预定义基础类型,使得开发跨平台应用时无须过多考虑符号和长度差异。
基本类型.png
基本类型2.png
支持八进制、十六进制以及科学计数法。标准库math定义了各数字类型的取值范围。
func main() {
a,b,c:=100,0144,0x64
fmt.Println(a,b,c)
fmt.Printf("0b%b,%#o,%#x\n",a,a,a)
fmt.Println(math.MinInt8,math.MaxInt8)
}
输出
100 100 100
0b1100100,0144,0x64
-128 127
标准库strconv可在不同进制(字符串)间转换。
package main
import (
"math"
"strconv"
"fmt"
)
func main() {
x,y,z:=100,0144,0x64
fmt.Println(x,y,z)
fmt.Printf("0b%b,%#o,%#x\n",x,y,z)
fmt.Println(math.MinInt8,math.MaxInt8)
a,_:=strconv.ParseInt("1100100",2,32)
b,_:=strconv.ParseInt("0144",8,32)
c,_:=strconv.ParseInt("64",16,32)
fmt.Println(a,b,c)
println("0b"+strconv.FormatInt(a,2))
println("0"+strconv.FormatInt(a,8))
println("0x"+strconv.FormatInt(a,16))
}
浮点数注意小数位的有效精度。
var a float32 = 1.1234567899 //注意,默认浮点类型是float64
var b float32 = 1.123456789
var c float32 = 1.1234567891
println(a,b,c)
println(a==b,b==c)
fmt.Printf("%v,%v,%v\n",a,b,c)
输出:
1.1234568,1.1234568,1.1234568 //小数点后7位,四舍五入
+1.123457e+000 +1.123457e+000 +1.123457e+000
true true
变量别名
官方语言规范中有两个别名。
byte alias for uint8
rune alias for int32
别名类型无须转换,可直接赋值:
func main(){
var a byte=0x11
var b uint8=a
var c uint8=a+b
Test(c)
}
func Test(x byte){
println(x)
}
但需要注意的是,拥有相同底层结构的就是别名。例如在64位平台上,int和int64结构完全相同,但它俩也分属不同类型,须显示转换。
2.4 引用类型
所谓引用类型特指slice、map、channel这三种预定义类型。
相比数字、数组等类型,引用类型拥有更复杂的存储结构。除分配内存外,他们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。
内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。
P.S. :注意new和make的区别:
小结:
new和make都在堆上分配内存,但是它们的行为不同,适用于不同的类型。
new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为 T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{}。
make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:slice、map 和 channel。
new其实完全可以不用。
避免语法歧义
在进行类型转换时,如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以免造成语法分解错误。
如下,如果*int不加括号就会报错
func main() {
x:=100
p:=(*int)(&x)
fmt.Printf("%#v\n",*p)
println(p)
}
2.5 自定义类型
使用关键字type定义用户自定义类型,包括基于现有基础类型创建,或者是结构体、函数类型等。
import "fmt"
type flags byte
const(
exec flags = 1 <<iota
write
read
)
func main() {
f:=read | exec
fmt.Printf("%b\n",f)
}
输出: 101
和var、const类似,多个type定义可合并成组,可在函数或代码内定义局部类型。
type (
flags byte
user struct {
name string
age byte
}
event func(string)bool
)
func main() {
u:=user{"bolen",18}
fmt.Println(u)
var ff event= func(s string) bool {
println(s)
return s!=""
}
ff("abc")
}
即便指定了基础类型,也只是表明他们有相同的底层数据结构,两者间是完全不同的数据类型,一定不能视作别名,不能隐式转换,不能直接用于比较表达式。
struct tag
最容易忽视的是struct tag,它也属于类型组成部分,而不仅仅是元数据描述。
package main
import (
"fmt"
)
func main() {
var a struct{ //匿名结构类型
x int `x`
s string `s`
}
var b struct{
x int
s string
}
b=a //错误:cannot use a (type struct { x int "x"; s string "s" })
// as type struct { x int; s string } in assignment
fmt.Println(b)
}
同样,函数的参数顺序也属签名组成部分。
package main
func main() {
var a func (int string)
var b func (string int)
b=a //报错
b("s",1)
}
2.6 未命名类型
与有明确标识符的bool、int、string等类型相比,数组、切片、字典、通道等类型与具体元素类型或长度等熟悉有关,故称作未命名类型。当然,也可以用type围棋命名,从而盖面命名类型。
未命名类型转换规则:
- 所属类型相同。
- 基础类型相同,且其中一个是未命名类型。
- 数据类型相同,将双向通道赋值给单向通道,且其中一个为未命名类型。
- 将默认值nil赋值给切片、字典、通道、指针、函数或接口。
- 对象实现了目标接口。
package main
import "fmt"
func main() {
type data [2]int
var d data = [2]int{1,2} //基础类型相同,右值为未命名类型
fmt.Println(d)
a:=make(chan int,2)
var b chan<- int =a //双向通道转换为单向通道,其中b为未命名类型。
b<-2
}