这一节,主要跟着专栏第一章补漏一些必备的基础概念,内容概览:

  • Go 中的源码文件分类
  • 关于命令源码文件、库源码文件你漏掉了那些知识点?
  • 程序实体那些事儿

源码文件

Go 中源码文件分为三种:命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。

命令源码文件

  • 命令源码文件的用途是什么,怎样编写他?

命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。

如果一个源码文件声明属于 main 包,并且包含一个无参数声明、无返回值声明的 main 函数,那么就是 命令源码文件,就像这样:

1
2
3
4
5
6
7
  package main

  import "fmt"

  func main() {
      fmt.Println("hello world")
  }
  • 命令源码文件运行:

    • 可以通过 go run 命令直接启动
    • 通过构建 go build 或 安装 go install 生成与其对应的可执行文件来运行,可执行文件名一般会与该命令源码文件的直接父目录同名。

Note:

  • 对于一个独立的程序来说,命令源码文件永远只会也只能有一个
  • 如果有与命令源码文件同包的源码文件,那么它们也应该声明属于 main 包

知识精讲:命令源码文件接收参数

  • 命令源码文件接收参数主要使用:标准库 flag 包完成,有关 flag 包的详细用法请点击 Here

库源码文件

库源码文件不能直接被运行,它仅用于存放程序实体。

程序实体是什么呢? Go 语言中,它是变量、常量、函数、结构体和接口的统称。程序实体的名字被统称为标识符。

关于代码包导入的几个思考题?

  • 代码包的导入路径总会与其所在目录的相当路径一致吗?

答案:自己编码一试便知

总结:请记住,源码文件所在的目录相对于 src 目录的相对路径就是它的代码包导入路径,而实际使用其程序实体时给定的 限定符 要与它声明所属的代码包名称对应

  • 对于程序实体,除了 包级私有公开,还有其他的访问权限规则吗?

答案:是肯定的,不然作者问题干啥哩… 那么,这个访问规则是什么呢?

在 Go 1.5 及后续版本中,我们可以通过创建 internal 代码包让一些程序实体仅仅能被当前模块中的其他代码引用。这被称为 Go 程序实体的第三种访问权限:模块级私有

具体规则是,internal 代码包中声明的公开程序实体仅能被该代码包的直接父级包及其子包中的代码引用。 当然,引用前需要先导入这个 internal 包,对于其他代码包,导入该 internal 包都是非法的,无法通过编译。

具体规则是怎么样的可以自己编码测试。

思考题

这次的思考题都是关于代码包导入的,如下。

  1. 如果你需要导入两个代码包,而这两个代码包的导入路径的最后一级是相同的,比如:deb/lib/flagflag,那会产生冲突吗?
  2. 如果会产生冲突,那么怎么解决这种冲突,有几种方式?

先来看问题 1:

  • import 后路径最后一级相同,不一定会冲突,分为两种情况:

    • 如果文件夹下文件声明的包名相同,则肯定冲突,会报错 redeclared
    • 如果文件夹下文件声明的包名不同,也不会冲突。

再来看问题 2:

  • 如果冲突,可能采用的解决方式:

    • 给包设置别名,调用的时候来区分开不同的 package, 比如:

      1
      2
      3
      4
      
      import (
      "flag"
      lflag "lib/flag"
      )
      • 导入的点操作,import(. “lib/flag”)。这样就可以直接调用 “lib/flag” 下面的函数而不用再 flag.funcname 的方式调用。
      • 如果只是想引入某包并没有在代码中实际调用则可以这么处理来避免冲突: import(_ “lib/flag”)
      • 像第一问一样采取不同的包名声明,毕竟包名不一定要和文件夹名一样

      Note: 推荐给包设定别名的方式

      总结

      • 同一个文件夹下,包的声明语句需要相同,代表同一个包。
      • 包名可以同目录名,也可以自定义(推荐同目录名),调用包内的程序实体所用的限定符始终为:包声明语句所定义的名称,而包导入路径为该目录的相对路径
      • 模块级私有新姿势:定义为 internal 包

      测试源码文件

      ……

      程序实体

      程序实体那些事儿(一)

      关键点:

      • ※ 变量重声明

      知识扩展:

      Go 语言的类型推断可以带来哪些好处?

      • 使得程序更加灵活,使得代码重构更加容易。 (PS: 比如这段代码 var v = getV() | v := getV() 声明的变量 v 的类型可以在初始化时由其他程序动态的确定, 这里由 getV() 函数的返回值类型来推导 v 的类型)。
      • 还有一个就是少敲几次键盘哇

      变量的重声明是什么意思?

      • 先明确一下:变量重声明涉及的是 短变量声明。 代码块指的是:由花括号 { } 括起来的区域
      • 变量重声明的前提条件:
      • 由于变量的类型在其初始化时就已经确定了,所以对它再次声明时赋予的类型必须与其原本的类型相同,否则会产生编译错误。
      • 变量的重声明只可能发生在某一个代码块中。如果与当前的变量重名的是外层代码块中的变量,那么就是另外一种含义了,我在下一篇文章中会讲到。
      • 变量的重声明只有在使用短变量声明时才会发生,否则也无法通过编译。如果要在此处声明全新的变量,那么就应该使用包含关键字 var 的声明语句,但是这时就不能与同一个代码块中的任何变量有重名了。
      • 被“声明并赋值”的变量必须是多个,并且其中至少有一个是新的变量。这时我们才可以说对其中的旧变量进行了重声明。

      思考题:

      如果与当前的变量重名的是外层代码块中的变量,那么这意味着什么…

      程序实体那些事儿(二)

      关键点:

      • ※ 可重名变量
      • 代码块:{ }
      • 作用域:一个程序实体的作用域总是会被限制在某个代码块中,而这个作用域最大的用处,就是对程序实体的访问权限的控制
      • Go 程序实体的访问权限:包级私有、模块级私有和公开

      思考题:

      如果一个变量与其外层代码块中的变量重名会出现什么状况? Demo 如下:

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      package main
      
      import "fmt"
      
      var block = "package"       // 作用域:main 包
      
      func main() {
      block := "function"     // 作用域:main 函数
      {
      block := "inner"    // 作用域:{} 内
      fmt.Printf("The block is %s.\n", block)
      }
      fmt.Printf("The block is %s.\n", block)
      }
  • 问题一:该源码文件中的代码能通过编译吗?如果不能,原因是什么?如果能,运行它后会打印出什么内容?

    • 首先是能通过编译, 自己跑一下这个 Demo 即可知道输出什么
  • 问题二:Demo 中有三处声明了 block,那么我引用变量的时候到底用的是哪一个? —— 即 Go 语言查找(代表了程序实体的)标识符的过程

    • 首先,代码引用变量的时候总会最优先查找当前代码块中的那个变量。注意,这里的“当前代码块”仅仅是引用变量的代码所在的那个代码块,并不包含任何子代码块。
    • 其次,如果当前代码块中没有声明以此为名的变量,那么程序会沿着代码块的嵌套关系,从直接包含当前代码块的那个代码块开始(向上查找:查找过程从小作用域查到大作用域),一层一层地查找。
    • 一般情况下,程序会一直查到当前代码包代表的那层代码块。如果仍然找不到,那么 Go 语言的编译器就会报错了。

小结:

  • 如果代码包导入语句写为 import . xxx 的形式,那么就会导致该 xxx 包中公开的程序实体被当作当前源码文件中的代码,视为当前代码包中的程序实体。(Note: 如果当前代码包有与 xxx 包重名的变量,则会报错:redeclared)

知识扩展:

思考问题:不同代码块中的 重名变量变量重声明 中的变量区别到底在哪儿?

  • 变量重声明中的变量一定是在某一个代码块内的。注意,这里的“某一个代码块内”并不包含它的任何子代码块,否则就变成了“多个代码块之间”。而可重名变量指的正是在多个代码块之间的由相同的标识符代表的变量。
  • 变量重声明是对同一个变量的多次声明,这里的变量只有一个。而可重名变量中涉及的变量肯定是有多个的。
  • ※ 不论对变量重声明多少次,其类型必须始终一致,具体遵从它第一次被声明时给定的类型。而可重名变量之间不存在类似的限制,它们的类型可以是任意的。
  • 如果可重名变量所在的代码块之间存在直接或间接的嵌套关系,那么它们之间一定会存在“屏蔽”的现象。但是这种现象绝对不会在变量重声明的场景下出现。

总结:

  • 通过代码块和作用域可以更精细化的控制 Go 程序实体(变量、常量、函数、结构体和接口的统称)的访问权限
  • 在具有嵌套关系的不同代码块中存在重名的变量时,可能会发生“屏蔽”的现象。这样你在不同代码块中引用到变量很可能是不同的。具体的鉴别方式需要参考 Go 语言查找(代表了程序实体的)标识符的过程。
  • 可重名变量在不同代码块中会出现屏蔽现象 ——即作用域不一样,会出现屏蔽现象。
  • 可重名变量可以各有各的类型 ——即在使用前应该做好类型检查
  • 变量重声明时的类型必须和原类型保持一致

程序实体那些事儿(三)

关键点:

  • ※ 判断变量的类型
  • ※ 别名类型声明和类型再定义

知识扩展:

你认为类型转化规则中有哪些值得注意的地方?

  • 首先,对于整数类型值、整数常量之间的类型转化,原则上只要源值在目标类型的可表示范围内就是合法的。

    • 需要注意:源整数类型的可表示范围较大,而目标类型的可表示范围较小的情况

    • 当整数值得类型的有效范围由宽变窄是,只需要在补码形式下截掉一定数量的高位二进制数即可。

    • 当把一个浮点数类型的值转化为整数类型值时,前者的小数部分会被全部截掉。

    • 关于原码、反码、补码的基本概念补充

    • 数据在计算机内部是以补码的形式存储的

    • 数据分为有符号数和无符号数:无符号数都是正数,由十进制直接转换到二进制直接存储(其实也是该十进制的补码)即可。 有符号数用在计算机内部是以补码的形式储存的。( 正数的最高位是符号位 0, 负数的最高位是符号位 1。 对于正数: 反码==补码==原码。 对于负数: 反码==除符号位以外的各位取反。补码=反码+1)

    • 原码:原码就是符号位加上真值的绝对值, 即用最左边第一位表示符号, 其余位表示值

      1
      2
      
      原码: [+1]0000 0001
      原码:[-1]1000 0001
    • 反码:正数的反码就是其本身;负数的反码就是在原码的基础上保持符号位不变,其余各位取反

      1
      2
      
      原码:[+1]0000 0001 反码:0000 0001
      原码:[-1]1000 0001 反码:1111 1110
    • 补码:正数的补码就是其本身;负数的补码就是在原码的基础上保持符号为不变,其余各位取反再加 1 ——即 反码 +1

      1
      2
      
      原码:[+1]0000 0001 反码:0000 0001 补码:0000 0001
      原码:[-1]1000 0001 反码:1111 1110 补码:1111 1111
  • 第二,虽然直接把一个 整数值 转换为一个 string 类型 的值是可行的,但值得关注的是,被转化的整数值应该可以代表一个有效的 Unicode 代码点,否则转化的结果将会是 "�" (仅由高亮的问号组成的字符串值)。

    • 字符 "�" 的 Unicode 代码点是 U+FFFD。它是 Unicode 标准中定义的 Replacement Character,专用于替换那些未知的、不被认可的以及无法展示的字符。
    1
    2
    3
    4
    5
    6
    
    func main() {
        i := -1
        println(string(i))
        // output:
        // �
    }

    你先要了解的是,一个值在从 string 类型向 []byte 类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节

    什么是别名类型,什么是潜在类型?

    我们可以用关键字 type 声明自定义的各种类型。当然了,这些类型必须在 Go 语言基本类型和高级类型的范畴之内。

    • 别名类型
    1
    2
    3
    4
    5
    6
    7
    
    type MyString = string
    
    // Note:
    //
    //  - MyString 是 string 类型的别名类型。顾名思义,别名类型与其源类型的区别恐怕只在名称上,它们是完全相同的
    //  - 源类型与别名类型是一对概念,是两个对立的称呼。别名类型主要是为了代码重构而存在的。
    //  - Go 语言内建的基本类型就存在两个别名类型byte  unit8 的别名类型rune  int32 的别名类型
  • 类型再定义

1
2
3
4
  type MyString string // 注意:这里没有等号

  // Note:
  //  - MyString2  string 这里属于两个不同的类型MyString2 是一个新的类型不同于其他任何类型属于类型再定义 string 类型就被称为 MyString2 类型的潜在类型(潜在类型的含义是某个类型在本质上是哪个类型或者那个类型的集合)

Demo 说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  package main

  import "reflect"

  // MyString 定义为 string 类型的别名
  type MyString = string

  // MyString2 属于类型再定义,其潜在类型为 string 类型
  type MyString2 string

  func main() {
      printInvalidUnicode()
      testTypeAlias()
  }

  func printInvalidUnicode() {
      i := -1
      println(string(i))
      // output:
      // �
  }

  func testTypeAlias() {
      var s MyString
      var ss MyString2

      println(reflect.TypeOf(s).String())
      println(reflect.TypeOf(ss).String())

      // output:
      // string
      // main.MyString2
  }

Note: 即使两个类型的潜在类型相同,它们的值之间也不能进行判等或比较,它们的变量之间也不能赋值。

总结:

  • 请记住,一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型):

    • interface{}:即不包含任何方法定义的空的接口类型
    • struct{}: 即不包含任何字段和方法的,空的结构体类型
    • []string{}: 即空的切片类型
    • map[int]string{}: 空的字典类型
  • 类型断言的时候,结果最好赋值给两个变量 ——即带 ok 变量的写法:v, ok := interface{}(x).(T) 。另外还要保证被判断的变量是接口类型且不能为 nil

  • Go 中,不同类型之间不能相互赋值,不存在隐式的类型转化,必须显示强转

  • type newType = Type 定义类型 Type 的别名 newType; type newType Type 定义新类型 newType