1.1变量
多变量赋值时,先计算所有相关值,然后再从左到右依次赋值
data, i := [3]int{0, 1, 2}, 0
fmt.Printf("初始化变量 data=%v, i=%v \n", data, i)
i, data[i] = 2, 100
fmt.Printf("赋值后的变量 data=%v, i=%v", data, i)
/*
初始化变量 data=[0 1 2], i=0
赋值后的变量 data=[100 1 2], i=2
*/
重新赋值和定义新的同名变量是有区别的,同名的变量需要在不同层次的代码块中
s := "abc"
fmt.Printf("申明新的变量s的变量地址=%v \n", &s)
s, y := "hello", 20 //重新赋值,将先前申明的s变量重新赋值
fmt.Printf("重新赋值s的变量地址=%v, y变量=%v \n", &s, y)
{
s, z := 1000, 30 //在不同层次的代码块中定义同名变量
fmt.Printf("不同层次代码块中赋值的新变量s的地址=%v, 变量z=%v", &s, z)
}
/*
申明新的变量s的变量地址=0xc000088220
重新赋值s的变量地址=0xc000088220, y变量=20
不同层次代码块中赋值的新变量s的地址=0xc0000aa058, 变量z=30
*/
1.2常量
在常量组中,如不提供类型和初始化值,那么视作与上一常量相同
const (
s = "abc"
x //x="abc"
)
如果常量类型足以存储初始化值,那么不会引发溢出错误。
const (
a byte = 100 //int to byte
b int = 1e20 //float64 to int, overflows
)
//此时的b常量引发了溢出错误
关于枚举,关键字iota定义常量组中从0开始按行计数的自增枚举值。
const (
_ = iota //iota=0
KB int64 = 1 << (10 * iota) //iota=1 此时KB=2e10=1024
MB //与KB表达式相同,但iota=2
GB
TB
)
如果iota自增被打断,需要显式恢复。
const (
A = iota //iota=0
B //iota=1
C = "c" //C=c
D //D=c,与上一行相同
E = iota //iota=4,显式恢复,注意计数包含了C、D两行
F //iota=5
)
可通过自定义类型来实现枚举类型限制
type Color int
const (
Black Color = iota
Red
Blue
)
func PrintColor(c Color) {
fmt.Printf("打印颜色为 %v \n", c)
}
func main() {
c := Black
PrintColor(c)
// x := 1
// PrintColor(x) //cannot use x (type int) as type Color in argument to PrintColor
PrintColor(1) //常量会被编译器自动转换
}
1.3基本类型
有符号和无符号的区别在于举个栗子:
int8范围在-128~127之间
unit8范围在0~255之间
总的个数为2e8=256个数值
值类型:声明变量之后可以直接使用
array 列表 值类型
struct 结构体 值类型
string UTF-8字符串 值类型
引用类型:对于引用类型,若使用var进行声明变量,必须使用make函数对其进行分配内存。若不初始化,该变量默认值为nil,向其添加元素时会导致panic。
slice 切片 引用类型
map 字典 引用类型
channel 通道 引用类型
interface 接口 引用类型
1.4引用类型
引用类型包括slice、map、channel、interface,它们有复杂的内部结构,除了申请内存外还需要初始化相关属性。
内置函数new计算类型大小,为其分配零值内存,返回指针。
而make会被编译器翻译成具体的创建函数,由其分配内存和初始化成员结构,返回对象而非指针
a := []int{0, 0, 0} //初始化时提供初始化值
a[1] = 10
b := make([]int, 3) //make slice,初始化值和内存地址
b[1] = 10
c := new([]int) //仅仅只是分配地址,并没有分配内存
c[1] = 10 //Error: invalid operation: c[1] (type *[]int does not support indexing)
1.5类型转换
不支持隐式转换,即便是从窄向宽转换也不行。
var b byte = 100
// var n int = b //Error: cannot use b (type byte) as type int in assignment
var n int = int(b) //显式转换
fmt.Printf("申明变量n=%v", n)
使用括号避免优先级错误,也不能将其他类型当bool值使用
*Point(p) // 相当于*(Point(p))
(*Point)(p)
<-chan int(c) //相当于<-(chan int(c))
(<-chan int)(c)
a := 100
if a { //Error: non-bool a (type int) used as if condition
fmt.Printf("true")
}
1.6字符串
字符串是不可变值类型,内部用指针指向UTF-8字节数组
不能用序号获取字节元素指针,如:&s[i]非法
不可变类型,无法修改字节数组
字节数组尾部不包含NULL
struct String
{
byte* str;
intgo len;
};
使用“`”定义不做转移处理的原始字符串,支持跨行。
s := `a
b\r\n\x00
c`
fmt.Printf("使用“`”定义不做转移处理的原始字符串,s=%v", s)
连接跨行字符串时,“+”必须在上一行末尾,否则导致编译错误
s := "Hello, " +
"word!"
fmt.Printf("字符串s=%v \n", s)
s1 := "Hello, "
+"word!" //Error:invalid operation: +"word!" (operator + not defined on untyped string)
fmt.Printf("字符串s1=%v \n", s1)
字符串支持两个索引号返回子串,但是子串依然指向原字节数组,仅修改了指针和长度属性。
单引号字符常量表示Unicode Code Point,支持\uFFFF、\U7FFFFFFF、\xFF格式,对应rune类型(rune 是int32的别名),UCS-4
注意:
- 值得一提的是Unicode 是国际通用字符集,涵盖了全球所有字符,并规定了每个字符对应到唯一的代码值(code point),代码值 从 0000 ~ 10FFFF 共 1114112 个值 。
- 而对这些字符进行编码的方式则多种多样。真正存储的时候需要多少个字节是由具体的编码格式决定的。比如:字符 「A」用 UTF-8 的格式编码来存储就只占用1个字节,用 UTF-16 就占用2个字节,而用 UTF-32 存储就占用4个字节。
fmt.Printf("字符串'a'的类型=%T \n", 'a')
var c1, c2 rune = '\u6211', '们'
fmt.Printf("(c1=='我')=%v,(string(c2)=='\xe4\xbb\xac')=%v \n", c1 == '我', string(c2) == "\xe4\xbb\xac")
/*
字符串'a'的类型=int32
(c1=='我')=true,(string(c2)=='们')=true
*/
要修改字符串,可先将其转换成[]rune或[]byte,完成后再转换为string,无论哪种转换,都会重新分配内存,并复制字节数组。
s := "abcd"
bs := []byte(s)
bs[1] = 'B'
fmt.Printf("字符串bs=%v \n", string(bs))
u := "电脑"
us := []rune(u)
us[1] = '话'
fmt.Printf("字符串us=%v \n", string(us))
/*
字符串bs=aBcd
字符串us=电话
*/
用for循环遍历字符串时,也有byte和rune两种方式。
注意:Go语言中byte和rune实质上就是uint8和int32类型。byte用来强调数据是raw data,而不是数字;而rune用来表示Unicode的code point
s := "abc汉字"
for i := 0; i < len(s); i++ { //byte
fmt.Printf("%c,", s[i])
}
fmt.Println()
for _, r := range s { //rune
fmt.Printf("%c,", r)
}
/*
a,b,c,æ,±,,å,,,
a,b,c,汉,字,
*/
1.7指针
支持指针类型T,指针的指针**T。
默认值为nil,没有NULL常量
操作符“&”取变量地址,“”透过指针访问目标对象。
不支持指针运算,不支持“->”运算符,直接用“.”访问目标成员。
注意
- 具体类型的指针,如*int,*string。
- unsafe.Pointer,在unsafe下面。
- uintptr,能存储任何类型的指针的类型。
- 不能对指针做加减法等运算。
type data struct {
a int
}
var d = data{1234}
var p *data
p = &d
fmt.Printf("%p, %v \n", p, p.a)
/*
0xc000014098, 1234
*/
可以在unsafe.Pointer和任意类型指针间进行转换。
x := 0x12345678
p := unsafe.Pointer(&x) //将x的地址转换成通用的指针类型 *int -> Pointer
n := (*[4]byte)(p) //Pointer -> *[4]byte
for i := 0; i < len(n); i++ {
fmt.Printf("%X ", n[i])
}
/*
78 56 34 12
*/
unsafe.Pointer 和 uintptr的区别
- uintptr是一个整数类型。即使uintptr变量仍然有效,由uintptr变量表示的地址处的数据也可能被GC回收。
- unsafe.Pointer是一个指针类型。但是unsafe.Pointer值不能被取消引用。如果unsafe.Pointer变量仍然有效,则由unsafe.Pointer变量表示的地址处的数据不会被GC回收。unsafe.Pointer是一个通用的指针类型,就像* int等。
由于uintptr是一个整数类型,uintptr值可以进行算术运算。 所以通过使用uintptr和unsafe.Pointer,我们可以绕过限制,* T值不能在Golang中计算偏移量
注意:GC把uintptr当成普通整数对象,它无法阻止“关联”对象被回收。
a := [4]int{0, 1, 2, 3}
p1 := unsafe.Pointer(&a[1])
p3 := unsafe.Pointer(uintptr(p1) + 2 * unsafe.Sizeof(a[0]))
*(*int)(p3) = 6
fmt.Println("a =", a) // a = [0 1 2 6]
type Person struct {
name string
age int
gender bool
}
who := Person{"John", 30, true}
pp := unsafe.Pointer(&who)
pname := (*string)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.name)))
page := (*int)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.age)))
pgender := (*bool)(unsafe.Pointer(uintptr(pp) + unsafe.Offsetof(who.gender)))
*pname = "Alice"
*page = 28
*pgender = false
fmt.Println(who) // {Alice 28 false}
返回局部变量指针是安全的,编译器会根据需要将其分配在GC Heap上。
func test() *int {
x := 100
return &x //在堆上分配x内存,但在内联时,也可能直接分配在目标栈。
}
堆和栈空间
- 栈空间(stack),我们在程序中所定义的定义的局部变量int、局部数组等都是存储在栈空间中。栈空间具有一个鲜明的特点:函数内定义的变量出了函数范围,其所占用的内存空间自动释放。但是,栈空间的尺寸有最大限制,不适合分配大空间使用;所以,因为栈空间出了函数范围就释放,所以不适合要给其他地方使用的内存需求。其最大的好处就在于:不用程序员手动释放内存。因此通常建议不要把局部变量的指针做为返回值返回,但是在go语言中是允许的。
- 堆空间(heap),一般是由程序员分配释放,若程序员不释放的话,程序结束时可能由OS回收。它与数据结构的堆是两回事,分配方式倒是类似于数据结构的链表。用于存放程序运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
- 对于go语言来说,Go的编译器会决定在哪(堆or栈)分配内存,保证程序的正确性。Go的编译器很聪明(自作聪明),它还会做逃逸分析(escape analysis),如果它发现变量的作用域没有跑出太远,它就可以在栈上分配空间而不是堆。比如如下这段代码,就不会在堆上分配内存,即使我们用new分配。
const Width, Height = 640, 480
type Cursor struct {
X, Y int
}
func Center(c *Cursor) {
c.X += Width / 2
c.Y += Height / 2
}
func CenterCursor() {
c := new(Cursor)
Center(c)
fmt.Println(c.X, c.Y)
}
//经过变量逃逸分析
/*
go tool compile -m test.go
test.go:17: can inline Center
test.go:24: inlining call to Center
test.go:25: c.X escapes to heap
test.go:25: c.Y escapes to heap
test.go:23: CenterCursor new(Cursor) does not escape
test.go:25: CenterCursor ... argument does not escape
test.go:17: Center c does not escape
*/
//参数-m是打印出编译优化。从输出上看,它说new(Cursor)没有escape,于是在栈上分配了。
//Go一方面会把一些,看上去会在栈上分配的东西,移到堆上分配;另一方面又会把看上去会在堆上分配的东西,在栈上分配。由编译优化那边做逃逸分析来控制。
1.8自定义类型
可将类型分为命名和未命名两大类。命名类型包括bool、int、string等,而array、slice、map等和具体元素类型、长度等有关,属于未命名类型。具有相同声明的未命名类型被视为同一类型。
具有相同基类型的指针
具有相同元素类型和长度的array
具有相同元素类型的slice
具有相同键值类型的map
具有相同元素类新型和传送方向的channel
具有相同字段序列(z字段名、类型、标签、顺序)的匿名struct
签名相同(参数和返回值,不包括参数名称)的function
方法集相同(方法名、方法签名相同,和次序无关)的interface
var a struct {
x int `a`
}
var b struct {
x int `ab`
}
b = a //Error: cannot use a (type struct { x int "a" }) as type struct { x int "ab" } in assignment
可用type在全局或函数内定义新类型
type bigint int64
var x bigint = 100
fmt.Printf("新类型变量值=%v", x)
新类型不是原类型的别名,除拥有相同数据存储结构外它们之间没有任何关系,不会持有原类型任何信息,除非目标类型是未命名类型,否则必须显示转换
type bigint int64
x := 1234
var b bigint = bigint(x) //必须显示转换,除非是常量
var b2 int64 = int64(b)
fmt.Printf("int64变量b2=%v \n", b2)
type myslice []int
var s myslice = []int{1, 2, 3} //未命名类型,隐式转换
var s2 []int = s
fmt.Printf("int变量s2=%v", s2)
有趣的是,虽然所有数值类型如整形int、字符串类型string、布尔类型bool,都是defined type;defined type意味着必须要显示转换
但是map、数组、切片、结构体、channel等原生复合类型(composite type)都不是defined type。意味着这些符合类型却可以支持隐式转换。
package main
type MyInt int
type MyMap map[string]int
func main() {
var x MyInt
var y int
x = y // 会报错: cannot use y (type int) as type MyInt in assignment
_ = x
var m1 MyMap
var m2 map[string]int
m1 = m2 // 不会报错
m2 = m1 // 不会报错
}