Language GO II
从本节开始,将阐述GO的基本语法,程序结构。
2.1 命名
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
如果一个名字是在函数内部定义,那么它就只在函数内部有效,即类似C语言中的局部变量。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问,即类似C语言中的全局变量。
名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如fmt包的Printf函数就是导出的,可以在fmt包外部访问。包本身的名字一般总是用小写字母。
2.2 声明
package main
import "fmt"
const boilingF = 212.0
func main() {
var f = boilingF
var c = (f - 32) * 5 / 9
fmt.Printf("boiling point = %g°F or %g°C\n", f, c)
// Output:
// boiling point = 212°F or 100°C
}
2.3 变量
2.3.1 简短变量声明
简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了,且简短变量声明语句中必须至少要声明一个新的变量。
2.3.2 指针
指针的概念也与C语言中类似,指针并非是指向变量,而是指向变量的地址。
x := 1
p := &x // p, of type *int, points to x
fmt.Println(*p) // "1"
*p = 2 // equivalent to x = 2
fmt.Println(x) // "2"
与C语言的语法大致相同,指向x的地址这一操作由&来实现,*p代表指针p指向的地址所存储的值。
如果用“var x int”声明变量x,那么&x将产生一个指向该整数变量的指针,如p=&x,指针对应的数据类型是*int,指针被称之为“指向int类型的指针”,可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时*p为p指针指向的变量的值。
对于聚合类型每个成员——比如结构体的每个字段、或者是数组的每个元素——也都是对应一个变量,因此可以被取地址,也就是可以有指针指向他们。
GO语言中,指针的零值是nil。如果p指向某个有效变量,那么p != nil就会为逻辑1。Go语言中逻辑1输出结果为True,逻辑0输出结果为false。指针间如果存在互相等价/等值的关系,则意味着他们指向同一个变量地址或都为nil。如:
var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false"
在Go语言中,函数中引用的局部变量的地址也是可以被当作返回值的。例如下面的代码,
func f() *int {
v := 1
return &v
}
在函数f的定义中,类型为*int,上文提到了指针的数据类型为本身变量类型前加一个“*”。而在函数f的调用中,我们返回了创建的局部变量v的地址,这个在GO语言中是可以被执行的,而在C语言中这是违规操作。并且因为这个v是f函数的局部变量,所以每次调用f函数时都会为v分配新的地址,所以每次调用后返回v的地址也是不同的。所以当执行以下语句时,输出会是false:
fmt.Println(f() == f()) // false
因为指针包含了一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。
例如下面这个例子就是通过指针来更新变量的值,然后返回更新后的值,以下代码并不能被Go编译器直接执行,需要自己更改。
func incr(p *int) int {
*p++ // 非常重要:只是增加p指向的变量的值,并不改变p指针!!!
return *p
}
v := 1
incr(&v) // side effect: v is now 2
fmt.Println(incr(&v)) // "3" (and v is 3)
incr函数为int类型,其中incr定义的参数为 *int类型的指针p,++语法与C语言中一致,*p++为p所在的地址的变量的值加1。而正因为incr中所需要的参数为指针类型,所以实际使用的时候,我们用incr函数所作用的对象也必须为指针变量(*p=v中的p)或变量的地址即&v。第一次调用incr(&v)时,v的值会增加1,所以当打印再次调用incr时,输出结果就会为3。
当我们对一个变量取地址,其实就是为原变量创建了新的代号。
例如,*p就是变量v的新代号。当我们使用指针后,我们可以不必记得原变量的名字也可以使用原变量的值,因为我们使用指针代指原变量,但是如果我们创建了太多新指针来指向同一个变量,有时就也会适得其反。
package main
import (
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")
func main() {
flag.Parse()
fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
fmt.Println()
}
}
Go语言中的flag库,高强度使用了指针。flag库主要为对命令行参数进行功能筛选,如linux的指令中ls -a的-a,输入-a表示展示所有。而flag库在Go语言中就是这个工作,为命令行参数提供功能选择。
上方的代码是之前echo函数的加强版。
代码中,n变量是flag函数中的bool变量,bool变量就是真假变量,只有true or false两种值。n变量的描述中,“n”代表了,激活n这个功能,需要输入-n,false代表默认值,即如果你不输入n的情况,默认是false,也就是假,最后一个“omit trailing newline”是功能描述,即无视换行符。即,此功能的作用就是,如果你在命令行中输入-n,那么输出结果就会无视换行符。
sep变量类似,flag.String表示sep是字符类型的变量,“s”表示需要输入-s来激活功能,“ ”表示默认是空格,“separator”表示功能是分离符。此功能即是,当你输入-s后,-s后面的第一个字符会被识别为新的分离符,即如果输入 -s / a b, 那么输出结果会是a/b,如果不输入-s,那么默认的分离符就是空格。
flag库中,基本都是指针变量,所以调用n,sep这两个全局变量时需要加上*。main函数的功能实现,基本是flag库的函数的作用,不做特殊讲解,值得注意的就是,if那一行的if !*n, 这句话代表,当!*n为真时,执行下面语句,!表示否,否*n为真就是当n为false时为真,也就是当你不输入-n时的情况,上文中提到,-n的功能时无视换行符,那么不输入-n也就是不无视换行符,所以会调用一下println,printlin会输出一个换行符。
2.3.3. new函数
p := new(int) // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2 // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"
此例中,new函数所创建的匿名变量为int类型,也可以是其他类型,匿名变量被创建后初始值为所属类型的零值。func newInt() *int {
return new(int)
}
func newInt() *int {
var dummy int
return &dummy
}
p := new(int)
q := new(int)
fmt.Println(p == q) // "false"
2.3.4. 变量的生命周期
2.4. 赋值
x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
a * = b
a + = b
2.4.1. 元组赋值
x, y = y, x
a[i], a[j] = a[j], a[i]
func gcd(x, y int) int {
for y != 0 {
x, y = y, x%y
}
return x
}
2.4.2. 可赋值性
medals := []string{"gold", "silver", "bronze"}
隐式地对slice的每个元素进行赋值操作,类似这样写的行为:
medals[0] = "gold"
medals[1] = "silver"
medals[2] = "bronze"
==
或!=
进行相等比较的能力也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是可赋值给第一个变量的。2.5. 类型
变量或表达式的类型定义了对应存储值在计算机存储器的特征,比如int和string的变量的存储空间的分配是不一样的。
我们也可以通过自己定义数据类型(基于Go语言本身提供的类型/底层类型)来提高书写程序时的可读性。
定义新数据类型时采用以下结构:
type 类型名字 底层类型
类型声明语句一般出现在包一级(如果不是全局可读,那么失去了定义新类型的意义),因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。
以下代码定义了两个新数据类型,华氏度与摄氏度:
// Package tempconv performs Celsius and Fahrenheit temperature computations.
package tempconv
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度
const (
AbsoluteZeroC Celsius = -273.15 // 绝对零度
FreezingC Celsius = 0 // 结冰点温度
BoilingC Celsius = 100 // 沸水温度
)
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
这个包声明了两种类型:Celsius和Fahrenheit分别对应摄氏度和华氏度。它们虽然有着相同的底层类型float64,但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算。
也因如此,这两种数据不可被直接进行运算,程序的可读性和维护性都得到了提供高;因此也需要有配套的将华氏度摄氏度相互转换的函数,上文中即CToF和FToC。 return Fahrenheit(c*9/5 + 32)
这一句中的Fahrenheit(c*9/5 + 32)的作用为,将最终的转换结果转化为华氏度的数据类型。Fahrenheit(t)与 Celsius(t)这样的操作,是强制数据类型转换,但是数据的值并不会发生变化,所以在括号内需要自己对数据的值根据规则进行改写。
对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对数学运算的支持。这意味着,Celsius和Fahrenheit类型的算术运算行为和底层的float64类型是一样的。
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
比较运算符==
和<
也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较:
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(c == Celsius(f)) // "true"!
命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,我们称为类型的方法集。
下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印。
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
2.6. 包和文件
// Package tempconv performs Celsius and Fahrenheit conversions.
package tempconv
import "fmt"
type Celsius float64
type Fahrenheit float64
const (
AbsoluteZeroC Celsius = -273.15
FreezingC Celsius = 0
BoilingC Celsius = 100
)
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
func (f Fahrenheit) String() string { return fmt.Sprintf("%g°F", f) }
转换函数则放在另一个conv.go源文件中:
package tempconv
// CToF converts a Celsius temperature to Fahrenheit.
func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }
// FToC converts a Fahrenheit temperature to Celsius.
func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }
每个源文件都是以包的声明语句开始,用来指明包的名字。当包被导入的时候,包内的成员将通过类似tempconv.CToF的形式访问。而包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。要注意的是tempconv.go源文件导入了fmt包,但是conv.go源文件并没有,因为这个源文件中的代码并没有用到fmt包。
然后就可以像一下代码一样引用包的内容。
fmt.Printf("Brrrr! %v\n", tempconv.AbsoluteZeroC) // "Brrrr! -273.15°C"
fmt.Println(tempconv.CToF(tempconv.BoilingC)) // "212°F"
2.6.1. 导入包
// Cf converts its numeric argument to Celsius and Fahrenheit.
package main
import (
"fmt"
"os"
"strconv"
"tempconv"
)
func main() {
for _, arg := range os.Args[1:] {
t, err := strconv.ParseFloat(arg, 64)
if err != nil {
fmt.Fprintf(os.Stderr, "cf: %v\n", err)
os.Exit(1)
}
f := tempconv.Fahrenheit(t)
c := tempconv.Celsius(t)
fmt.Printf("%s = %s, %s = %s\n",
f, tempconv.FToC(f), c, tempconv.CToF(c))
}
}
2.6.2. 包的初始化
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初始化函数
func init() { /* ... */ }
这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。以这种方式,可以确保在main函数执行之前,所有依赖的包都已经完成初始化工作了。
2.7. 作用域
作用域是指代码中可以有效使用变量/函数的范围。
作用域与生命周期并不一样。作用域是代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。
句法块是由花括弧所包含的一系列语句。句法块内部声明的名字是无法被外部访问或使用的。
对全局的代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个for、if和switch语句,也都有对应词法块;每个switch或select的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。
声明语句对应的词法域决定了作用域范围的大小。
一个程序可以有很多同名变量,只要他们的声明在不同的块中即可,因为互相不可被调用,所以即使同名也无法被影响。
当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字被两个不同的词法域声明过,则使用两个此法域中较小的那个,如下列代码中,f会首先读取main块中的定义而忽视全局里的f():
func f() {}
var g = "g"
func main() {
f := "f"
fmt.Println(f) // "f"; local var f shadows package-level func f
fmt.Println(g) // "g"; package-level var
fmt.Println(h) // compile error: undefined: h
}
在函数中词法域可以深度嵌套,因此内部的一个声明可能屏蔽外部的声明。还有许多语法块是if或for等控制流语句构造的。下面的代码有三个不同的变量x,因为它们是定义在不同的词法域(这个例子只是为了演示作用域规则,但不是好的编程风格)。
func main() {
x := "hello!"
for i := 0; i < len(x); i++ {
x := x[i]
if x != '!' {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
}
在x[i]
和x + 'A' - 'a'
声明语句的初始化的表达式中都引用了上一级的x变量的定义,如x:=x[i]中,右边的x实际上是上一级的x:="hello"。
正如上面例子所示,并不是所有的词法域都显式地对应到由花括弧包含的语句;还有一些隐含的规则。
下面的例子同样有三个不同的x变量,每个声明在不同的词法域,一个在函数体词法域,一个在for隐式的初始化词法域,一个在for循环体词法域;只有两个块是显式创建的,在for循环的定义行中,隐式的定义了一个x。
func main() {
x := "hello"
for _, x := range x {
x := x + 'A' - 'a'
fmt.Printf("%c", x) // "HELLO" (one letter per iteration)
}
}
和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
在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。但是如果一个变量或常量递归引用了自身,则会产生编译错误。
Comments
Post a Comment