构建高效率的 Go 程序 (Effective Go)
本文是 「Golang.org」的原文档翻译,出于本人有限的英语水平,如有不当,还请指正。
Go 是一个全新的语言,当然,它也从很多以存在多年的优秀语言中吸取了很多优秀的特性,但是因为其自身的特殊性,使得 Go 程序与其它相关的语言所开发的出来的程序有很大的不同,直接将C++或者Java程序翻译为Go程序并不会让你得到一个满意的结果——Java程序永远都只是Java程序,它成不了Go程序。另一方面,从Go的角度去想一个问题,你会得到一个可以成功解决它但是和其它语言完全不一样的方式。换句话说,想写出漂亮的Go程序,你必需要了解它的特性和风格,甚至是已有的约定,比如命名规范、代码格式化结构等,了解这些,可以让你的程序更简单地其它人看懂。
本文档给出了一些能让你写出更加干净的符合Go语言习惯的代码,但是建议你在阅读本文之前先阅读一下Language Specification、Tour of Go以及「如何写Go程序(How to Write Go Code)」.
示例
Go Package Sources 并不只包含了它的核心库,同时它还包含了很多使用该语言写的示例程序,如果你有关于如何使用Go解决问题或者在Go应该如何实现的相关问题,这些示例代码或话会给你答案,它还能告诉你,你应该将自己的想法以怎样的一种方式转换成Go程序。
格式化
格式化应该是最具有争议但是也是最重要的一个Go语言的特性,每一个开发者可以有属性自己的书写格式,但是所有人都使用同一种格式看起来肯定是最好的,这使得我们不再需要花费太多时间放在了解别人的书写风格上。
在Go中,不同于其它语言,它让计算机程序来管理着代码的格式化,gofmt 命令(也可以使用 go fmt)可以读取Go代码文件,并将它们以标准格式对其进行格式化,包括它们的缩进、空格等,请看下面这个示例:
type T struct {
name string // name of the object
value int // its value
}
gofmt 会将其格式化为:
type T struct {
name string // name of the object
value int // its value
}
所有Go标准库中的代码都已经通过gofmt进行了格式化。
比如下面这些Go中最常见的会使用到的格式化约定:
- 缩进:Go使用制表符(Tabs)进行缩进,而不是很多语言中都推荐的空格,gofmt默认会使用制表符进行缩进,你应该仅仅在需要空格的地使用空格。
- 行长度:Go没有行代度限制,所以,你完全不用担心这个问题,但是如果你感觉某一个行实在是太长了,那你可以将其截断,然后使用一些制表符让它看起来和它原本应该属于的那一行开始于同一个地方。
- 括号:Go只有很少的一些地方需要括号,结构控制(比如if, for, switch等的条件判断)都是不需要括号的,操作符的优先级层次更短更清晰,所以,x << 1 + y < 6,就可以很明白的知道这是什么了。
注释
Go同时对G风格的块注释 /* ......*/ 和 C++风格的行注释 // .....提供支持,行注释是标准的,而块注释一般只建议使用的对包的注释上或者使用在需要禁用(但不是删除)一大段代码的功能上。
godoc程序(同时也是文档服务器)会从Go代码中提供包的说明文档,Comments that appear before top-level declarations, with no intervening newlines, are extracted alog with the declaration to serve as explanatory text for the item. The nature and style of these comments determines the quality of the documentation godoc produces.
任何一个包都应该包含一个包注释——一个用来说明包功能以及使用方法等信息的块注释,对于被分割为多个文件的包,包注释只需要出现在某中的任何一个文件中即可,godoc会将它作为所有同一个包的说明文档。The package comment should introduce the package and provide information relevant to the package as a whole. It will appear first on the godocpage and should set up the detailed documentation that follows.
/*
Package regexp implements a simple library for
regular expressions.
The syntax of the regular expressions accepted is:
regecxp:
concatenation { '|' concatenation }
concatenation:
{ closure }
closure:
term [ '*' | '+' | '?' ]
term:
'^'
'$'
'.'
character
'[' [ '^' ] character-ranges ']'
'(' regexp ')'
*/
package regexp
如果某个包是一个十分简单的包,那么这个包的注释可以很简单:
// Package path implements utility routines for
// manipulating slash-separated filename paths.
注释不需要进行特殊的格式化,文档创建工作不能确定使用的是否是固定宽度的字体,所以,你不需要使用空格去对注释进行格式化——godoc像gofmt一样,它会帮助你完成这些格式化。注释应该仅仅保是纯文本的字符串,所以,HTML或者其它的标记方式(比如 THIS)都是不应该被使用的,godoc只会照字面的意思去解决,所以,请尽可能让你的注释内容就是你想表达的内容的文本——使用正确的拼写,合理的分段,长句进行合理的截段等。
在包内,任何一个仅跟着一个声明的注释都被当作该声明的注释文档,任何一个可导出的(大写开头的命名)的声明都应该具有一个说明注释,而这个注释文档最好也有遵循一定的约定,它的最第一句话应该是一个以该声明名称开始的用来概括该声明作用的句子。
// Compile parses a regular expression and returns, if successful, a Regexp
// object that can be used to match against text.
func Compile(str string) (regexp *Regexp, err error) {...}
Go 的声明语法允许分组声明,这使得一个单一的文档注释可以说明一组相关的常量或者变量,整个声明可以像下面这样来做(这是懒人做法啊):
// Error codes returned by failures to parse an expression.
var (
ErrInternal = errors.New("regexp: internal error")
ErrUnmatchedLpar= errors.New("regexp: unmatched '('")
ErrUnmatchedRpar= errors.New("regexp: unmatched ')'")
...
)
即使是私有名称,分组也是可以指明它们之间的关系的,比如下面这一组私有变量由一个互斥锁保护:
var (
countLock sync.Mutex
inputCount unit32
outputCount unit32
errorCount unit32
)
名称
与其它语言一样,名称在Go中也是十分重要的,在某些环境下,一个名称甚至还需要其特有的语义——对于一个实例,它的方法或变量是否能被外部调用和访问取决于那个方法或变量的名称是否以大写字母开始,PublicItem这样的就是可以被外部访问的,而 privateItem则只能放在家里自己使用,也就是说:大写开始表示可以被外部访问,也就是可以被导出,而小写开头则只能是私有的。
包名称
当一个包被导入之后,包名称就成为一个访问该包资源的入口,比如,当我们:
import "bytes"
之后,我们就可以请求 bytes.Buffer了,一个简洁、明了令人回味的包名称可以帮助他人在使用该包的过程中更加方便快乐。我们约定包名称应该是一个“仅仅小写字母的单个字汇字符串”,我们不必要使用下划线或者驼峰式的写出一个完整的长的名称,你要想想,假如你自己在使用别人的某一个包时,需要像fooBarFooBar.FooBar这样请求它的资源,你会是一种什么样的感觉,我们只需要简单的foo.Bar即可。你不必要担心你所选择包名可能与其它已有的包名相同,因为Go允许在导入一个包时为其指定一个本地使用的别名。
另一个约定是“包名是该访问该包所有资源的基础入口”,比如 src/pkg/encoding/base64 包应该以encoding/base64导入,它的名称为 base64,而不是 encoding_base64或者encodingBase64。
包导入工具将使用名称来引用它的内容(import . 只在某些特殊用途的时候使用,如非必要,请尽可能的不去使用),所以,包中可导出的内容就可以以最直接方式访问到,对于实例,如bufio包中的Reader,就可以直接使用 bufio.Reader访问到,而不是 BufReader或者其它的名称,因为用户看前者会更直观。由于导入的实体总是以其完整的位置访问,所以 bufio.Reader不会与 io.Reader 混淆。相似的,用来创建新的 ring.Ring实例的构建函数,通常我们可能会将其命名为 NewRing,但是,因为 Ring 是 ring 导出的唯一一个类型,所以,我们也只能创建ring.Ring 这一种类型的数据,所以,直接将该构建函数取名为 New即可。
另一个*简化名称*的示例是 once.Do,我们不将其命名为 once.DoOrWaitUntilDown,长的名称在绝大多数时候并不会让我们更容易理解其所指代的意思,要让使用者更方便的调用也让他们更好的了解其使用方法或者意思,那好的办法是选择简洁的名称,并为其写上足够说明其意思和使用方法的注释。
Getters
Go 不会自动的提供 getters 和 setters 函数,但是这种事情你完全可以自己去完成,但是你也应该在适当的时候这么做,但是,你不需要把 Get 写进你所定义的方法名称中去,比如你现在有一个字段 owner(注意这是小写字母开头的,所以不导出,外部也部无法直接调用),它的 getter 名称应该称之为 Owner而不是GetOwner,它成为了获取 owner 字段数据的钩子,而 setter 函数则应该称之为 SetOwner:
owner := obj.Owner()
if owner != user {
obj.SetOwner(user)
}
接口名称
Go 约定接口名称为方法名称后面加上 er:Reader、Writer、Formatter等,Go中有许多这样名称的标准接口,也有很多方法去实现了这些接口,它们这些方法的名称都是像 Read、Write、Close、Flush、String等这样的,为了避免造成混乱,在你自己的程序中尽可能的不要去使用上面这些标准库中提供了的方法或者接口名称。
相反的,如果你的程序实现了一个与某一个已经被另一个大家所熟知的程序实现过的接口,那么你最好是使用该程序所使用的方法名称,一个将你的数据转换为字符的方法,不要叫作ToString,而应该直接叫作 String。
多词汇名称
最后,Go约定使用“驼峰式”的命名,而不使用下划线的方式,比如MixedCaps或者mixedCaps而不是mixed_caps。
分号
与 C 一样,Go使用分号“;”来终止一条语句,而与C不一样的是这些分号一般是不会出现在的源代码中的,Go使用一种简单的规则在其扫描代码的过程中合适的插入这些分号,所以,你在写代码的时候完全不需要将其明显的写入进源代码中。
Go所约定的规则是这样的:如果一个新行的最后面是一个类型识别码(一般都是 int 或者 float64这样的),一个常量(比如数字或者字符串)或者下面这些关键字或者符号中的任何一个:
break continue fallthrough return ++ -- ) }
Go 会自动在其后面加了分号,上面所说的这些可以简单的描述为:“如果一个新行前面的Token可以终止一个声明,则在该Token后插入分号”。
结束大括号前的分号也是可以省略的,比如:
go func() { for { dst <- <-src } }()
就不需要分号,Go程序只需要在诸如循环、分隔初始值、条件或者延续元素的情况,才需要明显地使用分号,当然,你可以将多行地声明写进同一行中,这个时候每一个声明后就需要使用分号来区分。
有一点你需要特别注意的,你不应该把控制结构(if、for、switch或者select)的左大括号写在下一行,如果你这么做了,那么Go会在这些声明后面加上一个分号,这肯定不是你想看到的。
if i < f() {
g()
}
而下面这样就是错误的:
if i < f() // wrong!
{
go () // wrong!
}
控制结构
Go 的控制结构与 C 十分类似,但是有一个重要的不同在于Go没有 do 或者 while 循环,仅仅使用 for 来做这些基本工作,switch 有更多的灵活性以及if和switch可以接受一个初始值声明,它还有一个新的控制结构 select,语法也略有不同:没有括号,主体必须分隔。
if
在Go中一个简单的if语句看起来像下面这样的:
if x > 0 {
return y
}
强制性的要求 if 声明必须被分隔为多行,由于是在它的主体中包含有如 return或者break等控制语句的情况更为适用。
由于 if 和 switch 支持初始化声明,所以,我们可以在其条件判断前为该条 if 语句初始化一个本地变量:
if err := file.Chmod(0664); err != nil {
log.Print(err)
return err
}
在Go标准库中,如果一个 if 语句不接会导致程序继续执行其下一条语句时,我们可以不需要使用 else ,这通常的都是在if主体中存在 return、break、continue或者goto等等这些能控制代码结构的关键字。
f, err := os.Open(name)
if err != nil {
return err
}
d, err := f.Stat()
if err != nil {
f.close()
return err
}
codeUsing(f)
在上面的代码中,我们首先打开一个文件,如果没有错误存在,程序会继续往下执行,如果再没有错误,则会执行 codeUsing(f)函数,但是如果有错误出现,那么在if执行之后就会跳出整个程序。
声明重载(Redeclaration)
在上面的示例中,你应该看到,我们的一个操作符 := 将 os.Open 返回的值同时声明给了两人个变量:f和err:
f, err := os.Open(name)
接着:
d, err := f.Stat()
看到,err 在前面声明之后,我们接着又重新声明了它,这表明,在第一个声明中,创建了err并为其赋了值,而在后面一次声明中,仅仅只是为其赋了新值。
在一个为变量 v 赋值的 := 的声明中,那么:
- 如果在该声明同样的作用域内该声明之前已经存在了对该变量的声明,那么本声明直接为其赋值
- 没有出现上面的情况,本声明将先创建该变量之后再为其赋值
for
Go的for循环有点像C的for循环——但是是不一样的,它有三种不同的格式:
// Like a C for
for init; condition; post { }
// Like a C while
for condition { }
// Like a C for(;;)
for { }
更短的声明让我们更容易给循环一个索引变量:
sum := 0
for i := o; i < 10; i+ {
sum += 1
}
如果你在遍历一个数组、片(Slice)、字符串或者Map,或者从一个 channel 中读取数据,range 可以被用来控制该循环:
for key, value := range oldMap {
newMap[key] = value
}
如果你仅仅只需要循环过程中 range 产生的第一个值(range 产生两人个值 key, value),那么直接丢掉第二值即可:
for key := range m {
if expired(k) {
delete(m, key)
}
}
但是如果你仅仅只想要其产生的第二个值,那么需要使用一个“空定义(blank identifier)”作为占位符,它是一个下划线:“_”:
sum := 0
for _, value := range array {
sum += value
}
这是因为,如果你没有那个占位符,那么 range 还是会首先把它产生的第一个值赋值给 value,而有了那个点位符之后,range将key赋值给,之后会直接把该值丢掉,然后range就赋值其第二值 value给value了。
对于字符串,range能做更多的事情,breaking out individual Unicode characters by parsing the UTF-8. Erroneous encodings consume one byte and produce the replacement rune U+FFFD. The loop
func main() {
for pos, char := range "编程开发" {
fmt.Printf(" aracter %c starts at byte position %dn", char, pos)
}
}
将会打印出:
aracter 编 starts at byte position 0
aracter 程 starts at byte position 3
aracter 开 starts at byte position 6
aracter 发 starts at byte position 9
最后,Go没有逗号操作符,而 ++ 与 -- 是声明而不是表达式,如果你想在一个for循环中使用多个变量,你需要使用 Parallel Assignment。
// Reverse a
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i]
}
switch
Go 中的 switch 比C中的更加通用,它的表达式并不局限在常量或者数字中,它的 case 是从上到下进行匹配,直到找到一个为 true 的 case,如果你的 if-else-if-else-... 有点儿太多了,那么就使用 switch吧。
func unhex(c byte) byte {
switch {
case '0' <= c && c <= '9':
return c - '0'
case 'a' <= c && c <= 'f':
return c - 'a' + 10
case 'A' <= c && c <= 'F':
return c - 'A' + 10
}
` return 0
Go 中没有自动向下执行(Fall Through)机制,所以,你不能写成这个样子:
case i < 0:
case i > 10:
doSomethingWithI(i)
但是Go可以将多个条件使用逗号分开,所以上面这样在其它语言里面常见的写法在Go你应该写成:
case i < 0, i > 10:
doSomethingWithI(i)
Switch还可以被用来发现某一个接口变量的动态类型,像下面这样的去实现:
switch t := interfaceValue.(type) {
default:
fmt.Printf("unexpected type %T", t)
case bool:
fmt.Printf("boolean %tn", t)
case int:
fmt.Printf("integer %dn", t)
case *bool:
fmt.Printf("pointer to boolean %tn", *t)
case *int:
fmt.Printf("pointer to integer %dn", *t)
}
函数(Functions)
多值返回 (Multiple return values)
Go函数和其它语言最不一样的特性就是它可以在一个函数中同时返回多个值,这个特性能改进很多在C里面要通过各种各样的特殊办法实现的功能,而这个特性你在本文档前面也已经见到过了:
f, err := os.Open(name)
同样的,在os包中还定义的很多比如 Write方法:
func (file *File) Write(b []byte) (n int, err error)(
就像它的文档所说的,该方法返回写入的总字节数,并且当 n != len(b) 的时候返回一个 non-nil 的 error,这种写法在Go很通用,你可以在专门的章节中看到更多的关于 error 处理的示例。
类似的这种方法省却了一个指针的使用,而且它看起来更加符合我们的普通常人的理解(需要返回什么东西就直接返回就是),下面这个示例演示了一个能同时返回两个值的函数,它先从一个字节数组(byte array)中找到一个数字,然后返回下一个为数字的元素的位置:
func nextInt(b [byte], i int) (int, int) {
x := 0
for ; i < len(b) && isDigit(b[i]); i++ {
x = x*10 + int(b[i]) - '0'
}
return x, i
}
然后你可以像下面这样的去调用它:
for i := 0; i < len(a); {
x, i = nextInt(a, i)
fmt.Println(x)
}
带名称的结果参数
Go 函数中的返回结果参数可以被赋予一个名称,它可以像其它的变量一样的在函数中使用,命名之后,当函数执行时,它会被赋予一个其所属的数据类型的0值,而当函数执行一个 return声明之后(不需要带任何参数的声明),该命名了的返回参数当前的值就会被返回。
Go 不强制你必须为返回参数命名,但是这样做可以使你的代码更短——它们被文档化了。如果我们将上面的函数进行命名,那么:
func nextInt(b []byte, pos int) ( value, nextPos int) { ... }
因为命名了的返回结果会在函数执行时被初始化了,并且和一个未经修饰的 return 绑定,所以它能使唤代码更清晰,下面这是一个 io.ReadFull的一个版本:
func ReadFull(r Reader, buf []byte) ( n int, err error) {
for len(buf) > 0 && err == nil {
var nr int
nr, err = r.Read(buf)
n += nr
buf = buf[nr:]
}
return
}
延迟(Defer)
Go的defer声明一个定时运行的函数呼叫(deferred function),这样的函数会在函数终止前立马执行,这种方式在其它方法里面不太常见,但是它却非常有用,比如我们有一个函数要打开一个文件,但是不管这个函数是否执行成功,我们都应该在不再需要这个文件的时候关闭这个文件,这个时候我们就可以在成功打开文件之后,立马定时关闭该文件,当函数再往下执行时,不管是出现什么错误或者是成功执行,文件都会被正确关闭。
// Contents returns the file's contents as a string.
func Contents(filename string) (string, error) {
f, err := os.Open(filename)
if err != nil {
return "", err
}
defer f.close() // f.close will run when we're finished.
var result []byte
buf := make([]byte, 100)
for {
n, err := f.Read(buf[0:])
result = append(result, buf[0:n]...) // append is discussed later
if err != nil {
if err == io.EOF {
break
}
return "", err // f will be closed if we return here
}
}
return string(result), nil // f will be closed if we return here
}
延迟请求一个函数的执行至少有两人个好处:首先,它能让你永远不会因为某些错误(这实在是太容易发生了)而关闭文件;其次,关闭与打开文件的代码可以被写在一起,这样后面的代码完全可以放开安排自己的代码结构了(肯定啊,不需要在每一个 return 前面关闭一次文件了,我在任何一个地方可以 return ,文件会自动的关闭)。
传递给被延迟请求的函数的参数是在 defer 执行是就计算出来的,而不是函数执行时才计算的,所以像下面这段代码运行的结果将是 10 0,而不是10 10:
func forDefer() {
i := 0
defer fmt.Printf("%d ", i)
i = 10
fmt.Printf("%d ", i)
}
同时它还遵循LIFO(Last Input First Output:先进后出)所以,当defer执行之后,延迟函数所调用的参数不管再怎么改变,都不会影响延迟函数本身的输出,所以像下面这段你代码,它最终执行后的结果将不是 0 1 2 3 4,而是4 3 2 1 0:
for i := 0; i < 5; i++ {
defer fmt.Printf("%d ", i)
}
延迟函数执行还时常被用在跟踪函数本身的执行上,比如下面的示例:
func trace(s string) { fmt.Println("entering: ", s) }
func untrace(s string) { fmt.Println("leaving: ", s) }
// Use them like this:
func a() {
trace("a")
defer untrace("a")
// do something you like as normal...
}
对于这个跟踪函数,我们还可以做得更简洁高效,还记得我们在前面了解到的关于延迟函数的参数的值是在defer执行时就被计算的吗?我们在这里来使用这一特性重新完成前面的那个函数运行跟踪函数:
func trace(s string) string{
fmt.Println("entering: ", s)
return s
}
func un(s string) {
fmt.Println("leaving: ", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
def un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
上面代码的输出结果是:
entering: b
in b
entering: a
in a
leaving: a
leaving: b
对于习惯于以块为单位管理代码的其它语言的开发者来说,defer可以看起来很奇怪,但是它功能的功能及实用性是可以肯定的,而且是十分有趣的,它使得代码不在以块为单位而是以功能来进行组织管理的。
数据
通过 new 分配内存
Go内置了两人函数 make 和 new,它们做的事情是不一样的,而且绑定了不同的类型,这很容易让人混淆,但实际上它的规则是很简单的。让我们先来谈谈 new,它是一个用来分配内存的内置函数,但是它不像其它语言中的 new 函数,它不初始化内存, new(T) 只是创建一个没有任何数据的类型为T的实例,并且返回它的地址——*T类型的值,在Go的术语中,它返回一个新建的分配了0空间的类型为T的实例的指针。
由于new返回的内存是归零了的,所以它能让我们很方便的设计它的数据结构,因为归零的数据可以不需要更多的初始货即可直接使用,比如 bytes.Buffer的文档这么写着“Buffer的归零数据是一个空的可以随便人到中年来使用的缓冲”。相反的,sync.Mutex没有明确的构建或者初始化方法,转而它的归零值为一个未锁定的mutex。
The zero-value-is-useful property works transitively. Consider this type declaration.
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
SyncedBuffer类型的值在其分配了或者仅仅只是声明了之后都是可以仍的,下面这段代码不需要更多的安排即可以正确的工作:
p := new(SyncedBuffer) // type *Synceduffer
var v SyncedBuffer // type SyncedBuffer
构造函数与复合声明(Constructors and composite literals)
很多时候一个归零的值并不足以满足程序的业务需求,这时一个初始化构造函数就成了必须的东西了,比如下面这段从 os 衍生的函数:
func NewFile(fd ind, 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}
return &f
}
注意:不像 C,返回本地变量的地址在Go中是完全可以的,当函数返回之后,本地变量地址所占用的存储空间同样的会生存下来,事实上,我们可以直接可以将上面函数中的最后两人行合并为一行:
return &File(fd, name, nil, 0}
上面这些初始化成员必须按其构造函数定义了的顺序赋值,这是很麻烦的一件事情,我们必须要一直记得它们是一个什么样的顺序,即使有100个,我们也必须记住这100个的顺序,好在我们可以使用一种更加合理且方便的方法来使用它,那就是为每一个初始化成员指定一个标签,其格式为field: value:
return &File(fd: fd, name: name}
如果初始化成员没有任何元素的话,那么就会创建一个归零的实例,这个时候,&File{}与new(File)是等价的。
复合声明同样可以被用来创建array、slice、map,通过指定适当的索引和map键来标识字段,在下面这个例子中,无论是Enone、Eio还是Einval初始化都能很好的工作,只要确保他们不同就好了。
a := [...]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string {Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
用 make 分配内存
回到内存分配。内奸函数 make(T, args) 与 new(T) 有着不同的功能。它只能创建 slice、map和channel,并且返回一个有初始值(非零)的T类型的实例,而不是 *T。从本质上讲,导致这三个类型有所不同的原因在于指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量这三项的描述符,在这些项目被初始化之前,slice为nil,对于slice、map和channel,make初始化了内部的数据结构,填充适应的值。
make 返回初始化后的(非零)值
例如,*make([]int, 10, 100)分配了100个整数的数组,然后用长度10和容易100创建了slice结构指向数组的前10个元素,区别是new([]int)返回指向新分配的内存的指针,而零值填充的slice鸨是指向nil的slice值。
下面这个例子展示这两人者之间的不同:
var p *[]int = new([]int) // 分配slice结构内存 : *p = nil
// 已经可用
var v []int = make([]int, 100) // v 指向一个新分配的有 100 个整数的数组
var p *[]int = new([]int) // 不必要的复杂的例子
*p = make([]int, 10, 100)
v := make([]int, 100) // 更常见
务必赢得 make 仅适用于 map、slice和channel,并且返回的不是指针,应当用new来获得特定的指针。
评论已关闭