02程序结构

一:命名html

         1:Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等全部的命名,都遵循一个简单的标识符命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面能够跟任意数量的字母、数字或下划线。注意,这里的字母是指Unicode编码字母,所以Go语言开发者能够在代码中自由地使用他们的母语。程序员

         Go中的标识符区分大小写。golang

 

         2:Go定义了若干关键字,标识符不能是这些关键字之一。Go定义的关键字以下:数组

break安全

defaultapp

funcide

interface模块化

select函数

case工具

defer

go

map

struct

chan

else

goto

package

switch

const

fallthrough

if

range

type

continue

for

import

return

var

 

Go还预约义了一些标识符,虽然定义与这些预约义标识符同样的标识符也能编译经过,但最好不这么作。预约义的标识符以下:

Types

bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr

Constants

true false iota

Zero value

nil

Functions

append cap close complex copy delete imag len make new panic print println real recover

 

3:空标识符”_”是一个占位符,在赋值操做的时候将某个值赋值给空标识符,从而达到丢弃该值的目的。

空标识符不是一个新的变量,所以将它用于”:=”操做符的时候,必须同时为至少另外一个值赋值。

 

         4:若是一个名字是在函数内部定义,那么它就只在函数内部有效。若是是在函数外部定义,那么将在当前包的全部文件中均可以访问。

名字的开头字母的大小写决定了名字在包外的可见性。若是一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名自己也是包级名字),那么它将是导出的,也就是说能够被外部的包访问,例如fmt包的Printf函数就是导出的,能够在fmt包外部访问。包自己的名字通常老是用小写字母。

 

         5:Go语言的风格是尽可能使用短小的名字。一般来讲,若是一个名字的做用域比较大,生命周期也比较长,那么用长的名字将会更有意义。

 

         6:在习惯上,Go语言程序员推荐使用 驼峰式 命名,当名字有几个单词组成的时优先使用大小写分隔,而不是优先用下划线分隔。所以,在标准库有QuoteRuneToASCII和parseRequestLine这样的函数命名。

 

二:声明

         1:Go语言主要有四种类型的声明语句:var、const、type和func,分别对应变量、常量、类型和函数实体对象的声明。

 

         2:一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。每一个源文件以包的声明语句开始,说明该源文件是属于哪一个包。包声明语句以后是import语句导入依赖的其它包,而后是包一级的类型、变量、常量、函数的声明语句,包一级的各类类型的声明语句的顺序可有可无(译注:函数内部的名字则必须先声明以后才能使用)。好比:

func main() {
    str := "hehe"
    fmt.Println(str)
    
    fmt.Println(globalInt)

    printfun()
}

func printfun() {
    fmt.Println("hello,world")
}
    
var globalInt = 3

 

         上面的例子中,printfun函数和变量globalInt 在定义以前就使用了,这是没有问题的。可是在main中,若是在str定义以前就使用它,就会报语法错误。

 

三:变量

         1:var声明语句能够建立一个特定类型的变量,而后给变量附加一个名字,而且设置变量的初始值。

变量声明的通常语法以下:“  var 变量名字 类型 = 表达式 ”。其中“类型”或“= 表达式”两个部分能够省略其中的一个。若是省略的是类型信息,那么将根据初始化表达式来推导变量的类型信息。若是初始化表达式被省略,那么将用零值初始化该变量。 

数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数)变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每一个元素或字段都是对应该类型的零值。

零值初始化机制能够确保每一个声明的变量老是有一个良好定义的值,所以在Go语言中不存在未初始化的变量。Go语言程序员应该让一些聚合类型的零值也具备意义,这样能够保证无论任何类型的变量老是有一个合理有效的零值状态。

 

2:能够在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。若是省略每一个变量的类型,将能够声明多个类型不一样的变量(类型由初始化表达式推导):

var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string

 

初始化表达式能够是字面量或任意的表达式,也能够是函数的返回值:

var f, err = os.Open(name) // os.Open returns a file and an error

 

 

3:在包级别声明的变量会在main入口函数执行前完成初始化,局部变量将在声明语句被执行到的时候完成初始化:

var globalInt = getvalue()

func getvalue() int {
    fmt.Println("this is getvalue")
    return 3
}

func main() {
    fmt.Printf("globalInt is %d\n", globalInt)
}

 

结果以下:

this is getvalue
globalInt is 3

 

 

4:在函数内部,能够以“简短变量声明”的形式声明和初始化局部变量,也就是“名字 := 表达式”的形式。变量的类型根据表达式来自动推导。

注意,这种形式只能用于局部变量,不能用于包级别变量;

同var形式声明同样,在“简短变量声明”中,可使用字面量、表达式和函数返回值来初始化变量:也能够初始化一组变量:

//i := 3

func main() {
    i := getValue()
    j := 1
k := rand.Float64() * 3.0
    m, n := 3.4, "abc"
}    

 

上面的代码中,若是把注释去掉,就会报编译错误:”syntax error: non-declaration statement outside function body”。

 

简短变量声明左边的变量不必定都是新声明的变量。若是有一些变量已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。须要注意的是,至少要有一个变量是新的,不然回报编译错误

func getValue() (int,string) {
    return 3, "abc"
}

func main() {
    i := 2
    //j := "hehe"
    i,j := getValue()
}    

 

若是去掉注释的话,回报编译错误:”no new variables on left side of :=”。

 

注意:简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操做语句等价,若是变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域从新声明一个新的变量。

 

5:普通变量在声明语句建立时被绑定到一个变量名,经过指针,咱们能够直接读或更新对应变量的值,而不须要知道该变量的名字(若是变量有名字的话)。

类型*int表示指向int类型的指针。

任何类型的指针的零值都是nil。指针之间能够进行相等测试,只有当它们指向同一个变量或所有是nil时才相等。

 

在Go语言中,返回函数中局部变量的地址也是安全的。例以下面的代码,调用f函数时建立局部变量v,在局部变量地址被返回以后依然有效,由于指针p依然引用这个变量:

func f() *int {
    v := 1
    return &v
}

func main() {
    var p = f()
    fmt.Println(p)
    fmt.Println(*p)
    
    *p = 3
    fmt.Println(p)
    fmt.Println(*p)
    
    p = f()
    fmt.Println(p)
    fmt.Println(*p)
}

 

每次调用f函数都将返回不一样的结果,上述代码结果以下:

0xc420012098
1
0xc420012098
3
0xc4200120d0
1

 

 

每次对一个变量取地址,或者复制指针,咱们都是为原变量建立了新的别名。指针特别有价值的地方在于咱们能够不用名字而访问一个变量,可是这是一把双刃剑:要找到一个变量的全部访问者并不容易,咱们必须知道变量所有的别名(译注:这是Go语言的垃圾回收器所作的工做)。不只仅是指针会建立别名,不少其余引用类型也会建立别名,例如slice、map和chan,甚至结构体、数组和接口都会建立所引用变量的别名。

 

6:另外一个建立变量的方法是调用用内建的new函数。表达式new(T)将建立一个T类型的匿名变量,初始化为T类型的零值,而后返回变量地址,返回的指针类型为*T

p := new(int)    // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2            // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2" 

7:变量的生命周期指的是在程序运行期间变量有效存在的时间间隔。对于包一级声明的变量,它们的生命周期和整个程序的运行周期是一致的。而局部变量的生命周期则是动态的:从每次建立一个新变量的声明语句开始,直到该变量再也不被引用为止,而后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候建立。

那么Go语言的自动垃圾收集器是如何知道一个变量是什么时候能够被回收的呢?基本的实现思路是,从每一个包级的变量和每一个当前运行函数的每个局部变量开始,经过指针或引用的访问路径遍历,是否能够找到该变量。若是不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

由于一个变量的有效周期只取决因而否可达,所以一个循环迭代内部的局部变量的生命周期可能超出其局部做用域。同时,局部变量可能在函数返回以后依然存在。

 

编译器会自动选择在栈上仍是在堆上分配局部变量的存储空间,但可能使人惊讶的是,这个选择并非由用var仍是new声明变量的方式决定的:

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

 

f函数里的x变量必须在堆上分配,由于它在函数退出后依然能够经过包一级的global变量找到,用Go语言的术语说,这个x局部变量从函数f中逃逸了;相反,当g函数返回时,变量*y将是不可达的,也就是说能够立刻被回收的。所以,*y并无从函数g中逃逸,编译器能够选择在栈上分配*y的存储空间,虽然这里用的是new方式。

其实在任什么时候候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量须要额外分配内存,同时对性能的优化可能会产生细微的影响。

 

Go语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并非说你彻底不用考虑内存了。你虽然不须要显式地分配和释放内存,可是要编写高效的程序你依然须要了解变量的生命周期。例如,若是将指向短生命周期对象的指针保存到具备长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

 

四:赋值

         1:Go支持复合赋值语句,好比:a += 3

         2:数值变量支持++递增和--递减语句。

注意:自增和自减是语句,而不是表达式,所以x = i++之类的表达式是错误的;自增和自减只支持后缀形式,不支持前缀。因此,下面的三条语句都是错误的:

    b = a++
    fmt.Println(a++)
    
    ++a

 

3:元组赋值是另外一种形式的赋值语句,它容许同时更新多个变量的值。在赋值以前,赋值语句右边的全部表达式将会先进行求值,而后再统一更新左边对应变量的值。因此能够这样交换两个变量的值:x, y = y, x

 

4:赋值语句是显式的赋值形式。程序中还有不少地方会发生隐式的赋值行为:函数调用会隐式地将调用参数的值赋值给函数的参数变量,一个返回语句将隐式地将返回操做的值赋值给结果变量,一个复合类型的字面量也会产生赋值行为。例以下面的语句:

medals := []string{"gold", "silver", "bronze"}

 

隐式地对slice的每一个元素进行赋值操做,相似这样写的行为:

medals[0] = "gold" 
medals[1] = "silver" 
medals[2] = "bronze"

 

无论是隐式仍是显式地赋值,只有右边的值对于左边的变量是可赋值的,赋值才是容许的。

可赋值性的规则对于不一样类型有着不一样要求,目前而言,它的规则是简单的:类型必须彻底匹配,nil能够赋值给任何指针或引用类型的变量。常量则有更灵活的赋值规则,由于这样能够避免没必要要的显式的类型转换。

对于两个值是否能够用==或!=进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之依然。

 

五:类型

         1:变量或表达式的类型定义了对应存储值的属性特征,例如数值在内存的存储大小(或者是元素的bit个数),它们在内部是如何表达的,是否支持一些操做符,以及它们本身关联的方法集等。

         2:类型声明语句建立了一个新的类型名称,和现有类型具备相同的底层结构。新命名的类型提供了一个方法,用来分隔不一样概念的类型,这样即便它们底层类型相同也是不兼容的。

         类型声明的格式是:type 类型名字 底层类型

类型声明语句通常出如今包一级,所以若是新建立的类型名字的首字符大写,则在外部包也可使用。

 

3:下面的语句:

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

 

声明了两种类型:Celsius和Fahrenheit分别对应不一样的温度单位。它们虽然有着相同的底层类型float64,可是它们是不一样的数据类型,所以它们不能够被相互比较或混在一个表达式运算。刻意区分类型,能够避免一些像无心中使用不一样单位的温度混合计算致使的错误。

 

4:对于每个类型T,都有一个对应的类型转换操做T(x),用于将x转为T类型(译注:若是T是指针类型,可能会须要用小括弧包装T,好比(*int)(0))。只有当两个类型的底层基础类型相同时,才容许这种转型操做,或者是二者都是指向相同底层结构的指针类型。这些转换只改变类型而不会影响值自己。

数值类型之间的转型也是容许的,字符串和一些特定类型的slice之间也是能够转换的,这类转换可能改变值的表现。例如,将一个浮点数转为整数将丢弃小数部分,将一个字符串转为[]byte类型的slice将拷贝一个字符串数据的副本。在任何状况下,运行时不会发生转换失败的错误(译注: 错误只会发生在编译阶段)

下面的转换都是合法的;

    var a int = 1
    x := Celsius(1)
    y := Celsius(a)
    z := float64(a)

 

 

         5:比较运算符如==或<能够用来比较一个命名类型的变量和另外一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间作比较。可是若是两个值有着不一样的类型,则不能直接进行比较:

    var c Celsius
    var f Fahrenheit

    fmt.Println(c == 0)          // "true"
    fmt.Println(f >= 0)          // "true"
    fmt.Println(c == f)          // compile error: type mismatch
    fmt.Println(f >= float64(3)) // compile error: type mismatch
    fmt.Println(c == Celsius(f)) // "true"!

 

 

6:命名类型还能够为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,称为类型的方法集。后续会详细介绍,这里仅说写简单用法:

func (c Celsius) String() string { 
    return fmt.Sprintf("%g°C", c) 
}

 

Celsius类型的参数c出如今了函数名的前面,表示声明的是Celsius类型的一个叫名叫String的方法,该方法返回该类型对象的字符串表示。

 

许多类型都会定义一个String方法,由于当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印:

    x := Celsius(1.04)
    fmt.Println(x)      //1.04°C

 

 

六:包和文件

1:Go语言中的包和其余语言的库或模块的概念相似,目的都是为了支持模块化、封装、单独编译和代码重用。

一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,同一个包中的多个源文件中,不能定义同名的变量、函数和类型等。

一般一个包所在目录路径的后缀是包的导入路径;例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld。

 

2:每一个包都对应一个独立的名字空间。例如,在image包中的Decode函数和在unicode/utf16包中的 Decode函数是不一样的。要在外部引用该函数,必须显式使用image.Decode或utf16.Decode形式访问。

 

3:包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其余源文件是能够直接访问的,就好像全部代码都在一个文件同样。

包可让咱们经过控制哪些名字是外部可见的来隐藏内部实现信息。一个简单的规则是:若是一个名字是大写字母开头的,那么该名字是导出的(译注:由于汉字不区分大小写,所以汉字开头的名字是没有导出的)。

 

4:每一个源文件都是以包的声明语句开始,用来指名包的名字。包声明前能够有包注释。一个包一般只有一个源文件有包注释。若是有多个包注释,目前的文档工具会根据源文件名的前后顺序将它们连接为一个包注释。若是包注释很大,一般会放到一个独立的doc.go文件中。

 

5:在Go语言程序中,每一个包都是有一个全局惟一的导入路径。导入语句中相似"gopl.io/ch2/tempconv"的字符串对应包的导入路径。Go语言的规范并无定义这些字符串的具体含义或包来自哪里,它们是由构建工具来解释的。当使用Go语言自带的go工具箱时,一个导入路径表明一个目录中的一个或多个Go源文件。

除了包的导入路径,每一个包还有一个包名,包名通常是短小的名字(并不要求包名是惟一的),包名在包的声明处指定。按照惯例,一个包的名字和包的导入路径的最后一个字段相同,例如gopl.io/ch2/tempconv包的名字通常是tempconv。

导入语句将导入的包绑定到包名,而后经过该包名就能够引用包中导出的所有内容。所以能够tempconv.CToF的形式来访问gopl.io/ch2/tempconv包中的内容。在默认状况下,导入的包绑定到tempconv名字(译注:这包声明语句指定的名字),可是咱们也能够绑定到另外一个名称,以免名字冲突。

 

6:若是导入了一个包,可是又没有使用该包将被看成一个编译错误处理。

7:可使用golang.org/x/tools/cmd/goimports导入工具,它能够根据须要自动添加或删除导入的包。

 

8:包的初始化首先是解决包级变量的依赖顺序,而后按照包级变量声明出现的顺序依次初始化:

var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 经过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

 

若是包中含有多个.go源文件,Go语言的构建工具首先会将.go文件根据文件名排序,而后依次调用编译器编译,若是同一个包中多个源文件中的变量有相互依赖的关系,则初始化顺序也能够是穿插的。

 

每一个文件均可以包含多个init初始化函数。这样的init初始化函数除了不能被调用或引用外,其余行为和普通函数相似。在每一个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。

 

包的初始化过程当中,首先初始化全部go文件的变量,而后才是调用init函数。好比,若是包中包含两个go文件:file1和file2,file1和file2的代码分别以下:

//file1.go
var a int = getvalue(3)
var b int = c + getvalue(2)

func init() {
    fmt.Println("this is p1 init func")
}

//file2.go
func getvalue(arg int) int {
    fmt.Printf("this is getvalue(%d)\n", arg)
    return arg
}

func init() {
    fmt.Println("this is p2 init func")
}

func init() {
    fmt.Println("this is p22 init func")
}

var c int = getvalue(33)
var d int = getvalue(11) + getvalue(111)

 

根据文件名的排序,首先初始化file1中定义的变量a,该变量须要调用file2中定义的getvalue函数;而后初始化变量b,该变量须要用file2中定义的变量c,所以先初始化file2中的变量c,以后再初始化b;file1中的变量初始化完成以后,而后是初始化file2中的变量d;全部变量初始化完成以后,开始调用文件中的init函数,首先是file1中的init,而后是file2中的两个init,所以,运行结果以下:

this is getvalue(3)
this is getvalue(33)
this is getvalue(2)
this is getvalue(11)
this is getvalue(111)
this is p1 init func
this is p2 init func
this is p22 init func

 

 

10:每一个包在解决依赖的前提下,以导入声明的顺序初始化,每一个包只会被初始化一次。所以,若是一个p包导入了q包,那么在p包初始化的时候q包必然已经初始化过了。初始化工做是自下而上进行的,main包最后被初始化。以这种方式,能够确保在main函数执行以前,全部依然的包都已经完成初始化工做了。

 

七:做用域

         1:声明语句的做用域是指源代码中能够有效使用这个名字的范围。

不要将做用域和生命周期混为一谈。声明语句的做用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它能够被程序的其余部分引用;是一个运行时的概念。

 

2:语法块是由花括弧所包含的一系列语句,语法块内部声明的名字是没法被外部语法块访问的。

 

3:对于内置的类型、函数和常量,好比int、len和true等是在全局做用域的,所以能够在整个程序中直接使用;任何在在函数外部(也就是包级语法域)声明的名字能够在同一个包的任何源文件中访问的;对于导入的包,例如tempconv导入的fmt包,则是对应源文件级的做用域,所以只能在当前的文件中访问导入的fmt包,当前包的其它源文件没法访问在当前源文件导入的包;还有许多声明语句,则是局部做用域的,它只能在函数内部(甚至只能是局部的某些部分)访问;控制流标号,就是break、continue或goto语句后面跟着的那种标号,则是函数级的做用域。

 

4:一个程序可能包含多个同名的声明,只要它们在不一样的词法域就没有关系。例如,能够声明一个局部变量,和包级的变量同名。

当编译器遇到一个名字引用时,它首先从最内层的词法域向全局的做用域查找。若是查找失败,则报告“未声明的名字”这样的错误。若是该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种状况下,内部声明屏蔽了外部同名的声明。

 

5:有许多语法块是if或for等控制流语句构造的:

for i := 0; i < len(x); i++ {
    x := x[i]
    if x != '!' {
        x := x + 'A' - 'a'
        fmt.Printf("%c", x) 
    }
}

 

正如上面例子所示,并非全部的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。上面的for语句建立了两个词法域:花括弧包含的是显式的for循环体部分词法域,另一个隐式的部分则是循环的初始化部分,好比用于迭代变量i的初始化。隐式的词法域部分的做用域包含条件测试部分和循环后的迭代部分( i++ ),固然也包含循环体词法域。

和for循环相似,if和switch语句也会在条件部分建立隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效做用域范围:

if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

 

第二个if语句嵌套在第一个内部,所以第一个if语句条件初始化词法域声明的变量在第二个if中也能够访问。switch语句的每一个分支也有相似的词法域规则:条件部分为一个隐式词法域,而后是每一个分支的词法域。

 

6:在包级别,声明的顺序并不会影响做用域范围,所以一个先声明的能够引用它自身或者是引用后面的一个声明,可是若是一个变量或常量递归引用了自身,则会产生编译错误。

 

要特别注意短变量声明语句的做用域范围,考虑下面的程序:

var cwd string
func init() {
    cwd, err := os.Getwd() // compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

 

虽然cwd在外部已经声明过,可是 := 语句仍是将cwd和err从新声明为新的局部变量。由于内部声明的cwd将屏蔽外部的声明,所以上面的代码并不会正确更新包级声明的cwd变量。

 

有许多方式能够避免出现相似潜在的问题。最直接的方法是经过单独声明err变量,来避免使用 := 的简短声明方式:

var cwd string
func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

 

 

http://docs.ruanjiadeng.com/gopl-zh/ch2/ch2.html

相关文章
相关标签/搜索