自学内容网 自学内容网

【Go语言圣经】第二节:程序结构

二. 程序结构

上个月我跟随 Golang 开发实战项目快速过了一遍 Golang 的基础语法,感觉学习的不是非常深入,不如之前学 C++ 时直接啃 C++ Primer 的效果好,因此打算重新利用《Go 语言圣经》这个非常好的开源资源重新系统地回顾一下 Golang 的基础语法(也算是为春天即将开始找实习准备一下八股了)。

这里我跳过了第一章入门,实际上《Go 语言圣经》我半年前已经看过一小部分了,感觉第一章的内容偏向于若干个 demo,这个系列就不再回顾了,我直接从语法开始学习。

2.1 命名

Golang 当中的关键字有 25 个:

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

当然,和其它语言一样,不能使用单独的关键字自定义名字,只能在特定的语法结构中使用。

此外还有 30 多个预定义的名字,比如 int 和 true 等:

内建常量: true false iota nil

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

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

如果一个名字在函数内部定义,那么它就只在函数内部生效。如果名字定义在函数的外部那么这个名字在当前包的所有文件中都可以访问(重要:)名字的开头字母的大小写决定了名字在包外的可见性。如果名字是大写字母开头的,那么它将是导出的,即可以被外部的包访问。

名字的长度没有逻辑限制,但请尽量短小简洁。

在习惯上,Golang 程序员推荐采用驼峰式命名法。

2.2 声明

声明语句定义了程序的各种实体对象以及部分或全部的属性。Golang 主要有四种类型的声明语句:var、const、type 和 func。分别对应于变量、常量、类型和函数实体对象的声明(注意,在 Golang 当中,函数是一等公民,可以把函数当作对象那样来对待它)(速通完 TypeScript 之后,我个人认为 Golang 的声明与 Type/JavaScript 非常的像)。

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

下例声明了一些变量:

package main

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)
}

boilingF 是包一级范围声明语句声明的,f 和 c 是 main 函数内部声明语句声明的。包一级声明语句声明的名字在整个包对应的每个源文件都可以访问。

函数由函数名、参数列表、一个可选的返回值列表和包含函数定义的函数体租车给:

package main

import "fmt"

func main(){
const freezingF, boilingF = 32.0, 212.0
fmt.Printf("%g°F = %g°C\n", freezingF, fToC(freezingF)) // "32°F = 0°C"
    fmt.Printf("%g°F = %g°C\n", boilingF, fToC(boilingF))   // "212°F = 100°C"
}

func fToC(f float64) float64 {
return (f - 32) * 5 / 9
}

参数列表定义了一个形参 f,它的类型是 float64,fToC的返回值也是 float64。(注意,Golang 当作所有的函数都只有传值调用,没有“引用”的概念,不过 Golang 确实有地址和指针的概念,只不过指针当作保存的值也是地址的值)

2.3 变量

var 声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并设置变量的初始值,其一般语法如下:

var 变量名字 类型 = 表达式

其中“类型”和“= 表达式”是可以省略的。省略类型则根据初始化表达式的值来推导变量的类型信息。如果初始化表达式被省略,则使用零值(即默认值)来初始化变量。

零值初始化机制可以确保每个声明的变量总有一个良好定义的值(0、空串、nil 等都是零值)。

可以在一个声明语句中同时声明一组变量:

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

也可以通过函数调用,使用函数返回的多个返回值进行初始化:

var f, err = os.Open(name)

2.3.1 简短变量声明

函数内部,可以使用简短变量声明语句的形式来用于声明和初始化局部变量,它的形式是:名字 := 表达式。变量的类型根据表达式自动推导:

freq := rand.Float64() * 3.0

而 var 形式的声明语句往往用于需要显式指定变量类型的地方。

与 var 一样,简短变量声明可以用于声明和初始化一组变量:

i, j := 0, 1

记住:=是变量声明,而=是赋值,不要搞混。

可以使用函数的返回值初始化变量:

f, err := os.Open(name)

一个比较微妙的地方在于,简短变量声明左侧的变量未必是全部刚刚声明的。如果有一些已经在相同的词法域中声明过了,那么简短变量声明语句对这些声明过的变量只有赋值行为。(注意:简短变量声明语句中必须声明至少一个新的变量,否则编译将不通过)

2.3.2 指针

一个指针的值是另一个变量的地址。通过指针可以读写对应变量的值,而不需要知道该变量的名字。

使用取地址符&可以获取变量的地址,比如对于var x int声明的变量x&x可以获得它的地址,令p := &xp的类型就是*int

对指针类型使用*可以完成解引用操作,即使用*p可以获得变量x的值。

对于聚合类型的每个成员——比如结构体的每个字段、或是数组中的每个元素,它们都对应一个变量,因此它们都可以被取地址。

变量有时候也称为可寻址的值。即使变量由表达式临时生成,那么表达式也必须接受&取地址操作。

任何类型的指针的零值都是 nil,指针可以进行相等测试,当它们指向同一个变量或都为 nil 的时候相等。

在 Golang 当中,返回函数中的局部变量的地址也是安全的。实际上,Golang 可以在函数中建立局部变量并将局部变量作为返回值,这听起来非常反直觉(比如在 C++ 当中,返回局部变量的地址是绝对危险的),但是这个性质在 Golang 当中是 work 的:

var p = f()// valid

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

每次调用函数 f() 都将返回不同的结果:

fmt.Println(f() == f())// false

由于指针包含的是一个变量的地址,因此如果将指针作为参数调用函数,那将可以在函数中通过该指针来更新变量的值。例如:

func incr(p *int) int {
*p ++// 改变的只是 p 指向的变量的值, 注意这是 Golang 不是 C++
// C++ 的行为是先使迭代器后移, 再进行解引用
return p
}

v := 1
incr(&v)
fmt.Println(incr(&v))

每次我们对一个变量取地址或是复制指针,都是为原变量创建了新的别名。例如,*p就是 v 的别名。

2.3.3 new 函数

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

p := new(int)
fmt.Println(*p)// 0
*p = 2
fmt.Println(*p)// 2

2.3.4 变量的生命周期

包一级声明的变量的声明周期与整个程序的运行周期一致。局部变量的生命周期是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。

2.4 赋值

在 Golang 当中,支持++--的递增/递减语句(注意,它们是语句而不是表达式,因此没有x = i ++一说)

2.4.1 元组赋值

元组赋值允许同时更新多个变量的值,比如下例完成了两个元素的交换:

i, j = j, i
a[i], a[j] = a[j], a[i]

2.4.2 可赋值性

只有右边的值对于左边的变量是可赋值的,赋值语句才允许执行。它的规则很简单:类型必须完全匹配,nil 可以赋值给任何指针或引用类型的变量。

2.5 类型

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

type 类型名字 底层类型

类型声明通常出现在包一级。同样地,如果新创建的类型名字的首字母是大写的,则在包外部也可以使用。

下例将不同的温度单位分别定义为不同的类型:

type Celsius float64
type Fahrenheit float64

上例中的 Celsius 和 Fahrenheit 分别对应不同的温度单位。虽然它们有着相同的底层类型 float64,但是它们仍然是不同的数据类型。

2.6 包和文件

Golang 当中的包和其它语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。

每个包都对应一个独立的命名空间。

包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。在 Golang 当中,一个简单的规则是,如果一个名字是大写字母开头的,那么该名字是导出的。

2.6.1 导入包

如果导入了一个包但没有使用,将会被当作编译错误。编译器会为我们检查这个错误,并且大多是 IDE 已经集成了 Go 代码的格式化工具。

2.6.2 包的初始化

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

var a = b + c;// 第三个初始化
var b = f();// 第二个初始化
var c = 1;// 第一个初始化
func f() int { return c + 1 }

如果包中包含多个.go源文件,它们将按照发送给编译器的顺序进行初始化,Golang 的构建工具会首先将.go文件根据文件名排序,然后依次调用编译器编译。

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的 init 初始化函数来简化初始化工作。每个文件都可以包含多个 init 初始化函数:

func init() { /* ... ... ... */ }

init 函数除了不能被调用或引用外,其它行为和普通函数相同。每个二五年间中的 init 函数,在程序开始执行时按照它们声明的顺序被自动调用。

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因而,如果一个 p 包导入了 q 包,则默认 q 包已经被初始化过了。初始化工作是自下而上进行的,因此作为程序主入口的 main 函数将会最后被初始化。

2.7 作用域

Golang 的作用域与常见编程语言的作用域概念基本是相同的,此处不再赘述。


原文地址:https://blog.csdn.net/Coffeemaker88/article/details/145240878

免责声明:本站文章内容转载自网络资源,如侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!