好好学习,天天向上

Go, go|小白的go小抄:数据和初始化

数据

new(T)进行分配

new是一个内置函数,用来为变量分配内存。但和其他语言中的new不一样,该方法并不会初始化所分配的内存,而只是清空它。也就是说,new(T)为一个类型为T的项分配已清零的内存,然后返回该内存地址(一个类型为*T的值)。用Go的话来讲,它会返回一个指向一个新分配的类型为T的零值的指针。

利用new返回的内存使用类型零值的特点,自定义的数据结构在调用new之后,其字段就可以直接使用而不需要进一步的初始化了。例如对于下面的类型声明:

1
2
3
4
5
6
type SyncedBuffer struct {
lock sync.Mutext
buffer bytes.Buffer
}
p := new(SyncedBuffer) // *SyncedBuffer类型
var v SyncedBuffer // SyncedBuffer类型
一旦分配或者仅仅声明,类型SyncedBuffer的值就已经可以被使用了。在上面的代码中,pv都可以直接被使用。

构造函数和复合字面量

有时候,零值并不能满足我们的需求,此时,就需要一个初始化构造函数了。

1
2
3
4
5
6
7
8
9
func 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
8
func NewPersion(name String, age int) *Persion {
if age < 0 {
return nil
}
// 如果不指定字段名,那么复合字面量中,字段的值的顺序都要按照所定义类型中字段的顺序,并且每个字段都要有
// 使用下面这种键值对的形式,顺序任意,缺失的值会使用其零值。
return &Persion{name: name, age: age}
}
> 注意:不像C,返回一个本地变量的地址是完全没问题的。在函数返回之后,为该变量所分配的存储仍然存活。

复合字面量也适用于数组、切片和map,此时,视情况而定,字段标签是切片或者map键。例如:

1
2
3
4
5
6
7
8
9
10
11
12
const (
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
6
make([]int, 10, 100)
// 分配一个包含100个int的数组,
// 然后创建一个长度为10,容量为100,指向该数组头10个元素的切片结构
// 作为对比
new([]int)
// 返回一个指向新分配的清零切片结构的指针,即,指向一个nil切片值的指针
重要的事情说三遍: 1. 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切片,lencap函数是合法的,会返回0

二维切片/二维数组

Go的数组和切片都是一维的,如果需要创建二维数组/二维切片,可以如下定义:

1
2
3
4
5
6
7
8
type 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. 为二维切片中的每个切片元素独立分配空间
1
2
3
4
5
6
// 分配顶层的切片
picture := make([][]uint8, YSize)
// 遍历每一行,为每一行分配切片空间
for i := range picture {
picture[i] = make([]uint8, XSize)
}
2. 分配单个数组,然后将各个切片指向它
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")
map也保存指向底层数据结构的引用。如果将一个map传递给一个函数,在函数体内对该map做的改动是可以被函数调用者感知到的。

另外,可以利用一个值类型为bool的map来实现集合。

打印

fmt包的Print家族有三类成员: * Printf / Fprintf / Sprintf:类似C,接受一个格式化字符串作为第一个参数。 * Println / Fprintln / Sprintln:无需使用格式化字符串,而是会为其每个参数生成一个默认的格式。另外,还会在每个参数之间插入一个空格,然后在输出后附加一个新行。 * Print / Fprint / Sprint:和Println版本一样,无需使用格式化字符串。这种版本只会在参数两边都没有字符串的情况下插入空格。 > Fprint系列的函数的第一个参数为任何实现了io.Writer接口的对象,例如os.Stoutos.Stderr

好了,下面来点跟C不同的东西: * 例如%d这样的数值格式符不会使用用于符号或者大小的标志。使用参数类型来决定符号或者大小这些属性。

1
2
3
var 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
14
type 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
4
fmt.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
2
fmt.Printf("%T\n", timeZone)
// map[string]int
* 如果想控制一个自定义类型的默认格式,那么只需要给此类型定义一个带有String() string函数签名的方法。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (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
4
x := []int{1, 2, 3}
x = append(x, 4, 5, 6)
fmt.Println(x)
// 输出:[1 2 3 4 5 6]
如果我们想要将一个切片附加到另一个切片后,可以在调用的时候使用...
1
2
3
4
5
x := []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
13
type 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
15
func 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")
}
### 参考 - Effective Go - go语言的初始化顺序,包,变量,init

请言小午吃个甜筒~~