数据
用new(T)
进行分配
new
是一个内置函数,用来为变量分配内存。但和其他语言中的new
不一样,该方法并不会初始化所分配的内存,而只是清空它。也就是说,new(T)
为一个类型为T的项分配已清零的内存,然后返回该内存地址(一个类型为*T
的值)。用Go的话来讲,它会返回一个指向一个新分配的类型为T的零值的指针。
利用new
返回的内存使用类型零值的特点,自定义的数据结构在调用new
之后,其字段就可以直接使用而不需要进一步的初始化了。例如对于下面的类型声明: 1
2
3
4
5
6type SyncedBuffer struct {
lock sync.Mutext
buffer bytes.Buffer
}
p := new(SyncedBuffer) // *SyncedBuffer类型
var v SyncedBuffer // SyncedBuffer类型SyncedBuffer
的值就已经可以被使用了。在上面的代码中,p
和v
都可以直接被使用。
构造函数和复合字面量
有时候,零值并不能满足我们的需求,此时,就需要一个初始化构造函数了。 1
2
3
4
5
6
7
8
9func NewPersion(name String, age int) *Persion {
if age < 0 {
return nil
}
p := new(Persion)
p.name = name
p.age = age
return p
}1
2
3
4
5
6
7
8func NewPersion(name String, age int) *Persion {
if age < 0 {
return nil
}
// 如果不指定字段名,那么复合字面量中,字段的值的顺序都要按照所定义类型中字段的顺序,并且每个字段都要有
// 使用下面这种键值对的形式,顺序任意,缺失的值会使用其零值。
return &Persion{name: name, age: age}
}
复合字面量也适用于数组、切片和map,此时,视情况而定,字段标签是切片或者map键。例如: 1
2
3
4
5
6
7
8
9
10
11
12const (
Enone = 1
Eio = 3
Einval = 5
)
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// 此时,a的大小为6,值为["", no error", "", "Eio", "", "invalid argument"]
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// 此时,s的大小为6,值为["", no error", "", "Eio", "", "invalid argument"]
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
// 此时,m的大小为6,值为{5:"invalid argument", 1:"no error", 3:"Eio"}
用make(T, args)
进行分配
与new
不同,内置函数make
只创建切片、map和channel,返回一个类型为T(不是*T)的已初始化(非零值)的值。之所以有这样的差别是因为,这三种类型,内部指向在使用前必须被初始化的数据结构。以切片为例,一个切片是一个包含三个项(一个指向数据数组内部数据的指针、长度、容量)的描述器,在这三个项被初始化之前,切片都为nil
。对于切片、map和channel,make
初始化内部数据结构,准备要使用的值。例如: 1
2
3
4
5
6make([]int, 10, 100)
// 分配一个包含100个int的数组,
// 然后创建一个长度为10,容量为100,指向该数组头10个元素的切片结构
// 作为对比
new([]int)
// 返回一个指向新分配的清零切片结构的指针,即,指向一个nil切片值的指针make
只对map、切片和channel有用,并且不返回指针! 2. make
只对map、切片和channel有用,并且不返回指针! 3. make
只对map、切片和channel有用,并且不返回指针!
如果实在需要指针,那么使用
new
或者直接取值&p
数组
与C不同,在Go中: * 数组是值,将一个数组赋给另一个会拷贝所有的元素 * 特别是,如果你把一个数组传递给一个函数,该函数会收到该数组的一个拷贝,而不是指向这个数组的指针 1
2
3
4
5
6
7
8 func ChangeArray(a [3]int) {
a[1] *= 10
}
a := [3]int{1,2,3}
fmt.Println(a) // [1 2 3]
ChangeArray(a)
fmt.Println(a) // [1 2 3][10]int
和[20]int
是不同的。
切片
切片保存到底层数组的引用: * 如果将一个切片赋值给另一个,那么,这两个切片都会指向同一个数组。 * 如果将一个切片传递给一个函数,那么在函数内对此切片所做的修改对函数的调用者来说是可见的。这类似于传递一个指向底层数组的指针 1
2
3
4
5
6
7
8 func ChangeArray(a []int) {
a[1] *= 10
}
a := [3]int{1,2,3}
fmt.Println(a) // [1 2 3]
ChangeArray(a[0:2])
fmt.Println(a) // [1 20 3]cap
获得,代表该切片的最大长度。当给切片附加数据的时候,如果数据超过容量,那么会重新分配切片,并返回结果切片。 * 对于nil
切片,len
和cap
函数是合法的,会返回0
二维切片/二维数组
Go的数组和切片都是一维的,如果需要创建二维数组/二维切片,可以如下定义: 1
2
3
4
5
6
7
8type Transform [3][3]float64 // 一个3x3数组,即,一个元素为数组的数组
type LinesOfText [][]byte // 一个元素为byte切片的切片
text := LinesOfTeXT {
[]byte("Now is the time"),
[]byte("for all good gophers"),
[]byte("to bring some fun to the party"),
}
// 由于切片长度可变,因此,对于二维切片而言,每个元素的长度都是可变的。1
2
3
4
5
6// 分配顶层的切片
picture := make([][]uint8, YSize)
// 遍历每一行,为每一行分配切片空间
for i := range picture {
picture[i] = make([]uint8, XSize)
}1
2
3
4
5
6
7
8// 分配顶层的切片
picture := make([][]uint8, YSize)
// 分配一个大大的切片来保存所有的像素
pixels := make([]uint8, XSize*YSize)
// 遍历每一行,从剩余的pixels切片的前面给每一行分配切片空间
for i := range picture{
picture[i], pixels = pixels[:XSize], pixels[XSize:]
}
Map
map的键可以是任意定义了相等算子的类型,例如整型、浮点数、复数、字符串、指针、接口(只要该动态类型支持相等)、结构和数组。而切片则不能作为键。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 声明以及初始化
var timeZone = map[string]int {
"UTC": 0*60*60,
"EST": -5*60*60,
"CST": -6*60*60,
"MST": -7*60*60,
"PST": -8*60*60,
}
// 取值
offset1 := timeZone["EST"]
offset2, ok := timeZone["DST"]
// 可以使用ok来判断某个键是否存在于map中。
// 尝试获取一个键不存在的值将会返回map中值的类型的零值
// 删除map中的某一项。即使某个项不存在,也不会发生异常。
delete(timeZone, "PDT")
另外,可以利用一个值类型为bool
的map来实现集合。
打印
fmt
包的Print家族有三类成员: * Printf
/ Fprintf
/ Sprintf
:类似C,接受一个格式化字符串作为第一个参数。 * Println
/ Fprintln
/ Sprintln
:无需使用格式化字符串,而是会为其每个参数生成一个默认的格式。另外,还会在每个参数之间插入一个空格,然后在输出后附加一个新行。 * Print
/ Fprint
/ Sprint
:和Println
版本一样,无需使用格式化字符串。这种版本只会在参数两边都没有字符串的情况下插入空格。 > Fprint
系列的函数的第一个参数为任何实现了io.Writer
接口的对象,例如os.Stout
和os.Stderr
好了,下面来点跟C不同的东西: * 例如%d
这样的数值格式符不会使用用于符号或者大小的标志。使用参数类型来决定符号或者大小这些属性。 1
2
3var x uint64 = 1<<64 - 1
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
// 输出:18446744073709551615 ffffffffffffffff; -1 -1%v
(表示“value”,可以打印任何值,甚至是数组、切片、结构和map) * 当打印一个结构的时候,%+v
会用结构的字段名来注释结构的每个字段。而对于任意值,%#v
以完整的Go语法打印其值 1
2
3
4
5
6
7
8
9
10
11
12
13
14type T struct {
a int
b float64
c string
}
t := &T{7, -2.35, "abc\tdef"}
fmt.Printf("%v\n", t)
// &{7 -2.35 abc def}
fmt.Printf("%+v\n", t)
// &{a:7 b:-2.35 c:abc def}
fmt.Printf("%#v\n", t)
// &main.T{a:7, b:-2.35, c:"abc\tdef"}
fmt.Printf("%#v\n", timeZone)
// map[string]int{"PST":-28800, "UTC":0, "EST":-18000, "CST":-21600, "MST":-25200}string
或者[]byte
的值使用%q
获得。%#q
则在有可能的情况下使用反引号。(%q
格式也可以应用到整形和rune
,生成一个单引号rune
常量)。另外,%x
也对字符串、byte数组、byte切片以及整形有效,生成一个长长地十六进制字符串。而% x
则会在字节之间放置空格。 1
2
3
4fmt.Printf("%q, %#q; %q, %#q\n", "ele", "ele", 123, 123)
// "ele", `ele`; '{', '{'
fmt.Printf("%x, % x; %x, % x\n", "ele", "ele", 123, 123)
// 656c65, 65 6c 65; 7b, 7b%T
:打印一个值的类型 1
2fmt.Printf("%T\n", timeZone)
// map[string]intString() string
函数签名的方法。例如: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func (t *T) String() string {
return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
/* 这里可以调用Sprintf是因为print程序是完全重入的,可以这样封装。但是,关于此方法有一个要理解的重要细节:构造String时,千万不要让Sprintf方法再次调用你的String。这样会让你的String方法陷入无限递归。就像这样:
type MyString string
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", m)
}
上面的无限递归很容易更正:将参数转换成一个基本的string类型
func (m MyString) String() string {
return fmt.Sprintf("MyString=%s", string(m))
}
*/
}
fmt.Printf("%v\n", t)
// 7/-2.35/"abc\tdef"T
的值以及指向它的指针,那么String()
的接受者就必须是值类型(例如,使用func (t T) String() string
)。上面这个例子用了指针是因为,对于结构类型,这样更有效更理想。
> 另外,`Sprintf`只会在它需要一个字符串的时候才会调用`String`方法。
- 另一个打印技术是,直接将一个print程序的参数传递给另一个这样的程序。
Printf
的签名使用类型...interface{}
作为其最后一个参数,来指定在格式后面可以出现任意数量(任意类型)的参数:> 一个1
2
3
4
5
6
7// func Printf(format string, v ...interface{})(n int, err error)
// 其中,v就像一个类型为[]interface{}的变量,但是,如果它被传递给另外一个可变参数函数时,它就像一个普通的参数列表。
func Println(v ...interface{}) {
std.Output(2, fmt.Sprintln(v...))
// Output接受参数 (int, string)
// 我们在v后面写上...是为了告诉编译器将v当成一个参数列表,否则,会将v作为单个切片变量进行传递
}...
参数可以是一个指定的类型,例如,...int
func append(slice []T, elements ...T) []T
现在,该是说说内置函数append
的时候了。在其函数定义中,T是用于任意给定类型的占位符。在Go中,你实际上是无法编写一个类型T由调用者决定的函数的,这就是为什么append
是内置函数:它需要来自编译器的支持。
append
函数会将元素elements
附加到切片尾部,然后返回结果。需要返回结果是因为,底层的数组可能会发生改变。 1
2
3
4x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
fmt.Println(x)
// 输出:[1 2 3 4 5 6]1
2
3
4
5x := []int{1, 2, 3}
y := []int{4, 5, 6}
x = append(x, y...)
fmt.Println(x)
// 输出:[1 2 3 4 5 6]...
的话,会发生编译错误,因为类型不对。
初始化
常量
在编译期间创建(即使它们是定义在函数中的局部常量),只可以是数值、字符(rune),字符串或者布尔类型。由于编译时限制,定义敞亮的表达式也必须时常量表达式,可以由编译器计算得出。例如,1 << 3
是常量表达式,而math.Sin(math.Pi/4)
不是,因为对math.Sin
的函数调用是需要在运行时才发生的。
在Go中,枚举常量是通过iota
枚举符来创建的。由于iota
可以是表达式的一部分,而表达式是可以被隐式重复的,因此,很容易构造值的复杂集: 1
2
3
4
5
6
7
8
9
10
11
12
13type ByteSize float64
const (
_ = iota // 通过赋给一个空白标识符来忽略第一个值
KB ByteSize = 1 << (10 * iota) // 这里,iota得到的值是1
MB // 这里,iota得到的值是2,也就是说,MB的值是通过1 << (10 * iota) = 1 << (10 * 2)计算得到的。下面以此类推。
GB
TB
PB
EB
ZB
YB
)
init
函数
每一个源代码文件都可以定义它自己的init
函数来设置所需的状态(实际上,每个文件都可以有多个init
函数)。
在包中所有的变量声明都已经计算出它们的初始值后,才会调用init
函数,而变量声明的初始值计算则发生在所有导入的包完成初始化之后。示意图如下:
init
函数除了用于初始化那些不能通过声明来表达的东西外,一个常用的用途是,在真正的执行开始之前,验证/修复程序状态的正确性。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15func init() {
// 验证
if user == "" {
log.Fatal("$USER not set")
}
// 修复参数值
if home == "" {
home = "/home/" + user
}
if gopath == "" {
gopath = home + "/go"
}
// gopath可能会被命令行的--gopath标记所覆盖。
flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")
}