包、变量和函数

一、举个例子

现在我们来建立一个完整的程序 main.go(代码较长,可以快速跳过):

  1. // Golang程序入口的包名必须为 main
  2. package main // import "golang"
  3. // 导入其他地方的包,包通过 go mod 机制寻找
  4. import (
  5. "fmt"
  6. "golang/diy"
  7. )
  8. // init函数在main函数之前执行
  9. func init() {
  10. // 声明并初始化三个值
  11. var i, j, k = 1, 2, 3
  12. // 使用格式化包打印
  13. fmt.Println("init hello world")
  14. fmt.Println(i, j, k)
  15. }
  16. // 函数,两个数相加
  17. func sum(a, b int64) int64 {
  18. return a + b
  19. }
  20. // 程序入口必须为 main 函数
  21. func main() {
  22. // 未使用的变量,不允许声明
  23. //cannot := 6
  24. fmt.Println("hello world")
  25. // 定义基本数据类型
  26. p := true // bool
  27. a := 3 // int
  28. b := 6.0 // float64
  29. c := "hi" // string
  30. d := [3]string{"1", "2", "3"} // array,基本不用到
  31. e := []int64{1, 2, 3} // slice
  32. f := map[string]int64{"a": 3, "b": 4} // map
  33. fmt.Printf("type:%T:%v\n", p, p)
  34. fmt.Printf("type:%T:%v\n", a, a)
  35. fmt.Printf("type:%T:%v\n", b, b)
  36. fmt.Printf("type:%T:%v\n", c, c)
  37. fmt.Printf("type:%T:%v\n", d, d)
  38. fmt.Printf("type:%T:%v\n", e, e)
  39. fmt.Printf("type:%T:%v\n", f, f)
  40. // 切片放值
  41. e[0] = 9
  42. // 切片增加值
  43. e = append(e, 3)
  44. // 增加map键值
  45. f["f"] = 5
  46. // 查找map键值
  47. v, ok := f["f"]
  48. fmt.Println(v, ok)
  49. v, ok = f["ff"]
  50. fmt.Println(v, ok)
  51. // 判断语句
  52. if a > 0 {
  53. fmt.Println("a>0")
  54. } else {
  55. fmt.Println("a<=0")
  56. }
  57. // 死循环语句
  58. a = 0
  59. for {
  60. if a >= 10 {
  61. fmt.Println("out")
  62. // 退出循环
  63. break
  64. }
  65. a = a + 1
  66. if a > 5 {
  67. continue
  68. } else {
  69. fmt.Println(a)
  70. }
  71. }
  72. // 循环语句
  73. for i := 9; i <= 10; i++ {
  74. fmt.Printf("i=%d\n", i)
  75. }
  76. // 循环切片
  77. for k, v := range e {
  78. fmt.Println(k, v)
  79. }
  80. // 循环map
  81. for k, v := range f {
  82. fmt.Println(k, v)
  83. }
  84. // 定义 int64 变量
  85. var h, i int64 = 4, 6
  86. // 使用函数
  87. sum := sum(h, i)
  88. fmt.Printf("sum(h+i),h=%v,i=%v,%v\n", h, i, sum)
  89. // 新建结构体,值
  90. g := diy.Diy{
  91. A: 2,
  92. //b: 4.0, // 小写成员不能导出
  93. }
  94. // 打印类型,值
  95. fmt.Printf("type:%T:%v\n", g, g)
  96. // 小写方法不能导出
  97. //g.set(1,1)
  98. g.Set(1, 1)
  99. fmt.Printf("type:%T:%v\n", g, g) // 结构体值变化
  100. g.Set2(3, 3)
  101. fmt.Printf("type:%T:%v\n", g, g) // 结构体值未变化
  102. // 新建结构体,引用
  103. k := &diy.Diy{
  104. A: 2,
  105. }
  106. fmt.Printf("type:%T:%v\n", k, k)
  107. k.Set(1, 1)
  108. fmt.Printf("type:%T:%v\n", k, k) // 结构体值变化
  109. k.Set2(3, 3)
  110. fmt.Printf("type:%T:%v\n", k, k) // 结构体值未变化
  111. // 新建结构体,引用
  112. m := new(diy.Diy)
  113. m.A = 2
  114. fmt.Printf("type:%T:%v\n", m, m)
  115. s := make([]int64, 5)
  116. s1 := make([]int64, 0, 5)
  117. m1 := make(map[string]int64, 5)
  118. m2 := make(map[string]int64)
  119. fmt.Printf("%#v,cap:%#v,len:%#v\n", s, cap(s), len(s))
  120. fmt.Printf("%#v,cap:%#v,len:%#v\n", s1, cap(s1), len(s1))
  121. fmt.Printf("%#v,len:%#v\n", m1, len(m1))
  122. fmt.Printf("%#v,len:%#v\n", m2, len(m2))
  123. var ll []int64
  124. fmt.Printf("%#v\n", ll)
  125. ll = append(ll, 1)
  126. fmt.Printf("%#v\n", ll)
  127. ll = append(ll, 2, 3, 4, 5, 6)
  128. fmt.Printf("%#v\n", ll)
  129. ll = append(ll, []int64{7, 8, 9}...)
  130. fmt.Printf("%#v\n", ll)
  131. fmt.Println(ll[0:2])
  132. fmt.Println(ll[:2])
  133. fmt.Println(ll[0:])
  134. fmt.Println(ll[:])
  135. }

在相同目录下新建 diy 文件夹,文件下新建一个 diy.go 文件(名字任取):

  1. // 包名
  2. package diy
  3. // 结构体
  4. type Diy struct {
  5. A int64 // 大写导出成员
  6. b float64 // 小写不可以导出
  7. }
  8. // 引用结构体的方法,引用传递,会改变原有结构体的值
  9. func (diy *Diy) Set(a int64, b float64) {
  10. diy.A = a
  11. diy.b = b
  12. return
  13. }
  14. // 值结构体的方法,值传递,不会改变原有结构体的值
  15. func (diy Diy) Set2(a int64, b float64) {
  16. diy.A = a
  17. diy.b = b
  18. return
  19. }
  20. // 小写方法,不能导出
  21. func (diy Diy) set(a int64, b float64) {
  22. diy.A = a
  23. diy.b = b
  24. return
  25. }
  26. // 小写函数,不能导出,只能在同一包下使用
  27. func sum(a, b int64) int64 {
  28. return a + b
  29. }

进入文件所在目录,打开命令行终端,执行:

  1. go mod init
  2. go run main.go

会显示一些打印结果:

  1. init hello world
  2. 1 2 3
  3. hello world
  4. type:bool:true
  5. type:int:3
  6. type:float64:6
  7. type:string:hi
  8. type:[3]string:[1 2 3]
  9. type:[]int64:[1 2 3]
  10. type:map[string]int64:map[a:3 b:4]
  11. 5 true
  12. 0 false
  13. a>0
  14. 1
  15. 2
  16. 3
  17. 4
  18. 5
  19. out
  20. i=9
  21. i=10
  22. 0 9
  23. 1 2
  24. 2 3
  25. 3 3
  26. a 3
  27. b 4
  28. f 5
  29. sum(h+i),h=4,i=6,10
  30. type:diy.Diy:{2 0}
  31. type:diy.Diy:{1 1}
  32. type:diy.Diy:{1 1}
  33. type:*diy.Diy:&{2 0}
  34. type:*diy.Diy:&{1 1}
  35. type:*diy.Diy:&{1 1}
  36. type:*diy.Diy:&{2 0}
  37. []int64{0, 0, 0, 0, 0},cap:5,len:5
  38. []int64{},cap:5,len:0
  39. map[string]int64{},len:0
  40. map[string]int64{},len:0
  41. []int64(nil)
  42. []int64{1}
  43. []int64{1, 2, 3, 4, 5, 6}
  44. []int64{1, 2, 3, 4, 5, 6, 7, 8, 9}
  45. [1 2]
  46. [1 2]
  47. [1 2 3 4 5 6 7 8 9]
  48. [1 2 3 4 5 6 7 8 9]

我们看到 Golang 语言只有小括号和大括号,不需要使用逗号来分隔代码,只有一种循环 for

接下来我们会分析这个例子。

二、工程管理:包机制

每一个大型的软件工程项目,都需要进行工程管理。工程管理的一个环节就是代码层次的管理。

包,也称为库,如代码的一个包,代码的一个库,英文:Library 或者 Package。比如,我们常常听到某程序员说:嘿,X哥,我知道 Github 上有一个更好用的数据加密库,几千棵星呢。

在高级编程语言层次,也就是代码本身,各种语言发明了包(package)机制来更好的管理代码,将代码按功能分类归属于不同的包。

Golang 语言目前的包管理新机制叫 go mod

我们的项目结构是:

  1. ├── diy
  2. └── diy.go
  3. └── main.go

每一个 *.go 源码文件,必须属于一个包,假设包名叫 diy ,在代码最顶端必须有 package diy,在此之前不能有其他代码片段,如 diy/diy.go 文件中:

  1. // 包名
  2. package diy
  3. // 结构体
  4. type Diy struct {
  5. A int64 // 大写导出成员
  6. b float64 // 小写不可以导出
  7. }

作为执行入口的源码,则强制包名必须为 main,入口函数为 func main(),如 main.go 文件中:

  1. // Golang程序入口的包名必须为 main
  2. package main // import "golang"
  3. // 导入其他地方的包,包通过 go mod 机制寻找
  4. import (
  5. "fmt"
  6. "golang/diy"
  7. )

在入口文件 main.go 文件夹下执行以下命令:

  1. go mod init

该命令会解析 main.go 文件的第一行 package main // import "golang",注意注释 // 后面的 import "golang",会生成 go.mod 文件:

  1. module golang
  2. go 1.13

Golang 编译器会将这个项目认为是包 golang,这是整个项目最上层的包,而底下的文件夹 diy 作为 package diy,包名全路径就是 golang/diy

接着,main.go 为了导入包,使用 import ()

  1. // 导入其他地方的包,包通过 go mod 机制寻找
  2. import (
  3. "fmt"
  4. "golang/diy"
  5. )

可以看到导入了官方的包 fmt 和我们自已定义的包 golang/diy,官方的包会自动寻找到,不需要任何额外处理,而自己的包会在当前项目往下找。

在包 golang/diy 中,我们定义了一个结构体和函数:

  1. // 结构体
  2. type Diy struct {
  3. A int64 // 大写导出成员
  4. b float64 // 小写不可以导出
  5. }
  6. // 小写函数,不能导出,只能在同一包下使用
  7. func sum(a, b int64) int64 {
  8. return a + b
  9. }

对于包中小写的函数或者结构体中小写的字段,不能导出,其他包不能使用它,Golang 用它实现了私有或公有控制,毕竟有些包的内容我们不想在其他包中被使用,类似 Javaprivate 关键字。

结构体和函数会在后面的章节介绍,现在只需知道只有大写字母开头的结构体或函数,才能在其他包被人引用。

最后,Golang 的程序入口统一在包 main 中的 main 函数,执行程序时是从这里开始的:

  1. package main
  2. import "fmt"
  3. // init函数在main函数之前执行
  4. func init() {
  5. // 声明并初始化三个值
  6. var i, j, k = 1, 2, 3
  7. // 使用格式化包打印
  8. fmt.Println("init hello world")
  9. fmt.Println(i, j, k)
  10. }
  11. // 程序入口必须为 main 函数
  12. func main() {
  13. }

有个必须注意的事情是函数 init() 会在每个包被导入之前执行,如果导入了多个包,那么会根据包导入的顺序先后执行 init(),再回到执行函数 main()

三、变量

Golang语言可以先声明变量,再赋值,也可以直接创建一个带值的变量。如:

  1. // 声明并初始化三个值
  2. var i, j, k = 1, 2, 3
  3. // 声明后再赋值
  4. var i int64
  5. i = 3
  6. // 直接赋值,创建一个新的变量
  7. j := 5

可以看到 var i int64,数据类型是在变量的后面而不是前面,这是 Golang 语言与其他语言最大的区别之一。

同时,作为一门静态语言,Golang 在编译前还会检查哪些变量和包未被引用,强制禁止游离的变量和包,从而避免某些人类低级错误。如:

  1. package main
  2. func main(){
  3. a := 2
  4. }

如果执行将会报错:

  1. go run main.go
  2. ./main.go:26:2: cannot declared and not used

提示声明变量未使用,这是 Golang 语言与其他语言最大的区别之一。

变量定义后,如果没有赋值,那么存在默认值。我们也可以定义常量,只需加关键字 const,如:

  1. const s = 2

常量一旦定义就不能修改。

四、基本数据类型

我们再来看看基本的数据类型有那些:

  1. // 定义基本数据类型
  2. p := true // bool
  3. a := 3 // int
  4. b := 6.0 // float64
  5. c := "hi" // string
  6. d := [3]string{"1", "2", "3"} // array,基本不用到
  7. e := []int64{1, 2, 3} // slice
  8. f := map[string]int64{"a": 3, "b": 4} // map
  9. fmt.Printf("type:%T:%v\n", p, p)
  10. fmt.Printf("type:%T:%v\n", a, a)
  11. fmt.Printf("type:%T:%v\n", b, b)
  12. fmt.Printf("type:%T:%v\n", c, c)
  13. fmt.Printf("type:%T:%v\n", d, d)
  14. fmt.Printf("type:%T:%v\n", e, e)
  15. fmt.Printf("type:%T:%v\n", f, f)

输出:

  1. type:bool:true
  2. type:int:3
  3. type:float64:6
  4. type:string:hi
  5. type:[3]string:[1 2 3]
  6. type:[]int64:[1 2 3]
  7. type:map[string]int64:map[a:3 b:4]

数据类型基本有整数,浮点数,字符串,布尔值,数组,切片(slice) 和 字典(map) 。

  1. 布尔值:bool
  2. 整数:int (默认类型,一般视操作系统位数=int32或int64),int32int64
  3. 浮点数:float32float64(默认类型,更大的精度)
  4. 字符:string
  5. 数组,切片(可变长数组),字典(键值对结构)。

没声明具体变量类型的时候,会自动识别类型,把整数认为是 int 类型,把带小数点的认为是 float64 类型,如:

  1. a := 3 // int
  2. b := 6.0 // float64

所以当你需要使用确切的 int64float32 类型时,你需要这么做:

  1. var a int64 = 3
  2. var b float32 = 6.0

Golang 有数组类型的提供,但是一般不使用,因为数组不可变长,当你把数组大小定义好了,就再也无法变更大小。所以 Golang 语言造出了可变长数组:切片(slice),将数组的容量大小去掉就变成了切片。切片,可以像切东西一样。自动调整大小,可以切一部分,或者把两部分拼起来。

  1. d := [3]string{"1", "2", "3"} // array,基本不用到
  2. e := []int64{1, 2, 3} // slice

切片可以像数组一样按下标取值,放值,也可以追加值:

  1. // 切片放值
  2. e[0] = 9
  3. // 切片增加值
  4. e = append(e, 3)

切片追加一个值 3 进去需要使用 append 关键字,然后将结果再赋给自己本身,这是 Golang 语言与其他语言最大的区别之一,实际切片底层有个固定大小的数组,当数组容量不够时会生成一个新的更大的数组。

同时,因为日常开发中,我们经常将两个数据进行映射,类似于查字典一样,先查字母,再翻页。所以字典 map 开发使用频率极高,所以 Golang 自动提供了这一数据类型,这是 Golang 语言与其他语言最大的区别之一。

字典存储了一对对的键值:

  1. // 增加map键值
  2. f["f"] = 5
  3. // 查找map键值
  4. v, ok := f["f"]
  5. fmt.Println(v, ok)
  6. v, ok = f["ff"]
  7. fmt.Println(v, ok)

结构如 map[string]int64 表示键为字符串 string,值为整数 int64,然后你可以将 f = 5 这种关系进行绑定,需要时可以拿出键 f 对应的值。

五、slice 和 map 的特殊说明

键值结构字典:map 使用前必须初始化,如:

  1. m := map[string]int64{}
  2. m1 = make(map[string]int64)

如果不对字典进行初始化,作为引用类型,它是一个 nil 空引用,你使用空引用,往字典里添加键值对,将会报错。

而切片结构 slice 不需要初始化,因为添加值时是使用 append 操作,内部会自动初始化,如:

  1. var ll []int64
  2. fmt.Printf("%#v\n", ll)
  3. ll = append(ll, 1)
  4. fmt.Printf("%#v\n", ll)

打印:

  1. []int64(nil)
  2. []int64{1}

同时切片有以下特征:

  1. ll = append(ll, 2, 3, 4, 5, 6)
  2. fmt.Printf("%#v\n", ll)
  3. ll = append(ll, []int64{7, 8, 9}...)
  4. fmt.Printf("%#v\n", ll)
  5. fmt.Println(ll[0:2])
  6. fmt.Println(ll[:2])
  7. fmt.Println(ll[0:])
  8. fmt.Println(ll[:])

内置语法 append 可以传入多个值,将多个值追加进切片。并且可以将另外一个切片,如 []int64{7, 8, 9}...,用三个点表示遍历出里面的值,把一个切片中的值追加进另外一个切片。

在切片后面加三个点 ... 表示虚拟的创建若干变量,将切片里面的值赋予这些变量,再将变量传入函数。

我们取切片的值,除了可以通过下标取一个值,也可以取范围:[下标起始:下标截止(不包括取该下标的值)],如 [0:2],表示取出下标为 0和1 的值,总共有两个值,再比如 [0:4],表示取出下标为 0,1,2,3 的值。如果下标取值,下标超出实际容量,将会报错。

如果下标起始等于下标 0,那么可以省略,如 [:2],如果下标截止省略,如 [2:] 表示从下标 2 开始,取后面所有的值。这个表示 [:] 本身没有作用,它就表示切片本身。

六、函数

我们可以把经常使用的代码片段封装成一个函数,方便复用:

  1. // 函数,两个数相加
  2. func sum(a, b int64) int64 {
  3. return a + b
  4. }

Golang 定义函数使用的关键字是 func,后面带着函数名 sum(a, b int64) int64,表示函数 sum 传入两个 int64 整数ab,输出值也是一个 int64 整数。

使用时:

  1. // 定义 int64 变量
  2. var h, i int64 = 4, 6
  3. // 使用函数
  4. sum := sum(h, i)
  5. fmt.Printf("sum(h+i),h=%v,i=%v,%v\n", h, i, sum)

输出:

  1. sum(h+i),h=4,i=6,10

将函数外的变量 hi 传入函数 sum 作为参数,是一个值拷贝的过程,会拷贝 hi 的数据到参数 ab,这两个变量是函数 sum 内的局部变量,两个变量相加后返回求和结果。

就算函数里面改了局部变量的值,函数外的变量还是不变的,如:

  1. package main
  2. import "fmt"
  3. func changeTwo(a, b int) {
  4. a = 6
  5. b = 8
  6. }
  7. func main() {
  8. a, b := 1, 2
  9. fmt.Println(a, b)
  10. changeTwo(a, b)
  11. fmt.Println(a, b)
  12. }

输出:

  1. 1 2
  2. 1 2

变量是有作用域的,作用域主要被约束在各级大括号 {} 里面,所以函数里面的变量和函数体外的变量是没有关系的,互相独立。

我们还可以实现匿名的函数如:

  1. input := 2
  2. output := func(num int) int {
  3. num = num * 2
  4. return num
  5. }(input)
  6. fmt.Println(output)

打印出:

  1. 4

本来函数在外部是这样的:

  1. func A(num int) int {
  2. num = num * 2
  3. return num
  4. }

现在省略了函数名,定义后直接使用:

  1. output := func(num int) int {
  2. num = num * 2
  3. return num
  4. }(input)

input 是匿名函数的输入参数,匿名函数返回的值会赋予 output

七、其他

Golang 会智能进行变量分析。所以有一个叫变量逃逸的说法。

也就是变量被分配到堆里面 heap,还是栈里面 stack,主要取决于后期该变量会不会继续使用,如果不会,那么就分配到栈里,如果会,分配到堆里。

附录

代码下载: https://github.com/hunterhug/goa.c/tree/master/code/golang_learn