[TOC]

0X00 Go语言基础之接口

Q: 在开发编程中您有可能遇到以下场景?

答: 我不关心变量是什么类型,只关心能调用它的什么方法,此时我们可以采用接口(Interface)类型进行解决相关问题。

1.接口类型

描述: 在Go语言中接口(interface)是一种类型,一种抽象的类型, 其定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

如 interface 是一组 method 的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

Tips: 为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

Q: 为什么要使用接口?
在我们编程过程中会经常遇到:

  • 比如一个网上商城可能使用支付宝、微信、银联等方式去在线支付,我们能不能把它们当成“支付方式”来处理呢?
  • 比如三角形,四边形,圆形都能计算周长和面积,我们能不能把它们当成“图形”来处理呢?
  • 比如销售、行政、程序员都能计算月薪,我们能不能把他们当成“员工”来处理呢?

例如:面的代码中定义了猫和狗,然后它们都会叫,你会发现main函数中明显有重复的代码,如果我们后续再加上猪、青蛙等动物的话,我们的代码还会一直重复下去。那我们能不能把它们当成“能叫的动物”来处理呢?

1
2
3
4
5
6
7
8
9
10
type Cat struct{}
func (c Cat) Say() string { return "喵喵喵" }
type Dog struct{}
func (d Dog) Say() string { return "汪汪汪" }
func main() {
c := Cat{}
fmt.Println("猫:", c.Say()) // 猫: 喵喵喵
d := Dog{}
fmt.Println("狗:", d.Say()) // 狗: 汪汪汪
}

Go语言中为了解决类似上面的问题,就设计了接口这个概念。接口区别于我们之前所有的具体类型,接口是一种抽象的类型。当你看到一个接口类型的值时,你不知道它是什么,唯一知道的是通过它的方法能做什么。


2.接口的定义

描述: Go语言提倡面向接口编程,每个接口由数个方法组成,接口的定义格式如下:

1
2
3
4
5
type 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2

}

参数说明:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

基础示例:

1
2
3
type writer interface{
Write([]byte) error
}

Tips: 当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

Tips :实现接口的条件, 即一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说接口就是一个需要实现的方法列表。


3.接口类型变量

Q: 那实现了接口有什么用呢?

答: 接口类型变量能够存储所有实现了该接口的实例,接口类型变量实际上你可以看做一个是一个合约。

基础示例:

1
2
// 定义一个接口类型writer的变量w。
var w writer // 声明一个writer类型的变量w

Tips: 观察下面的代码,体味此处_的妙用

1
2
3
4
5
6
// 摘自gin框架routergroup.go
type IRouter interface{ ... }

type RouterGroup struct { ... }

var _ IRouter = &RouterGroup{} // 确保RouterGroup实现了接口IRouter


示例演示:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import "fmt"

// 接口声明定义以及约定必须实现的方法
type speaker interface {
speak()
eat(string)
}

// 人结构体
type person struct{ name, language string }
func (p person) speak() {
fmt.Printf("我是人类,我说的是%v, 我叫%v\n", p.language, p.name)
}
func (p person) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

// 猫结构体
type cat struct{ name, language string }
func (c cat) speak() {
fmt.Printf("动物猫,说的是%v, 叫%v\n", c.language, c.name)
}
func (c cat) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

// 狗结构体
type dog struct{ name, language string }
func (d dog) speak() {
fmt.Printf("动物狗,说的是%v, 叫%v\n", d.language, d.name)
}
func (d dog) eat(food string) { fmt.Printf("喜欢的食物: %v\n", food) }

func talk(s speaker) {
s.speak()
}

// (1) 接口基础使用演示
func demo1() {
p := person{"WeiyiGeek", "汉语"}
c := cat{"小白", "喵喵 喵喵..."}
d := dog{"阿黄", "汪汪 汪汪...."}
talk(p)
talk(c)
talk(d)
}

// (2) 接口类型的使用(可看作一种合约)方法不带参数以及方法带有参数
func demo2() {
// 定义一个接口类型writer的变量w。
var s speaker
fmt.Printf("Type %T\n", s) // 动态类型

s = person{"接口类型-唯一", "汉语"} // 动态值
fmt.Printf("\nType %T\n", s) // 动态类型
s.speak()
s.eat("瓜果蔬菜")

s = cat{"接口类型-小白", "喵喵..."} // 动态值
fmt.Printf("\nType %T\n", s) // 动态类型
s.speak()
s.eat("fish")

s = dog{"接口类型-阿黄", "汪汪..."} // 动态值
fmt.Printf("\nType %T\n", s) // 动态类型
s.speak()
s.eat("bone")
}

func main() {
demo1()
fmt.Println()
demo2()
}

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
我是人类,我说的是汉语, 我叫WeiyiGeek
动物猫,说的是喵喵 喵喵..., 叫小白
动物狗,说的是汪汪 汪汪...., 叫阿黄

Type <nil>

Type main.person
我是人类,我说的是汉语, 我叫接口类型-唯一
喜欢的食物: 瓜果蔬菜

Type main.cat
动物猫,说的是喵喵..., 叫接口类型-小白
喜欢的食物: fish

Type main.dog
动物狗,说的是汪汪..., 叫接口类型-阿黄
喜欢的食物: bone

注意: 带参数和不带参数的函数,在接口中实现的不是同一个方法,所以当某个结构体中没有完全实现接口中的方法将会报错。


4.接口实现之值接收者和指针接收者

Q: 使用值接收者实现接口和使用指针接收者实现接口有什么区别呢?

  • 1) 值接收者实现接口: 结构体类型和结构体指针类型的变量都可以存储,由于因为Go语言中有对指针类型变量求值的语法糖,结构体指针变量内部会自动求值(取指针地址中存储的值)。
  • 2) 指针接收者实现接口: 只能存储结构体指针类型的变量

我们通过下面的例子进行演示:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package main

import (
"fmt"
)

// 接口类型声明
// (1) 值接收者实现接口
type Mover interface {
move()
}
type dog struct{}
func (d dog) move() { fmt.Println("值接收者实现接口 -> 狗...移动....") } // 关键点

// 使用值接收者实现接口之后,不管是dog结构体还是结构体指针*dog类型的变量都可以赋值给该接口变量.
func demo1() {
var m1 Mover
var d1 = dog{} // 值类型
m1 = d1 // m1可以接收dog类型的变量
fmt.Printf("Type : %#v \n", m1)
m1.move()

var d2 = &dog{} // 指针类型
m1 = d2 // x可以接收指针类型的(*dog)类型的变量
fmt.Printf("Type : %#v \n", m1)
m1.move()
}

// (2)指针接收者实现接口
type Runer interface{ run() }
type cat struct{}
func (c *cat) run() { fmt.Println("指针接收者实现接口 -> 猫...跑....") }
// 此时实现run接口的是*cat类型,所以不能给m1传入cat类型的c1,此时x只能存储*dog类型的值。
func demo2() {
var m1 Runer
var c1 = cat{}
//m1不可以接收dog类型的变量
// m1 = c1 // 报错信息: cannot use c1 (variable of type cat) as Runer value in assignment: missing method run (run has pointer receiver)compilerInvalidIfaceAssign
fmt.Printf("Type : %#v \n", c1)

//m1只能接收*dog类型的变量
var c2 = &cat{}
m1 = c2
fmt.Printf("Type : %#v \n", c2)
m1.run()
}
func main() {
demo1()
fmt.Println()
demo2()
}

执行结果:
1
2
3
4
5
6
7
8
Type : main.dog{} 
值接收者实现接口 -> 狗...移动....
Type : &main.dog{}
值接收者实现接口 -> 狗...移动....

Type : main.cat{}
Type : &main.cat{}
指针接收者实现接口 -> 猫...跑....

面试题: 注意这是一道你需要回答“能”或者“不能”的题!
问: 首先请观察下面的这段代码,然后请回答这段代码能不能通过编译?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

type People interface {
Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
if think == "man" {
talk = "你好,帅哥"
} else {
talk = "您好,美女"
}
return
}

func main() {
var peo People = Student{} // 此处为关键点
think := "woman"
fmt.Println(peo.Speak(think))
}

答案: 是不行会报 ./interface.go:21:6: cannot use Student{} (type Student) as type People in assignment: Student does not implement People (Speak method has pointer receiver) (exit status 2)错误,由于指针接收者实现接口必须是有指针类型的结构体实例化对象以及其包含的方法。


5.接口与类型

一个类型实现多个接口

描述: 一个结构体类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。

例如: 狗可以叫也可以动,我们就分别定义Sayer接口和Mover接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Sayer 接口
type Sayer interface { say() }
// Mover 接口
type Mover interface { move() }
// dog既可以实现Sayer接口,也可以实现Mover接口。
type dog struct { name string }
// 实现Sayer接口
func (d dog) say() { fmt.Printf("%s会叫 汪汪汪\n", d.name) }
// 实现Mover接口
func (d dog) move() { fmt.Printf("%s会动 \n", d.name) }

func main() {
var a = dog{name: "旺财"}
var x Sayer = a // 将dog类型赋予给Sayer接口类型的变量x,此时它可以调用say方法
var y Mover = a // 将dog类型赋予给Mover接口类型的变量y,此时它可以调用move方法
x.say() // 旺财会叫 汪汪汪
y.move() // 旺财会动
}


多个类型实现同一接口

描述: Go语言中不同的类型还可以实现同一接口,比如我们前面Person、Cat、Dog结构体类型中实现的Speak()方法。

例如:我们定义一个Mover接口,它要求结构体类型中必须有一个move方法, 如狗可以动,汽车也可以动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Mover 接口
type Mover interface { move() }
type dog struct { name string }
type car struct { brand string }
// dog类型实现Mover接口
func (d dog) move() { fmt.Printf("%s会跑\n", d.name) }
// car类型实现Mover接口
func (c car) move() { fmt.Printf("%s速度120迈\n", c.brand) }
func main() {
var x Mover
var a = dog{name: "旺财"}
x = a
x.move() // 旺财会跑
var b = car{brand: "保时捷"}
x = b
x.move() // 保时捷速度120迈
}

非常注意: 并且一个接口的方法,不一定需要由一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。

示例演示:

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
34
package main

import "fmt"

// 接口类型
type android interface {
telephone(int64)
music()
}

// 结构体声明 实现music方法
type mp3 struct{}
// 实现接口中的方法
func (m *mp3) music() { fmt.Println("播放音乐.....")}

// 结构体声明
type mobilephone struct {
production string
mp3 // 嵌入mp3结构体并拥有它的方法
}

// 实现接口中的方法
func (mb *mobilephone) telephone(number int64) { fmt.Printf("%v 手机, 正在拨打 %v 电话....\n", mb.production, number)}

func main() {
// android 接口类型
var a android
// 指针类型结构体变量mb
var mp = &mobilephone{production: "小米"}
a = mp
fmt.Printf("Type : %#v\n", a) // android 接口类型变量输出
a.telephone(10086)
a.music()
}

执行结果:
1
2
3
Type : &main.mobilephone{production:"小米", mp3:main.mp3{}}
小米 手机, 正在拨打 10086 电话....
播放音乐.....


6.接口嵌套

描述: 接口与接口间可以通过嵌套创造出新的接口,嵌套得到的接口的使用与普通接口一样,这里我们让cat实现animal接口。

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Sayer 接口
type Sayer interface {say()}
// Mover 接口
type Mover interface {move()}
// 接口嵌套
type animal interface {
Sayer
Mover
}
// cat 结构体
type cat struct {
name string
}
// 接口方法的实现
func (c cat) say() {fmt.Printf("%v 喵喵喵",c.name)}
func (c cat) move() {fmt.Printf("%v 猫会动",c.name)}
func main() {
var x animal
x = cat{name: "花花"}
x.move() //喵喵喵
x.say() //猫会动
}


7.空接口

空接口的定义
描述: 空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口, 该类型的变量可以存储任意类型的变量。他会在我们以后GO编程中常常出现。

例如:

1
2
3
4
5
6
// interface 是关键字,并不是类型。
// 方式1.但一般不会采用此种方式
var empty interface{}

// 方式2.我们可以直接忽略接口名称(空接口类型)
interface{}

空接口的应用

  • 1) 空接口作为函数的参数: 使用空接口实现可以接收任意类型的函数参数。
  • 2) 空接口作为map的值: 使用空接口实现可以保存任意值的字典。

示例演示:

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
package main

import "fmt"

// (1) 空接口作为函数参数
func showType(a interface{}) { fmt.Printf("参数类型:%T, 参数值:%v\n", a, a) }
func main() {
// (2) 空接口作为map的值
var m1 map[string]interface{} // 类似于Java中的 Map<String,Object> m1
m1 = make(map[string]interface{}) // 为Map申请一块内存空间
// 可以存储任意类型的值
m1["name"] = "WeiyiGeek"
m1["age"] = 20
m1["sex"] = true
m1["hobby"] = [...]string{"Computer", "NetSecurity", "Go语言编程学习"}

fmt.Printf("#空接口作为map的值\n%#v", m1)
fmt.Println(m1)

fmt.Printf("\n#空接口作为函数参数\n")
showType(nil)
showType([]byte{'a'})
showType(true)
showType(1024)
showType("我是一串字符串")
}

执行结果:
1
2
3
4
5
6
7
8
9
10
#空接口作为map的值 
map[string]interface {}{"age":20, "hobby":[3]string{"Computer", "NetSecurity", "Go语言编程学习"}, "name":"WeiyiGeek", "sex":true}
map[age:20 hobby:[Computer NetSecurity Go语言编程学习] name:WeiyiGeek sex:true]

#空接口作为函数参数
参数类型:<nil>, 参数值:<nil>
参数类型:[]uint8, 参数值:[97]
参数类型:bool, 参数值:true
参数类型:int, 参数值:1024
参数类型:string, 参数值:我是一串字符串

Tips : 因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛。

8.接口之类型断言

描述: 空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?

接口值
描述: 一个接口的值(简称接口值)是由一个具体类型具体类型的值两部分组成的,这两部分分别称为接口的动态类型动态值

我们来看一个具体的例子:

1
2
3
4
var w io.Writer
w = nil
w = os.Stdout
w = new(bytes.Buffer)

请看下图分解:
WeiyiGeek.动态类型与动态值

WeiyiGeek.动态类型与动态值

想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:x.(T),其中:

  • 1) x:表示类型为interface{}的变量
  • 2) T:表示断言x可能是的类型。

该语法返回两个参数,第一个参数是x转化为T类型后的变量第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败

示例演示

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package main

import "fmt"

// 示例1.采用if进行判断断言
func assert(x interface{}) {
v, ok := x.(string) // v 接受是string类型
if ok {
fmt.Printf("assert successful : %v, typeof %T\n", v, v)
} else {
fmt.Printf("assert failed 非 string 类型! : %v, typeof %T\n", x, x)
}
}
func demo1() {
var x interface{}
x = "WeiyiGeek"
assert(x) // assert successful : WeiyiGeek, typeof string
x = 1024
assert(x) // assert failed 非 string 类型! : 1024, typeof int
}

// 示例2.如果要断言多次就需要写多个if判断,这个时候我们可以使用switch语句来实现:
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
func demo2() {
var x interface{}
x = "i'm string"
justifyType(x)
x = 225
justifyType(x)
x = true
justifyType(x)
}

func main() {
demo1()
fmt.Println()
demo2()
}

执行结果:
1
2
3
4
5
6
assert successful : WeiyiGeek, typeof string
assert failed 非 string 类型! : 1024, typeof int

x is a string,value is i'm string
x is a int is 225
x is a bool is true

接口总结:
描述: 关于需要注意的是只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口,不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。


0x01 Go语言基础之包

描述: 在工程化的Go语言开发项目中,Go语言的源码复用是建立在包(package)基础之上的, 可以提高开发效率,使用其他开发者已经写好的代码(站在巨人的肩膀上)。

1.包的定义

描述: Go语言的包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、os、io等。

我们还可以根据自己的需要创建自己的包,一个包可以简单理解为一个存放.go文件的文件夹。

该文件夹下面的所有go文件都要在代码的第一行添加如下代码声明该文件归属的包。

1
2
3
4
package 包名

// graphical.go
package area


注意事项:

  • 1) 一个文件夹下面直接包含的文件只能归属一个package,同样一个package的文件不能在多个文件夹下。
  • 2) 包名可以不和文件夹的名字一样,但可以与.go文件名称一致,包名不能包含 - 符号并且严格按照变量命名的规则进行。
  • 3) 在导入包时应该从包的GOPATH/src后的路径开始写起其以/作为分隔符。
  • 4) 包名为main的包为应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。


2.包的导入

描述: 要在代码中引用其他包的内容,需要使用import关键字导入使用的包。具体语法如下:

1
import "包的路径"

示例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 单行导入的格式如下:
import "包1"
import "包2"

// 多行导入的格式如下:
import (
"包1"
"包2"
)

// 实际案例
import (
"fmt" // golang 内置包
"math/rand" // golang 内置包
"github.com/mattn/go-sqlite3" // golang 项目的工程组织规范
)

Tips: 使用go get导入github上的package, 以 go-sqlite3 为例,采用go get将package进行下载go get github.com/mattn/go-sqlite3,此时该包对应的物理路径是 $GOPATH/src/github.com/mattn/go-sqlite3, 此外在你也可以手动进行下载项目到$GOPATH/src

注意事项:

  • import导入语句通常放在文件开头包声明语句的下面。
  • 导入的包名需要使用双引号包裹起来,并且如果是多个包需要使用()进行包含。
  • 包名是从$GOPATH/src/后开始计算的,使用/进行路径分隔。
  • Go语言中禁止循环导入包。


3.包的可见性

描述: 如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识符必须是对外可见的(public)。

在Go语言中只需要将标识符的首字母大写就可以让标识符对外可见了。

举个例子, 我们定义一个包名为pkg2的包,代码如下:

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
package pkg2
import "fmt"
// # 包变量可见性探究

// 1.首字母小写,外部包不可见,只能在当前包内使用
var a = 100
// 2.首字母大写外部包可见,可在其他包中使用
const Mode = 1

// 3.首字母大写,外部包可见,可在其他包中使用
func Add(x, y int) int {
return x + y
}
// 4.首字母小写,外部包不可见,只能在当前包内使用
func age() {
var Age = 18 // 函数局部变量,外部包不可见,只能在当前函数内使用
fmt.Println(Age)
}
// 5.首字母小写,外部包不可见,只能在当前包内使用
type person struct {
name string
}
// 6.结构体中的字段名和接口中的方法名如果首字母都是大写,外部包可以访问这些字段和方法
type Student struct {
Name string //可在包外访问的方法
class string //仅限包内访问的字段
}
type Payer interface {
init() //仅限包内访问的方法
Pay() //可在包外访问的方法
}


4.自定义包名

描述: 在导入包名的时候我们还可以为导入的包设置别名。通常用于导入的包名太长或者导入的包名冲突的情况。

具体语法格式如下:

1
import 别名 "包的路径"

示例演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1.单行导入方式定义别名:
import "fmt"
import calc "github.com/studygo/pkg_test"
func main() {
fmt.Println(calc.Add(100, 200))
fmt.Println(calc.Mode)
}

// 2.多行导入方式定义别名:
import (
"fmt"
m "github.com/studygo/pkg_test"
)
func main() {
fmt.Println(m.Add(100, 200))
fmt.Println(m.Mode)
}


5.匿名导入包

描述: 如果只希望导入包,而不使用包内部的数据时,可以使用匿名导入包。

具体的格式如下:import _ "包的路径"

Tips: 匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

补充说明:
我们可以通过如下格式省略包前前缀,使用想过与同一个.go文件函数类似,但是常常不建议这样使用,可以会与当前文件中的某些相同方法的冲突。

具体的格式如下:import . "包的路径", 示例如下

1
2
3
4
5
6
7
import (
. "fmt"
)

func main() {
Println("我是fmt内置包的函数....")
}


6.包init()初始化函数

描述: 在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。

语法格式:

1
2
3
4
package custompackage
func init() {
fmt.Println("custompackage init() execute....")
}


init()函数执行顺序

  • 通常包初始化执行的顺序,如下图所示:

WeiyiGeek.包init函数执行时机

  • 但是实际项目中,Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
    在运行时,被最后导入的包会最先初始化并调用其init()函数,如下图示:

WeiyiGeek.多包中初始化函数执行顺序


注意事项:

  • 1) init() 函数没有参数也没有返回值。
  • 2) init() 函数在程序运行时自动被调用执行,不能在代码中主动调用它。


7.示例演示

工程项目结构:

1
2
3
4
5
6
7
8
// 自定义包的.go文件
➜ pkg pwd & ls
/home/weiyigeek/app/program/project/go/src/weiyigeek.top/custom/pkg/demo1 //(from $GOPATH))
demo1.go

// 调用自定义包的.go文件
➜ pkg ls
weiyigeek.top/studygo/Day04/packagemain.go

不多说上代码:

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
34
35
36
// # weiyigeek.top/custom/pkg/demo1/demo1.go #
package demo

import "fmt"

// 静态变量
const FLAG = true

// 基础变量
var Flag = 1

// 包初始化函数
func init() {
fmt.Println("This is a package demo ")
Flag = 1024 // 注意点
}

// 包函数
func Show() {
var msg = " 我是函数内部的变量 "
fmt.Printf("FLAG => %v, Flag => %v\nmsg:%v\n", FLAG, Flag, msg)
}

// 结构体
type Person struct{ Name string }

func (p Person) paly() {
fmt.Printf("%v 正在打游戏....", p.Name)
}

// 接口
type IPerson interface{ paly() }

func Exec(i IPerson) {
i.paly()
}

调用自定义包.go文件:

1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"fmt" // 不建议如. "fmt" 此使用
demo "weiyigeek.top/custom/pkg/demo1"
)

func main() {
fmt.Println(demo.Flag)
fmt.Println(demo.FLAG)
demo.Show()
demo.Exec(demo.Person{Name: "Weiyieek"})
}

执行结果:
1
2
3
4
5
6
This is a package demo 
1024
true
FLAG => true, Flag => 1024
msg: 我是函数内部的变量
Weiyieek 正在打游戏....


包总结:

  • 1) 我们可以在GOPATH/src路径下按照golang项目工程组织规范进行创建自定义包。
  • 2) 自定义包中需要外部调用访问的(如变量、常量、类型、函数等),必须首字母进行大写。
  • 3) 导入自定义包时我们可以自定义别名,但是需要满足命名规则已经不能与当前目录名称重名。
  • 4) 多个包都定义init()函数时,从调用的最后一包中递归向上执行输出。


错误说明:

  • Step 1.引入自定义包的时候报 go.mod file not found in current directory or any parent directory 错误.
    1
    2
    go: go.mod file not found in current directory or any parent directory; see 'go help modules' (exit status 1)
    no required module provides package weiyigeek.top/custom/pkg/demo1; to add it: go get weiyigeek.top/custom/pkg/demo1 (compile)go-staticcheck
    问题原因: go的环境设置问题,其次查看GOPATH目录中src为存放第三方go包。
    解决办法: go env -w GO111MODULE=auto


  • Step 2.main redeclared in this block (see details)compiler
    错误信息:
    1
    2
    main redeclared in this block (see details)compilerDuplicateDecl
    03datatype.go(151, 6): main redeclared in this block
    原因分析: 在学习study go时候会创建许多.go文件,并且在同一个目录下每个.go的文件里面都有package main,也就是main函数,这就是问题所在。
    解决办法: 同一个目录下面不能有多个package main,调整或者创建多个文件夹分别放入对应的文件下执行即可。