Go笔记之程序结构

这篇的内容关于Go的程序结构,包含命名声明变量赋值类型作用域

命名

  • 25个关键字:

    1
    2
    3
    4
    5
    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余个预定义名:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    内建常量: 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
  • 命名的作用域(程序实体的访问权限)

    1
    2
    3
    模块级私有 -> 命名于函数内:函数作用域,仅函数内部可以访问;
    包级私有 -> 命名于函数外:包级作用域,可在当前包的所有文件中访问;
    公开 -> 命名于函数外且首字母大写:可被导出,允许被外部包所访问;
  • 命名要求简短且遵循驼峰命名原则

  • 程序实体
    程序实体即:变量、常量、函数、结构体、接口的统称

  • 标识符
    标识符即:声明或定义程序实体的变量名,或者可以说是程序实体的名字(即变量的命名)

  • 限定符
    限定符即:代码包被导入使用时,调用包内方法的前缀名,格式[限定符].[包内方法名]
    限定符即导入代码包的包名称,并非包所在路径目录名称;

声明

  • 四种声明关键字
    1
    2
    3
    4
    var     // 变量
    const // 常量
    type // 类型
    func // 函数

变量

  • 变量声明的一般语法:

    1
    2
    3
    4
    5
    6
    // 常见的几种变量声明方式
    var 变量名 类型 = 表达式
    var 变量名 = 表达式 // 若没有类型,则通过表达式推导变量类型
    var 变量名 类型 // 若没有表达式,则将对应类型零值初始化给该变量
    var 变量1, 变量2, 变量3 类型 // 没有表达式的多变量声明
    var 变量1, 变量2, 变量3 = 表达式1, 表达式2, 表达式3 // 没有表达式的多变量声明
  • 零值初始化机制:

    1
    2
    3
    4
    5
    6
    7
    1. 数值类变量零值为0
    2. 布尔类变量零值为false
    3. 字符串类变量零值为""(空字符串)
    4. 接口或引用类型(slice、指针、mapchanfunc)变量对应零值为nil
    5. 数组或结构体等聚合类型变量的零值是该类型下元素具体类型的对应零值

    由于零值初始化机制使得Go语言不存在未初始化变量;
  • 简短变量声明
    简短变量声明的类型确定是在编译期完成的,因此不会对程序的运行效率产生影响;
    简短变量声明语句用于声明和初始化局部变量,并且仅允许在函数内部使用;
    变量名 := 表达式,注::=是一个变量声明语句
    Q: 关于变量声明,什么时候用var?什么时候使用简短变量声明?
    A: var往往用于需要显示指定变量类型的地方及变量稍后会重新赋值而初始值无关紧要的地方,而简短变量声明则不要求严格指定类型可接收根据表达式推导且声明当即要能表达式赋值的地方;

    1
    2
    3
    4
    5
    i := 100                  // an int
    var boiling float64 = 100 // a float64
    var names []string
    var err error
    var p Point

简短变量声明中的变量名必须保证在之前没有被声明过(单一变量时),当简短变量声明多个变量时,变量中至少要保证有一个在之前未声明过,其他已声明的变量仅做赋值操作;
简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量。

  • 变量的重声明

    1
    2
    3
    4
    5
    6
    7
    > 1. 由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误;

    > 2. 变量的重声明只可能发生在某一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么就会在当前代码块中声明一个新的变量,而非外层变量的重声明;

    > 3. 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字var的声明语句,但是这时就不能与同一个代码块中的任何变量有重名了。

    > 4. 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。
  • 指针
    如果用 var x int 声明语句声明一个x变量,那么&x表达式(取x变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是int,指针被称之为“指向int类型的指针”。如果指针名字为p,那么可以说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。同时p表达式对应p指针指向的变量的值。一般p表达式读取指针指向的变量的值,这里为int类型的值,同时因为p对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值;

    1
    2
    3
    4
    5
    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"
  • new函数
    调用内建函数new也可以创建变量;表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T。
    new并不是关键字,而是自定义函数,所以要注意new内建函数名称被重新定义为其他类型的情况;

    1
    2
    3
    4
    p := new(int)   // p, *int 类型, 指向匿名的 int 变量
    fmt.Println(*p) // "0"
    *p = 2 // 设置 int 匿名变量的值为 2
    fmt.Println(*p) // "2"
  • 变量的生命周期
    变量的生命周期指的是在程序运行期间变量有效存在的时间段。对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。函数的参数变量和返回值变量都是局部变量。它们在函数每次被调用的时候创建。
    因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在。

赋值

  • 元组赋值
    当出现以下三种类型赋值语句时,右边的变量都会出现ok布尔变量值,用以判断赋值是否成功;
    1
    2
    3
    v, ok = m[key]             // map lookup映射查询
    v, ok = x.(T) // type assertion类型断言
    v, ok = <-ch // channel receive通道接收

和变量声明一样,我们可以用下划线空白标识符_来丢弃不需要的值;

  • 可赋值性
    不管是隐式还是显式地赋值,在赋值语句左边的变量和右边最终的求到的值必须有相同的数据类型。更直白地说,只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。

类型

  • 类型声明
    类型声明语句 type 类型名字 底层类型 一般出现在包级作用域,若类型名首字母大写则可被包外使用;
    例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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) }

其中以类型名(x)方式可以将传参x转换成与类型名一致的类型,但是前提是x参数的类型要与类型名的底层类型保持一致;

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

1
2
3
4
5
6
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"!

包与文件

  • Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重用。
  • 每个包都对应一个独立的名字空间。
  • 如果导入了一个包,但是又没有使用该包将被当作一个编译错误处理
  • 包的初始化
    1
    func init() { /* ... */ }

作用域

声明语句的作用域是指源代码中可以有效使用这个名字的范围;

  • 作用域与生命周期
    声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。
    变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

  • 句法块
    句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样;
    句法块内部声明的名字是无法被外部块访问。即决定块内变量的作用域;

  • 词法块
    词法块即是没有显式地使用花括号包裹的申明代码;
    全局词法块是对于全局源代码来说存在的一个整体词法块;
    其他的词法块如:包级词法块for语句词法块if语句词法块switch语句词法块switch分支词法块select分支词法块显式词法块(即花括弧内的句法块)