内存分配
Go 同样也垃圾收集,也就是说无须担心内存分配和回收。
Go 有两个内存分配原语,new 和 make。它们应用于不同的类型,做不同的工作, 可能有些迷惑人,但是规则很简单。
1、用 new 分配内存
内建函数 new 本质上说跟其他语言中的同名函数功能一样: new(T) 分配了零值填充的 T 类型的内存空间,
并且返回其地址,一个 *T 类型的值。用 Go 的术语 说,它返回了一个指针,指向新分配的类型 T 的零值。
有一点非常重要:
new 返回指针。
这意味着使用者可以用 new 创建一个数据结构的实例并且可以直接工作。
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer 的值在分配内存或定义之后立刻就可以使用。
在这个片段中, p 和v 都可以在没有任何更进一步处理的情况下工作。
p := new(SyncedBuffer) ← Type *SyncedBuffer,已经可以使用
var v SyncedBuffer ← Type SyncedBuffer,同上
2、用 make 分配内存
内建函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建 slice,map 和 channel,
并且返回一个有初始值(非零)的 T 类型,而不是 *T。
本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前 必须被初始化。
3、new 和 make 的区别
例如,一个 slice,是一个包含指向数据(内部 array)的指针, 长度和容量的三项描述符;
在这些项目被初始化之前,slice 为 nil。对于 slice, map 和 channel,
make 初始化了内部的数据结构,填充适当的值。
make 返回初始化后的(非零)值。
例如, make([]int, 10, 100) 分配了 100 个整数的数组,然后用长度 10 和容量 100 创建了 slice
结构指向数组的前 10 个元素。区别是,new([]int) 返回指向新分配的内存的指针,
而零值填充的 slice 结构是指向 nil 的 slice 值。
这个例子展示了 new 和 make 的不同。
var p *[]int = new([]int) ← 分配 slice 结构内存;*p == nil, 已经可用
var v []int = make([]int,100) ← v 指向一个新分配的有 100 个整数的数组。
var p *[]int = new([]int) ← 不必要的复杂例子
*p = make([]int, 100, 100)
v := make([]int, 100) ← 更常见
务必记得 make 仅适用于 map,slice 和 channel,并且返回的不是指针。
应当用 new 获得特定的指针。
new 分配;make 初始化上面的两段可以简单总结为:
? new(T) 返回 *T 指向一个零值 T
? make(T) 返回初始化后的 T
当然 make 仅适用于 slice,map 和 channel。
4、构造函数与复合声明
有时零值不能满足需求,必须要有一个用于初始化的构造函数
例如这个来自 os 包的例子。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
有许多冗长的内容。可以使用复合声明使其更加简洁,每次只用一个表达式创建一个新的实例。
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0} ← Create a new File
return &f ← Return the address of f
}
返回本地变量的地址没有问题;在函数返回后,相关的存储区域仍然存在。
事实上,从复合声明获取分配的实例的地址更好,
因此可以最终将两行缩短到一行。
return &File{fd, name, nil, 0}
在特定的情况下,如果复合声明不包含任何字段,它创建特定类型的零值。
表达式 new(File) 和 &File{} 是等价的。
复合声明同样可以用于创建 array,slice 和 map,通过指定适当的索引和 map 键 来标识字段。
十二、定义自己的类型
1、Go 允许定义新的类型,通过保留字 type 实现:
type foo int
创建了一个新的类型 foo 作用跟 int 一样。创建更加复杂的类型需要用到 struct 保留字。
这有个在一个数据结构中记录某人的姓名(string)和年龄(int),
并且使其成为一个新的类型的例子:
package main
import "fmt"
type NameAge struct {
name string ← 不导出
age int ← 不导出
}
func main() {
a := new(NameAge)
a.name = "Pete"
a.age = 42
fmt.Printf("%v\n", a)
}
通常,fmt.Printf("%v\n", a) 的输出是 &{Pete 42}
如果仅想打印某一个,或者某几个结构中的 字段,需要使用 .<field name>。例如,仅仅打印名字:
fmt.Printf("%s", a.name) ← %s 格式化字符串
2、结构字段
之前已经提到结构中的项目被称为field。
没有字段的结构:struct {}
有四个字段的:
struct {
x, y int
A *[]
int F func()
}
如果省略字段的名字,可以创建匿名字段,例如:
struct {
T1 ← 字段名字是 T1
*T2 ← 字段名字是 T2
P.T3 ← 字段名字是 T3
x,y int ←字段名字是x 和y
}
注意首字母大写的字段可以被导出,也就是说,在其他包中可以进行读写。
字段名以小写字母开头是当前包的私有的。包的函数定义是类似的。
3、方法
可以对新定义的类型创建函数以便操作,可以通过两种途径:
1. 创建一个函数接受这个类型的参数。
func doSomething(in1 *NameAge, in2 int) { /* ... */ }
(你可能已经猜到了)这是 函数调用。
2. 创建一个工作在这个类型上的函数:
func (in1 *NameAge) doSomething(in2 int) { /* ... */ }
这是方法调用,可以类似这样使用:
var n *NameAge
n.doSomething(2)
这里 Go 会查找 NameAge 类型的变量 n 的方法列表,没有找到就会再查找 *NameAge
类型的方法列表,并且将其转化为 (&n).doSomething(2)。
使用函数还是方法完全是由程序员说了算,但是若需要满足接口就必须使用方法。
如果没有这样的需求,那就完全由习惯来决定是使用函 数还是方法了。
如果 x 可获取地址,并且 &x 的方法中包含了 m, x.m() 是 (&x).m() 更
短的写法。
十四、转换
1、有时需要将一个类型转换为另一个类型。在 Go 中可以做到,不过有一些规则。
首先,将一个值转换为另一个是由操作符(看起来像函数:byte())完成的,
并且不是所有的转换都是允许的。
从string到字节或者ruin的slice。
mystring := "hello this is string"
byteslice := []byte(mystring)
转换到 byte slice,每个 byte 保存字符串对应字节的整数值。
注意 Go 的字符串是 UTF-8 编码的,一些字符可能是 1、2、3 或者 4 个字节结尾。
runeslice := []rune(mystring)
转换到 rune slice,每个 rune 保存 Unicode 编码的指针。
字符串中的每个字符对应一个整数。
从字节或者整形的slice到string。
b := []byte{‘h‘,‘e‘,‘l‘,‘l‘,‘o‘} ← 复合声明
s := string(b)
i := []rune{257,1024,65}
r := string(i)
对于数值,定义了下面的转换:
? 将整数转换到指定的(bit)长度:uint8(int);
? 从浮点数到整数:int(float32)。这会截断浮点数的小数部分;
? 其他的类似:float32(int)。
2、用户定义类型的转换
如何在自定义类型之间进行转换?这里创建了两个类型 Foo 和 Bar,
而 Bar 是 Foo 的一个别名:
type foo struct { int } ← 匿名字段
type bar foo ← bar 是 foo 的别名
然后:
var b bar=bar{1} ←声明b 为bar 类型
var f foo=b ←赋值b 到f
最后一行会引起错误:
cannot use b (type bar) as type foo in assignment(不能使用 b(类型 bar)作为
类型 foo 赋值)
这可以通过转换来修复:
var f foo = foo(b)
注意转换那些字段不一致的结构是相当困难的。
同时注意,转换 b 到 int 同样 会出错;
整数与有整数字段的结构并不一样。