Go编程基础-2. 程序结构

75

程序结构

1.名称

  1. 函数、变量、常量、类型等命名规则:名称的开头是一个字母或下划线,后面可以跟任意数量的字母、数字和下划线,并区分大小写;注意名称不能是Go内置的25个关键词及预声明的变量、函数、类型:image-1.png
    这些预声明的名称不是预留的,可以在声明中使用但是并不建议,因为这可能会产生冲突从而造成未知的错误。
  2. 如果一个实体在函数中声明,那么它只在函数局部有效;如果声明在函数外,那么它对包内的所有源文件有效;实体名称的第一个字母的决定了其是否跨包可见,大写表示导出的,对外可见且可访问。
  3. Go程序员使用"驼峰式"的风格——更推荐使用大写字母而非下划线,例如:QuoteRuneToASCII

2.变量声明

// 1. 标准变量声明
var i, j, k int
var i, j, k = true, 2.3, 4
var f, err = os.Open(fileName)

// 2. 短变量声明
i, j, k := true, 2.3, 4

3.指针

Go语言中的函数传参都是值传递,因此想要在函数中修改原有变量就需要使用指针。Go指针的基本用法和c类似:

x := 1
p := &x
fmt.Println(*p)

// 通过函数修改变量值
func incr(p *int) int {
    *p++ // 递增p所指向的值,p自身保持不变
    return *p
}

指针类型的零值是nil。

4.new函数

另外一种创建变量的方法是使用内置的new函数, new(T)表示创建一个未命名的T类型变量,初始化为零值,并返回其地址*T:

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

使用new创建的变量和普通变量没有什么不同,知识不需要引入一个虚拟名称,而是直接通过指针进行使用。

5. 变量的声明周期

生命周期指的是程序执行过程中变量存在的时间阶段。

  • 包级别变量的生命周期是整个程序的执行时间;
  • 局部变量的生命周期则是动态的,从创建开始一致生存到不可访问为止,这时占用的存储空间才会被回收 - 因此要小心内存泄漏
  • Go语言中,局部变量的生命周期通过可达性分析法确定: 每一个包级别的变量,以及每一个当前执行函数的局部变量,都可以作为追溯该变量路径的源头,通过指针或其它方式的引用来找到该变量。如果没有路径可以指向当前变量,那么该变量就不再可以访问,可以被GC回收,视作生命周期的结束。
    PS: Go编译器可以选择使用堆或栈上的空间来分配,但是这个选择不是基于使用var或new关键词来生命变量,而是基于编译器的逃逸分析。正常情况下,局部变量应该保存在栈上(无论是var或者new),但是当这个变量被的全局变量指针所引用后,这个变量就发生了逃逸现象,需要在堆上开辟一片空间并进行存储。————每一次变量逃逸都需要一次额外的内存分配过程

6. 赋值

GO语言的赋值方法和其它语言大体一致,其中比较特殊的是多重赋值机制,利用这一机制我们可以较为简单地实现 1. swap 2. 斐波那契数列 3...

// 1. swap
a[i], a[j] = a[j], a[i]

// 2. 斐波那契数列
func fib(n int) int {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        x, y = y, x+y
    }
    return x
}
// 斐波那契数列完整实现
package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
)

func main() {
	input := bufio.NewScanner(os.Stdin)
	var num int
	var err error

	if input.Scan() {
		numStr := input.Text()
		num, err = strconv.Atoi(numStr)
		if err != nil {
			fmt.Fprintln(os.Stderr, err)
			os.Exit(1)
		}

	}

	res := fib(num)
	fmt.Println(res)
}

func fib(n int) int {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		x, y = y, x+y
	}
	return x
}

除了显式赋值之外,return等方式也可以实现隐式地赋值效果,但是无论哪种赋值方式都要求左边的变量和右边的值类型相同,即:赋值 只有在值对于变量类型是可赋值的时候才是合法的。

7. 类型声明

Go的类型声明是其实现面向对象编程的一种重要方式,type关键词声明和定义了一个新的命名类型,它既可以是某个已有类型的别名,也可以是多个已有类型的组合。基本语法如下:

type name underlying_type

命名类型提供了一种方式实现不同应用场景下数据类型的区分/不兼容使用

以摄氏度和华氏度为例:

package main

import "fmt"

type Celsius float64
type Fahrenheit float64

const (
	AbsolutZeroC Celsius = -273.15
	FreezingC    Celsius = 0
	BoilingC     Celsius = 100
)

func C2F(c Celsius) Fahrenheit {
	return Fahrenheit(c*9/5 + 32)
}

func F2C(f Fahrenheit) Celsius {
	return Celsius((f - 32) * 5 / 9)
}

func main() {
	var c Celsius
	var f Fahrenheit

	fmt.Println(c == 0) // true
	fmt.Println(f >= 0) // true
	// fmt.Println(c == f) // 编译错误:类型不匹配
	fmt.Println(c == Celsius(f)) // true
}

从上述案例中可以看出,命名类型和底层数据类型之间可以相互比较;但是同一底层类型的多个别名之间是相互隔离的,无法对比,会在编译期间报错

8. 包和文件

包的作用

Go语言中,包的作用和其它语言中的库或模块作用相似,用于支持模块化、封装、编译隔离和重用

  1. 一个包的源代码保存在一个或多个以 .go 结尾的文件中,它所在目录名的尾部就是包的导入路径,例如,gopl.io/chl/helloworld 包的文件存储在目录 $GOPATH/src/gopl.io/chl/helloworld 中。
  2. 每一个包给它的声明提供独立的命名空间。例如,Decode 标识符在 image包中和unicode/utfl6 包中一样,但是关联了不同的函数。为了从包外部引用一个函数,我们必须明确修饰标识符来指明所指的是 image.Decode 或 utfl6.Decode。
  3. 包让我们可以通过控制变量在包外面的可见性或导出情况来隐藏信息。在 Go 里,通过一条简单的规则来管理标识符是否对外可见:导出的标识符以大写字母开头

包的引用

// 1. 正常导包(单个)
import "fmt"

// 2. 正常导包(多个)
import (
    "fmt"
    "net/http"
    "package"
)

// 3. 别名导包
// 不同包下的类型可能标识符一致,例如:image.Decode 和 utfl6.Decode, 这种情况下我们无法通过Decode进行区分;
// 要么注明完整路径进行引用,或者通过别名的方式进行区分和使用)
import (
    fmt "fmt"
    net "net/http"
    test "github.com/xxx/test"
)

// 4. 点导包
// 将包中的方法直接注册到当前包的上下文中,直接调用方法名即可,不再需要加包前缀
import (
    . "fmt"
) 

// 5. 下划线导包
// 一些包需要在导入的同时完成初始化操作,一遍后续函数的执行,例如建立数据库连接 
// * Go语言为这一需求提供了init函数,当一个包被引用是,就会执行包下各模块的init方法
// * 但是有些情况下,我们只想执行init方法,并不想要真正导入包,此时就可以使用下划线导包
import (
    _ "github.com/xxx/initPack"
)

9. 包初始化

包的初始化分为两个部分:1. 包级别变量的初始化, 2. init方法的执行

    1. 包的初始化按照在程序中导入的顺序进行,自下向上(如果包之间的引用关系为a->b->c,那么初始化顺序为c->b->a),main包最后初始化。
    1. init函数用来执行一些复杂、动态的初始化操作。包下的任何文件,可以包含任意个init函数(甚至一个文件中可以包含多个)。
// 案例: PopCount函数,用于统计一个整数的二进制位中1的个数
package main

// 预初始化的pc数组,记录了0-255的所有整数的popcount
var pc [256]byte 

// 初始化过程,每个数的popcount == pc[i>>1] + byte(i&1)
func init() {
	for i := range pc {
		pc[i] = pc[i/2] + byte(i&1)
	}
}

// 对于一个uint64位整数,可以将其拆分为8个0-255之间的整数,单独进行PopCount统计设置为1的位数(查表),然后进行求和
func PopCount(x uint64) int {
	return int(pc[x>>(0*8)] +
		pc[byte(x>>(1*8))] +
		pc[byte(x>>(2*8))] +
		pc[byte(x>>(3*8))] +
		pc[byte(x>>(4*8))] +
		pc[byte(x>>(5*8))] +
		pc[byte(x>>(6*8))] +
		pc[byte(x>>(7*8))])
}

10.作用域

声明将名字和程序实体关联起来,如一个函数或一个变量。声明的作用域是指用到声明函数或变量的源代码段。

  • PS: 不要将作用域和生命周期混淆。声明的作用域是声明的函数、变量在程序文本中出现的区域,是一个编译时属性;变量的生命周期是变量在程序执行期间能被程序的其他部分所引用的起止时间,它是一个运行时属性。

语法块(block)是由大括号围起来的一个语句序列,比如一个循环体或函数体。

  • 在语法块内部声明的变量对块外部不可见。块把声明包围起来,并且决定了它的可见性。我们可以把块的概念推广到其他没有显式包含在大括号中的声明代码,将其统称为词法块。
  • 包含了全部源代码的词法块,叫作全局块。每一个包,每一个文件,每一个for、if和switch语句,以及switch和select语句中的每一个条件,都是写在一个词法块里的。当然,显式写在大括号语法里的代码块也算是一个词法块。

一个程序中可以包含多个同名的声明,前提是它们在不同的词法块中。

  • 当编译器遇到对一个名称的引用时,它会从最内层的封闭词法块到全局块进行查找其声明。如果没有找到,将会报告“undeclared name”错误;如果在内层和外层块都存在这个声明,内层的声明将首先被找到。在这种情况下,内层声明将覆盖外部声明。