Go语言圣经 .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

常量、类型和函数

  • 常量: true, false, iota, nil
  • 类型: int, int8, int16, int32, int64, unit, unit8, unit16, unit32, unit64, unitptr, float32, float64, complex128, complex64, bool, byte, rune, string, error
  • 函数: make, len, cap, new, append, copy, close, delete, complex, real, imag, panic, recover

注意事项:

  1. 包名本身总是由小写组成的

  2. 实体第一个字母的大小写决定其可见性是否跨包。如果大写开头,它是导出的, 对外是可见和可访问的

    例如: fmt包中的Printf

声明

声明给一个程序实体命名,并且设定其部分或者全部属性。

4个主要的声明:

  • 变量(var)
  • 常量(const)
  • 类型(type)
  • 函数(func)

go程序存放在一个或者多个以.go结尾的文件里,每个文件以package声明开头,表明文件属于哪个包,package声明之后是import声明,然后是包级别的类型、变量、常量、函数的声明、不区分顺序。

package main

import "fmt"

func main() {
var f = "hello world"
fmt.Println(f)
}

变量

var声明创建一个具体类型的变量,通用形式:

var name type = expression

其中类型(type)和表达式(express)只能省略一个

  • 如果省略type,则变量的类型将由初始表达式决定

  • 如果省略expression, 则变量的初始值对应类型的零值,

    各个类型的零值:

    • 布尔 - false
    • 字符串 - ""
    • 接口和引用类型(slice、指针、map、通道、函数) - nil
    • 数组或结构体一样的符合类型 - 其所有元素或成员的零值

    例如:

    var s string
    fmt.Println(s) // ""

短变量声明

短变量声明的形式:

name := expression

指针

变量是存储值的地方。

指针的值是一个变量的地址。

如果一个变量的声明为var x int,

表达式 &x(x的地址) 获取一个指向整型变量的指针, 它的类型是整型指针*int

指针p指向的变量写作 *p

x := 1          // 
p := &x // x的地址赋值给p, p指向x
fmt.Println(*p) // p指向的变量,结果 "1"
*p = 2 // 将2赋值给p指向的变量
fmt.Println(x) // 结果 "2"

指针类型的零值是nil, 若p != nil,则说明p指向一个变量。

指针是可比较的。两个指针当且仅当指向同一个变量或者两者都是nil的情况下才相等。

var x y int
fmt.Println(&x==&x, &x==&y, &x==nil) // true false false

指针与变量的关系

  1. 声明变量:, var x int= 100, 编译器会在内存中留出一个唯一的地址单元来存储变量,如下,编译器地址为1004的内存单元留给变量,数值为100,将地址1004与该变量的值100关联起来

  1. 创建指针:变量x的地址是1004,是一个数字,地址的这个数字可以用另一个变量来保存它,假设这个变量为p,此时p未初始化,系统为它分配了空间,但值还不确定

  1. 初始化指针:将变量x的地址存储到变量p中,初始化后(p=&x),p指向x,称为一个指向x的指针。指针是一个变量,存储了另一个变量的地址。

  1. 声明指针:*p和x指向的是x的内容,p和&x指向的是x的地址

new 函数

另一种创建变量的方式是使用内置的new函数

表达式new(T)创建一个未命名的T类型变量,初始化为T的零值,并返回其地址(地址类型为*T)

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

变量的生命周期

变量的声明周期是根据其是否可达来确定的。

赋值

x = 1
*p = true
person.name = "bob"
count[x] = count[x] * scale
count[x] *= scale
x ++
x --

可赋值性

  • 隐式赋值

    medals := []string{"gold", "silver", "bronze"}
  • 显式赋值

    var a = 1

可赋值性的规则对于不同类型有着不同的要求:

  • 类型必须完全匹配

  • nil可以赋值给任务指针或者引用类型的变量

  • 常量有更加灵活的赋值规则

类型

类型声明语句:

type 类型名字 底层类型

对于每一个类型T,都有一个对应的类型转换操作T(x), 用于将x转换为T类型(如果T是指针类型,则可能需要用小括弧包装T,比如(*int)(0)

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类型的参数出现在了函数名(String)的前面,表示声明的是Celsius类型的一个叫做String的方法:

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

许多类型都会定义一个String方法,当使用fmt包打印的时候,会优先使用该类型的String方法

c := FToC(212.0)
fmt.Println(c.String()) // 100 ℃
fmt.Printf("%v\n", c) // 100 ℃
fmt.Printf("%s\n", c) // 100 ℃
fmt.Println(c) // 100 ℃
fmt.Printf("%g\n", c) // 100; 没有调用String方法
fmt.Println(float64(c)) // 100; 没有调用String方法

包和文件

  • 通常一个包所在目录路径的后缀是包的导入路径;例如: 包 gopl.io/ch1/hellowworld对应的目录路径是 $GOPATH/src/gopl.io/ch1/helloworld
  • 每个包对应一个独立的名字空间,例如,image包中的Decode函数和在unicode/utf16包中的Decode函数是不同的。要在外部引用该函数,必须显示的使用image.Decode或utf16.Decode
  • 包可以用过控制名字首字母来控制外部可见的信息。大写开头,名字是导出的,小写则是隐藏

在每个源文件的包声明前紧跟着的注释是包注释

如果包注释很大, 通常会放到一个独立的doc.go文件中

导入包

每个包都是有一个全局唯一的导入路径。导入语句中类似gopl.io/ch2/tempconv的字符串对应包的导入路径,

每个包还有一个包名,包名一般是短小的名字,一般包名跟导入路径最后一个字段相同,例如`gopl.io/ch2/tempconv的包名是tempconv

要使用gopl.io/ch2/tempconv包,需要先导入:

package main 

import (
"fmt"
"os"
"strconv"
"gopl.io/ch2/tempconv"
)

如果导入了一个包,但是又没有使用该包,将被当作编译错误来处理。

包的初始化

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

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

func f() int { return c + 1 }

作用域

var cwd string

func init() {
cwd, err := os.Getwd() // wrong !
if err!=nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}

这里的全局变量cwd没有被正确初始化,

可以调整为

var cwd string

func init() {
var err string
cwd, err := os.Getwd() // wrong !
if err!=nil {
log.Fatalf("os.Getwd failed: %v", err)
}
log.Printf("Working directory = %s", cwd)
}

单独声明err变量,避免使用:=短声明

练习

练习 2.1

向tempconv包添加类型、常量和函数用来处理Kelvin绝对温度的转换,Kelvin 绝对零度是−273.15°C,Kelvin绝对温度1K和摄氏度1°C的单位间隔是一样的。

练习 2.2

写一个通用的单位转换程序,用类似cf程序的方式从命令行读取参数,如果缺省的 话则是从标准输入读取参数,然后做类似Celsius和Fahrenheit的单位转换,长度单位可以对应英尺和米,重量单位可以对应磅和公斤等

练习 2.3

重写PopCount函数,用一个循环代替单一的表达式。比较两个版本的性能。(11.4节将展示如何系统地比较两个不同实现的性能。)

练习 2.4

用移位算法重写PopCount函数,每次测试最右边的1bit,然后统计总数。比较和查表算法的性能差异。

练习 2.5

表达式 x&(x-1) 用于将x的最低的一个非零的bit位清零。使用这个算法重写PopCount函数,然后比较性能。