《GO语言圣经》读书笔记 第二章 程序结构

  1. 命名规则

一个名字必须以一个字母(unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的。
Go语言中关键字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 等,主要对应内建的常量、函数和类型。这些内部预定义的名字并不是关键字,可以在定义中重新使用它们,在一些特殊场景中可以重新定义它们。
名字的开头字母的大小写决定了名字在包外的可见性。
包本身的名字一般总是用小写字母。
习惯上,Go语言程序员推荐使用驼峰式命名,当名字有几个单词组成的时候,优先使用大小写分隔,而不是优先用下划线分隔。而像ASCII和HTML这样的缩略词则避免使用大小写混合的下发,它们可以称为htmlEscape、HTMLEscape或escapeHTML,而不是escapeHtml。
声明语句定义了程序的各种实体对象以及部分或全部的属性。
Go语言主要有四种类型的声明语句:var、const、type 和 func,分别对应变量、常量、类型和函数实体对象的声明。

  1. 变量

变量声明: var 变量名字 类型 = 表达式
其中,“类型”或“=表达式”两个部分可以省略其中的一个。
可以在一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。
如果省略每个变量的类型,将可以声明多个类型不同的变量(类型有初始化表达式推导)。
例如:

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

简短变量声明被广泛用于大部分的局部变量的声明和初始化。
var形式的声明语句往往是用于需要显式指定变量类型的地方,或者是变量稍后会被重新赋值而初始值无关紧要的地方。
记住,“:=” 是一个变量声明语句,而"="是一个变量赋值操作。
简短变量声明左边的变量可能并不是全部都是刚刚声明的。
如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。
但是简短变量声明语句中必须至少要 声明一个新的变量,否则无法编译通过。

var x int

对于上面的变量声明语句,&x 表达式(取x变量的内存地址)将产生一个指向该整数变量的指针。
指针对应的数据类型是*int,被称之为“ 指向int类型的指针”
如果指针的名字为p,那么可以说"p指针指向变量x"或者说"p指针保存了x变量的内存地址",同时 *p 表达式对应p指针指向的变量的值。
一般 *p 表达式读取指针指向的变量的值。
任何类型的指针的零值都是nil.
在Go语言中,返回函数中局部变量的地址也是安全的,例如:

var p = f()   //每次调用f()函数都将会返回不同的结果

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

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

我们每次对一个变量取地址,或者复制指针,都是为原变量创建了新的别名。
表达式 new(T)将创建一个T类型的匿名变量,初始化为T类型的零值。然后返回变量地址,返回的指针类型为 *T .
new 类似是一种语法糖,而不是一个新的基础概念,如下面的两个函数是等价的:

func newInt() *int{
  return new(int) 
}

f unc newInt() *int {
  var dummy int 
  return &dummy
}

以下为关于指针的一些基础操作:

p := new(int) // p,*int 类型,指向匿名的 int 变量
fmt.Println(*p) //0
*p = 2      //设置int匿名变量的值为2
fmt.Println(*p) //2

每次调用new函数都会返回一个新的变量的地址。
** 但是如果两个类型都是空的,也就是说类型的大小是0,有可能有相同的地址**
Go语言垃圾回收器的实现思路:
从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的。也就是说,它是否存在并不会影响程序后续的计算结果。
因为一个变量的有效周期只取决于是否可达,因此一个循环迭代内部的局部变量的生命周期可能超出其局部作用域。同时,局部变量可能在函数返回之后依然存在,例如:

var global *int
func f() {
  var x int
   x = 1
  global = &x 
}

func g(){
 y := new(int)
*y = 1
}

f()函数里的x变量必须在 上分配,因为它在函数退出后依然可以通过包一级的global变量找到。虽然它是在函数内部定义的。用go语言的术语说:“x局部变量从函数f中逃逸了”

  1. 赋值

自增和自减是语句,而不是表达式,因此x=i++之类的表达式是错误的。
元组赋值
允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。
以下为几个实例:

//最大公约数
func gcd(x,y int) int {
  for y != 0 {
  x,y = y,x%y
}
return x
}

//计算斐波那契数组 (Fibonacci)
func fib(n int) int {
  x,y := 0,1
for i := 0; i < n ; i++{
  x,y = y ,x+y
}
return x
}

一个有多个返回值的函数调用出现在元组赋值右边的表达式中时(右边不能再有其他表达式),左边变量的数目必须和右边一致。

  1. 可赋值性

隐式赋值:函数调用会隐式地将调用参数的值赋值给函数的参数变量;
一个返回语句会隐式地将返回操作的值赋值给结果变量;
一个复合类型的字面量也会产生赋值行为。
不管是隐式地还是显示地赋值,在赋值语句左边的变量和右边最终的求到的值,必须有相同的数据类型。即:只有右边的值对于左边的变量是可赋值的,赋值语句才是允许的。
nil可以赋值给任何指针或引用类型的变量。
对于两个值是否可以用==或!=进行相等比较的能力,也和可赋值能力有关系:对于任何类型的值的相等比较,第二个值必须是对第一个值类型对应的变量是可赋值的,反之亦然。

  1. 类型

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

type  类型名称  底层类型

对于中文汉字,Unicode标志都作为小写字母处理,因此中文的命名默认不能导出。
对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转换为T类型(如果T是指针类型,可能会需要小括号包装T,比如(*int)(0))。
只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。
如果x是可以赋值给T类型的值,那么x必须也可以被转为T类型,但是一般没有这个必要。
底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。
命名类型还可以为该类型的值定义新的行为,这些行为表示为一组关联到该类型的函数的集合,我们称之为类型的方法集。

  1. 包和文件

每个包都对应一个独立的名字空间。
包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现的信息。
一个简单的规则:如果一个名字是大写字母开头的,那么该名字是导出的。
每个源文件都是以包的声明语句开始,用来指明包的名字。
每个源文件的包声明前紧跟着的注释是包注释。
一个包通常只有一个源文件有包注释(如果有多个包注释,目前的文档工具会根据源文件的名的先后顺序将它们链接成为一个包注释)。
在Go程序中,每个包都有一个全局的唯一的导入路径。
除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(并不要求包名是唯一的),包名在包的声明处指定,按照惯例,一个包的名字和包的导入路径的最后一个字段相同。
包的初始化过程:首先解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化。例如:

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

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

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

init 初始化函数除了不能被调用或引用外,其他行为和普通函数类型。
在每个文件中的init初始化函数 ,在程序开始执行时,按照它们声明的顺序被自动调用。
每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。
初始化工作是自下而上进行的,main()包最后被初始化。

//x&(x-1) 用于将x的最低的一个非零的bit位清零
func PopCountByClearing(x unint64) int {
  n := 0
  for x != 0 {
      x = x&(x-1)// clear rightmost non-zero bit
      n++
}
      return n
}

func PopCountByShifting(x uint64) int {
  n := 0
  for i := uint(0); i < 64; i++{
      if x&(1<<i) != 0 {
      n++
    }
}
     return n
}
  1. 作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。
声明语句的作用域是指源码中可以有效使用这个名字的范围。
不要将作用域和生命周期混为一谈。
声明语句的作用域对应的是一个源代码的文本区域。它是一个编译时的属性。
一个变量的生命周期视锥程序运行时变量存在的有效时间段。在此时间区域内它可以被程序的其他部分引用,是一个运行时的概念。
声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。
声明语句对应的词法域决定了作用域范围的大小,对于内置的类型、函数和常量,比如 int,len和true等是全局作用域的,因此可以在整个程序中使用。
任何函数外部(也就是包级词法域)声明的名字,可以在同一个包中的任何源文件访问的。
对于导入的包,例如fmt,则是对应源文件级的作用域。因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文件导入的包。
一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。
当编译器遇到一个名字引用时,如果它看起来像一个声明,它首先从最内层的词法域向全局的作用域查找。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部声明的名字无法被访问。
并不是所有的词法域都显示地对应到由花括弧包含的语句,还有一些隐含的规则:例如for循环语句创建了两个词法域:for循环体部分词法域(显示);另一个隐式的部分是循环的初始化部分。
Go语言的习惯是在if中处理错误后直接返回,不通过else块,这样可以确保正常执行的语句不需要代码缩进。

推荐阅读更多精彩内容