笔记声明
本人智商较低,好记性不如烂笔头。笔记记录了网上一套Golang免费视频教程的知识点,以供未来自己翻阅查看。不写源视频的名称和网站地址了,免得被说是广告!
20小时快速入门go语言视频 - Day2
一、基础函数
函数构成代码执行的逻辑结构。
1.1 定义格式
函数基本构成元素:由关键字 func
开头,紧接着是一个函数名 FuncName
,然后才是参数列表、返回值、函数体和返回语句。(参数列表、返回值、返回语句是可选的)
1.1.1 基本语法
示例:
func FuncName(/*参数列表*/) (o1 type1, o2 type2/*返回值:名称 数据类型*/) {
//函数体
//也就是这一块区域里的代码
return v1, v2 //返回值
}
1.1.2 定义格式的说明
1.1.2.1 func
func
是声明函数的关键字。
1.1.2.2 函数名称
函数名称采用驼峰格式
命名。Golang 规定:函数名首字母小写即对外部不可见,函数名首字母大写即对外部可见。
1.1.2.3 参数列表
紧跟在函数名称后面的一对小括号 ()
,不管有没有参数,这对小括号 ()
必须写上!
参数是可选的,可以是 0 个或者多个参数。参数格式为:参数名 数据类型
。
Golang 没有默认参数!
1.1.2.4 返回值
可选。如果这个函数没有返回值,就直接省略最后的返回参数以及 return
语句。
如果只有一个返回值且没有声明返回值的名称,那么就可以省略不写返回值名称以及括号,直接在函数体中使用 return
语句把这个只有一个的返回值给返回出去。但返回值的数据类型必须写!同理 1.1.1 的示例中,返回值声明了两个变量名(返回参数名称)o1 和 o2,这个也不是必须写的,可以只写数据类型而不写返回参数名称。
注意:Golang 没有默认值,如果函数没有返回值,Golang 是不会返回任何内容。在Golang中,函数没有返回值,那就是真的没有任何内容会返回出来了!
例:
如果有返回参数,函数体内必须写上 return
语句。
1.2 有参数无返回值函数(普通参数列表)
Golang 函数参数都属于必备参数,调用时必须传给它实参!
1.2.1 形参与实参
形参:定义函数时,函数的参数。
实参:调用函数时,传递过去的值。
//定义函数时,在函数第一个圆括号内定义的参数叫形成
//参数传递是单向传递,且只能由实参传递给形参,不能反过来
func MyTest(a int) {
fmt.Println("a=", a)
}
func main() {
//调用函数时,传递过去的参数叫实参
MyTest(111)
}
1.2.2 多个同类型参数的简写方式
这样写非常简洁,但参数一多就容易发生混乱,到底这个参数是什么类型。
//都同类型参数的时候,可以简写
func MyTest(a, b, c int) {
fmt.Printf("a=%d\tb=%d\tc=%d\t", a, b, c)
//a=111 b=222 c=333
}
func main() {
MyTest(111, 222, 333)
}
1.2.3 多个不同类型参数列表的写法
参数多起来,建议每个参数都写明数据类型,这样不会造成数据类型弄错或忘记。
func MyTest(name string, age int, salary float64) {
fmt.Printf("name=%s\tage=%d\tsalary=%f\n", name, age, salary)
//name=golang age=11 salary=1234567890.000000
}
func main() {
MyTest("golang", 11, 1234567890)
}
1.2.4 定义了几个参数,就必须传递几个值
如果定义的是普通参数列表,定义了几个参数,就必须传递几个值过去,否则就报错!
例如:
1.3 不定参数列表
如果不知道到底会有几个参数,那么就可以用不定参数列表。不定参数列表是指函数传入的参数数量不确定,它是通过切片来实现的,且只能放在形参中的最后一个!
1.3.1 不定参数类型的定义
首先将一个函数的形参定义为接受不定参数类型。
语法:func FuncName(varName ...Type)
。(...Type
其实就是一种语法糖,接收 0 个或者多个 Type 类型参数)
//...int就是不定参数类型,接收0个或者多个int参数
func myTest(a ...int) {
}
func main() {
myTest(111, 222)
}
1.3.2 不定参数的传参
此时,传递过去的实参可以是 0 个或者多个。
package main
import "fmt"
//...int就是不定参数类型,接收0个或者多个int参数
func myTest(a ...int) {
fmt.Println(a)
fmt.Printf("a type is : %T\n", a)
fmt.Println("--------------------------------")
}
func main() {
myTest()
myTest(1)
myTest(111, 222)
}
/*
运行结果:
[]
a type is : []int
--------------------------------
[1]
a type is : []int
--------------------------------
[111 222]
a type is : []int
--------------------------------
*/
可以看到,不定参数使用切片来实现。
1.3.3 不定参数必须放在形参的最后面
报错大意:...
在列表中只能作为最后一个参数。
1.3.4 不定实参传递给另一个函数的不定形参
不定参数作为实参,传递给另外一个函数的不定形参,另一个函数的形参必须也是一个不定参数类型。
1.3.4.1 基本示例
//参数类型被定义为不定参数
func f1(a ...int) {
f2(a...) //不定参数作为实参,传递给目标函数
}
//目标函数的形参也必须是不定参数类型
func f2(a ...int) {
fmt.Printf("a type is : %T, a=%v\n", a, a)
}
func main() {
f1(1, 2, 3, 4, 5)
}
/*
运行结果:
a type is : []int, a=[1 2 3 4 5]
*/
1.3.4.2 形参和实参都要加上...
目标函数的形参需要定义成不定参数类型,传递过去的实参后面也需要加上 ...
。
示例:
package main
import "fmt"
//想要接受不定参数,这里的形参也必须定义成不定参数
func myFunc(tmps ...int) {
fmt.Println(tmps)
}
func test(args ...int) {
//此处,想要把args全部传递给myFunc,需要这样写
myFunc(args...) //所有的都传过去了。...不能忘
}
func main() {
test(1, 2, 3, 4, 5)
}
/*
运行结果:
[1 2 3 4 5]
*/
注意:传递过去的时候,第 12 行 args 后面的 ...
不能忘。不定参数的类型是一个切片,不加 ...
就是一个数组类型。在 Golang 中,数组和切片是不同类型。
1.3.4.3 可以使用切片截取
如果只想传递某几个元素过去,可以使用切片截取某个片段。
示例:
package main
import "fmt"
//想要接受不定参数,这里的形参也必须定义成不定参数
func myFunc(tmps ...int) {
fmt.Println(tmps)
}
func test(args ...int) {
//只想把元素2,3,4传递给myFunc
myFunc(args[1:4]...) //...不能忘
}
func main() {
test(1, 2, 3, 4, 5)
}
/*
运行结果:
[2 3 4]
*/
1.4 无参有返回值
有返回值的函数,在函数体内必须通过 return
返回。
1.4.1 一个返回值
只有一个返回值,可以不写该返回值的名称。
func test() int { //不给返回值起名称
return 777
}
func main() {
fmt.Println(test()) //777
}
1.4.2 多个返回值
有多个返回值的时候,官方推荐给每个返回值起个名字。
下面这种写法是官方推荐:
func test() (name string, age int) { //每个返回值都起了名称
//函数中可以直接使用这及格返回值的名称
name = "golang"
age = 11
return //按照返回值参数定义的顺序,自动把name和age都给返回回去了
}
func main() {
fmt.Println(test())
//golang 11
}
Golang 是按照返回值参数定义的顺序进行返回。
使用 Goland 时,按住 Ctrl
,鼠标移动到函数上的时候,就能清晰看到那个函数返回了哪些信息,信息更加明显:
1.5 有参数有返回值
func getMax(v1 int, v2 int) (max int) {
switch {
case v1 > v2:
return v1
case v1 < v2:
return v2
default:
return v1
}
}
func main() {
fmt.Println(getMax(1, 1))
}
二、递归函数
2.1 普通函数调用流程
先调用后返回,先进后出(FILO
)。
示例:
func f1() {
f2()
fmt.Println("f1 finished.")
}
func f2() {
fmt.Println("f2 finished.")
}
func main() {
f1()
fmt.Println("main finished.")
}
/*
运行结果:
f2 finished.
f1 finished.
main finished.
*/
大致流程图:
2.2 递归函数的特性
递归函数可以直接或间接地调用自身,同样利用的是普通函数调用流程:先调用后返回、先进后出的特性。
递归函数通常有相同的结构:一个跳出条件和一个递归体。跳出条件就是根据传入的实参判断是否需要停止递归,递归体说白了就是包裹在函数体内的代码。
2.3 阶乘示例
func test(a int) int {
if a == 5 { //终止函数递归调用的条件
return a
} else {
return test(a+1) + a //没有终止的时候,执行的是test(a+1),函数没有执行完毕,是不会返回的
}
}
func main() {
fmt.Println(test(1))
}
2.4 处理大整数
int64
这个数据类型,运算阶乘的话,从 21 开始就会发生因 int64
容量不够而导致溢出的问题。
解决方案:使用 Golang 的内建包 math/big
中的 big.NewInt()
来存放大整数。
示例:
import (
"fmt"
"math/big"
)
func main() {
res := big.NewInt(1)
for i := 1; i <= 50; i++ {
bigI := big.NewInt(int64(i))
res = res.Mul(res, bigI)
}
fmt.Println(res)
}
/*
运行结果:
30414093201713378043612608166064768844377641568960512000000000000
*/
只要内存够大,尽管往大的数字去写。
三、函数类型
Golang中,函数也是一个数据类型:所有拥有相同的参数、相同的返回值的一种数据类型。
3.1 大致流程
3.1.1 给函数起一个别名。
使用关键字 type
声明一个函数类型。
语法:type 变量名 func(/*参数列表*/)
。例:type myFunc func(int, int) int
。
注意:type
定义的函数类型,func()
后面不能接返回值和大括号。
3.1.2 声明一个函数类型的变量名
例:var f myFunc
。声明一个变量,名称为 f,类型为 myFunc 函数类型。
3.1.3 赋值给一个变量
把某个同类型参数以及同类型返回值的函数体赋值给一个变量。
例:f = add
。注意:这个 add 的参数数量和参数类型必须和 f 函数的类型一模一样!
3.1.4 通过这个变量来调用函数
变量名+小括号+实参,实现调用。例:f(10, 20)
。
3.2 综合示例,演示多态的思想
func addTest(a int, b int) int {
return a + b
}
func minusTest(a, b int) int {
return a - b
}
//使用type给一个函数起一个名字,myAddFunc就变成了一个函数类型
//这个函数类型需要接收2个int类型的参数,返回值是1个int类型
type myAddFunc func(int, int) int
//注意:参数列表和返回值要跟addTest和minusTest一模一样
func main() {
var f1 myAddFunc //声明一个函数类型的变量名
f1 = addTest //是变量就可以赋值。赋值同类型的函数,相当于绑定
result := f1(1, 2) //等价于addTest(1, 2)
fmt.Println("result:", result) //result: 3
f2 := minusTest //自动推导成为函数类型
result = f2(10, 5) //等价于minusTest(10, 5)
fmt.Println("f2 result:", result) //f2 result: 5
fmt.Printf("f2 type is : %T\n", f2) //f2 type is : func(int, int) int
}
四、回调函数
所谓回调函数就是函数的参数里有一个参数是函数类型。
4.1 大致流程
1.定义一个函数类型。
2 函数形参指定为一个函数类型。定义另一个函数,函数的参数列表里放入刚才定义的函数类型。此时,这个函数就变成了回调函数。
3.在回调函数内部去调用其他不同的函数。
4.2 综合示例
//1.首先要定义一个函数类型
type myFunc func(int, int) int
//每个函数的实现
//参数数量和数据类型以及返回值类型,要跟myFunc函数类型一模一样
func add(a, b int) int {
return a + b
}
//每个函数的实现
//参数数量和数据类型以及返回值类型,要跟myFunc函数类型一模一样
func minus(a, b int) int {
return a - b
}
//每个函数的实现
//参数数量和数据类型以及返回值类型,要跟myFunc指向的函数类型一模一样
func mul(a, b int) int {
return a * b
}
//2.定义一个回调函数。函数的参数列表里,funcName形参被指定为myFunc函数类型,此时Calc就是一个回调函数
//多态:多种形态。调用同一个接口,可以实现不同的功能
func Calc(a, b int, funcName myFunc) (result int) {
//传过来就是add,minus,mul三个中的一个,funcName可以动态变成add或minus或mul,以实现不同的功能
//调用函数依然采用:函数名+小括号的方式调用,这里就可以根据实际传过来的函数名去动态调用,动态实现不同功能
result = funcName(a, b)
//return add(a, b) //这样就写死了,Calc这个函数只能实现add这个功能,而不能实现其他功能
return
}
func main() {
a := Calc(3, 4, add)
fmt.Println("add a:", a)
a = Calc(3, 4, minus)
fmt.Println("minus a:", a)
a = Calc(3, 4, mul)
fmt.Println("mul a:", a)
}
/*
运行结果:
add a: 7
minus a: -1
mul a: 12
*/
注意:关键字 type
定义的函数类型的数据类型是什么,其他函数也必须跟它一样,不然数据类型不匹配就会报错。
五、匿名函数与闭包
匿名函数是指不需要定义函数名称的一种函数实现方式。
在 Golang 中,所有的匿名函数(Golang 规范中称之为函数字面量)都是闭包。
闭包就是一个函数"捕获"了和它在同一作用域的其他常量和变量,闭包不关心"捕获"的常量和变量是否已经超出了作用域,只要闭包还在使用它们,这些常量和变量就还会存在。
闭包需要通过匿名函数来实现。
5.1 无参无返回值的匿名函数
func main() {
age := 11
name := "golang"
//定义匿名函数
f1 := func() { //自动推导成为函数类型
fmt.Println("name=", name)
fmt.Println("age=", age)
}
f1() //调用匿名函数
type myFunc func() //无参无返回值的函数类型
var f2 myFunc //声明变量f2为myFunc类型
f2 = f1 //都是无参无返回值的同类型函数,所以可以相互赋值
f2() //本质上就是在调用f1
//定义匿名函数且同时调用
func() {
fmt.Printf("name=%s\tage=%d\n", name, age)
}() //这个圆括号代表直接调用此匿名函数
}
/*
运行结果:
name= golang
age= 11
name= golang
age= 11
name=golang age=11
*/
5.2 带参数的匿名函数
func main() {
//匿名函数不需要写函数名称,这里只写上参数即可
//例如:func(i, j int) {/**/}
//自动类型推导
f1 := func(i, j int) {
fmt.Printf("i=%d,j=%d\n", i, j)
}
f1(1, 2)
func(i, j int) {
fmt.Printf("i=%d,j=%d\n", i, j)
}(10, 20) //简单粗暴,定义且直接调用,记得小括号中要给参数,因为定义了有2个参数
}
/*
运行结果:
i=1,j=2
i=10,j=20
*/
5.3 有参数有返回值
func main() {
x, y := func(i, j int) (max, min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return //定义了返回值,必须写上return
}(10, 20) //这里加小括号表示直接调用
fmt.Printf("max=%d,min=%d\n", x, y) //max=20,min=10
}
5.4 闭包捕获外部变量的特点
闭包是以 引用
的方式去捕获外部变量,用的是同一个变量。
示例:
func main() {
a := 10
b := "str bbb"
func() {
//闭包以引用方式捕获外部变量
a = 999 //直接赋值,这里改了会影响外部,外部也被修改了
b = "golang" //直接赋值,这里改了会影响外部,外部也被修改了
fmt.Printf("内部,a=%d,b=%s\n", a, b)
}() //()代表直接调用,否则就需要用一个变量去接收这个匿名函数
fmt.Printf("外部,a=%d,b=%s\n", a, b)
}
/*
运行结果:
内部,a=999,b=golang
外部,a=999,b=golang
*/
但是,如果在闭包内使用海象运算符 :=
声明了新变量,那么新变量就跟外部没有关系了。
示例:
func main() {
a := 10
b := "str bbb"
func() {
a := 999
b := "golang"
fmt.Printf("内部,a=%d,b=%s\n", a, b)
}() //()代表直接调用,否则就需要用一个变量去接收这个匿名函数
fmt.Printf("外部,a=%d,b=%s\n", a, b)
}
/*
运行结果:
内部,a=999,b=golang
外部,a=10,b=str bbb
*/
5.5 只要闭包还在使用,变量就还存在
闭包"捕获"外部变量并不会去关心它的作用域,只要闭包还在使用这些变量,变量就会存在。也就是说闭包有记忆变量的特性。
示例:
func test1() func() int { //返回值是一个匿名函数,该匿名函数有一个int类型的返回值
var x int
return func() int { //return语句后面接上这个匿名函数的函数体
x++
return x * x
}
}
func main() {
f := test1()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}
/*
1
4
9
16
*/
5.6 注意事项
1.匿名函数定义了必须要有变量去接收,或者在最后接圆括号 ()
直接调用它。
2.闭包不能在函数体外声明。
六、defer
defer
用于延迟一个函数、方法、或者当前所创建的匿名函数的执行。常用于函数执行完毕被释放前的一些清理工作。
defer
只能出现在函数或者方法的内部!
6.1 最基本的例子
func main() {
fmt.Println("aaa")
defer fmt.Println("bbb") //最后才执行的
fmt.Println("ccc")
}
/*
运行结果:
aaa
ccc
bbb
*/
6.2 多个defer的执行顺序
一个函数中有多个defer
语句,它们会以FILO
(先进后出)的顺序执行,哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
在错误代码那行,没有 defer
的演示:
下例中,有了 defer
就会把所有的信息都打印出来,但错误信息是最后一个显示:
func test1(x int) {
fmt.Println(100 / x)
}
func main() {
defer fmt.Println("aaaaaa")
defer fmt.Println("bbbbbb")
defer test1(0) //报错时,会放到最后一个被显示出来
defer fmt.Println("dddddd")
}
/*
运行结果:
dddddd
bbbbbb
aaaaaa
panic: runtime error: integer divide by zero
*/
在没有报错的情况下,defer
将按照先进后出的顺序执行:
6.2 defer和匿名函数结合使用
func main() {
a := 10
b := 20
defer func() {
fmt.Printf("defer语句,匿名函数内部:a=%d,b=%d\n", a, b)
}() //()代表匿名函数直接执行
a = 111
b = 222
fmt.Printf("外部:a=%d,b=%d\n", a, b)
}
/*
运行结果:
外部:a=111,b=222
defer语句,匿名函数内部:a=111,b=222
*/
在上例的基础中,给匿名函数传递两个实参:
func main() {
a := 10
b := 20
defer func(a, b int) {
fmt.Printf("匿名函数内部:a=%d,b=%d\n", a, b)
}(a, b) //()代表匿名函数直接执行
a = 111
b = 222
fmt.Printf("外部:a=%d,b=%d\n", a, b)
}
/*
运行结果:
外部:a=111,b=222
匿名函数内部:a=10,b=20
*/
七、获取命令行参数
无论在哪个系统中敲命令,或多或少都会遇到带着参数的命令。比如:ping www.google.com
,ping
是一个可执行程序 ,www.google.com 就是参数。
命令行以空格 space
区分各个参数,且以字符串方式传递!
7.1 如何实现
需要用到 Golang 自带的 os.Args
变量,官方对于这个变量的描述:
// Args hold the command-line arguments, starting with the program name.
var Args []string
Args 保持住命令行参数,从程序自身的名字开始。
7.2 最基本的例子
func main() {
cmdList := os.Args
fmt.Println("command line:", cmdList)
}
先编译,再运行可执行程序的效果:
第一个参数是程序自己本身的名称。
直接 go run
的方式只是人类自己看起来省力而已,Golang 底层依旧会先去编译并生成一个可执行程序,然后再运行这个可执行程序,如下图所示:
八、作用域
作用域就是:变量起作用的范围。
8.1 局部变量的特点
执行到定义变量的那句话,才开始分配内存空间,离开作用域被自动回收且被释放。
8.2 全局变量的特点
1.定义在函数外部的变量叫全局变量。
2.全局变量不能使用海象运算符 :=
声明。
3.const
声明的名称不再是全局变量,而是 常量
了,是不允许被修改的。
var a int //这个a是全局变量,可以给赋值
const b = 10 //const声明的b是常量了,不能给赋值
func main() {
a = 10
fmt.Println("a=", a)
b = 20 //cannot assign to b ===> 不能赋值给b
}
8.3 不同作用域同名变量
1.不同作用域允许定义同名变量。
2.使用变量的原则:就近原则。
package main
import "fmt"
var a int
func test() {
fmt.Printf("in test, a type is : %T\n", a)
}
func main() {
var a byte
fmt.Printf("in main, a type is : %T\n", a)
{
var a float64
fmt.Printf("in block, a type is : %T\n", a)
}
test()
}
/*
运行结果:
in main, a type is : uint8
in block, a type is : float64
in test, a type is : int
*/
找全局变量的时候,是
引用
全局变量:
package main
import "fmt"
var a int
func test() {
a = 10 //引用全局变量,会修改其结果
fmt.Println("a=", a)
}
func main() {
test()
fmt.Println("a=", a) //依然是引用,test函数中已改变了其值,所以也将引用改变后的值
}
/*
运行结果:
a= 10
a= 10
*/
九、工程管理
9.1 Go1.11 版本之前的工作区
Go1.11 版本之前,使用 GOPATH 来管理工程。
9.1.1 工作区介绍
Golang 代码必须放在工作区中。工作区是对应一个特定工程的目录,它应包含 3 个子目录:
1.src目录
用于以代码包的形式组织并保存 Go 源代码文件。(比如:.go
.c
.h
.s
…)
src用于包含所有的源代码,是 Go1.11 版本之前,命令行工具的强制规则。
2.pkg目录
用于存放由 go install
命令构建安装后的代码包(包含 go 库源码文件)的 .a
归档文件。
3.bin目录
与 pkg 目录类似,在通过 go install
命令完成安装后,保存由 Go 命令源码文件生成的可执行文件。
9.1.2 GOPATH设置
标准库中的包会在安装 Go 语言的位置找到。程序员自己创建的包会在 GOPATH
环境变量指定的目录中查找。
首先需要把工程目录的根路径加入到环境变量 GOPATH
中,这样才能构建这个工程。否则,即使处于同一工作目录(工作区),代码之间也无法通过绝对代码包路径完成调用。
src 这个目录是强制要求存在,而 pkg 和 bin 目录则无需手动创建,Go 命令行工具在构建过程中会自动创建这些目录。
特别注意:只有当环境变量 GOPATH
中只包含一个工作区的目录路径时,go install
命令才会把命令源码安装到当前工作区的 bin 目录下。如果环境变量 GOPATH
中包含多个工作区的路径,则必须设置环境变量 GOBIN
,否则执行 go install
命令就会失效。
9.1.3 分文件编程(同一个目录下)
1.多个源文件,必须放在 src
目录下。
2.设置 GOPATH
环境变量时,注意:别把 src
目录包含进去。
3.同一个目录,包名必须一样。
4.同一个目录,调用别的文件中的函数,无需加包名引用直接调用即可。
9.1.4 分文件编程(不同目录下)
1.依旧必须放在 src
目录下。
2.不同目录,包名不一样。
3.调用不同包里面的函数,格式:包名.函数名()
。
4.被调用的其他包里的函数名称首字母必须大写,否则对其他包不可见
9.1.5 包
9.1.5.1 自定义包
创建的自定义包最好放在GOPATH
的src
目录下(或者GOPATH
src
的某个子目录下)
在Golang中,代码包中的源码文件名是可以任意的,但这些源码文件都必须以包声明语句作为文件中非注释语句的第一行,每个包都对应一个独立的空间
例:
package calc
包中成员以名称首字母大小写决定访问权限:首字母大写可被包外访问,首字母小写仅包内成员可以访问。
注意:同一个目录(文件夹)中,每个 go 文件所声明的包名称必须一样!
9.1.5.2 给包起个别名
语法:import 新名字 原本的包名
。
//新名字+原本的名字
import myFMT "fmt"
func main() {
myFMT.Println("test info.")
}
9.1.5.3 忽略丢弃一个包
引入包的时候使用 _
,主要是为了让 Golang 自动调用那个包中的 init() 函数。
例如,我有这么一个项目结构:
hello/hello.go 中的代码如下:
package hello
import "fmt"
func init() {
fmt.Println("this message from hello/hello.go init() function.")
}
func Add(a, b int) int {
return a + b
}
func PrintHello() {
fmt.Println("hello message.")
}
我只想引入这个模块,但不想用这个模块的任何函数。那么可以在引入这个模块的时候,使用_
丢弃这个包。
main.go 中的代码:
package main
import _ "gitee.com/quanquan616/hello" //引入的同时,丢弃这个包,我只想做初始化使用
func main() {
}
/*
运行结果:
this message from hello/hello.go init() function.
Process finished with exit code 0
*/
编译通过,没有任何问题,而且也可以看到 Golang 自动调用了 hello/hello.go 中的 init() 函数。
9.1.5.4 下划线_
丢弃包的常用场景1
import "database/sql"
import _ "github.com/go-sql-driver/mysql"
第二个 import 就是不使用 mysql 包,将其丢弃,只是执行一下这个包的 init() 函数,把 mysql 的驱动注册到 sql 包里,然后程序里就可以使用 sql 包来访问 mysql 数据库了。
9.2 Go Module 模块管理
Go1.11 开始,官方推出并建议使用 Go Module
来管理工程。
详情使用:link
十、main()函数和init()函数
Golang 有两个保留函数:main()
和 init()
。这两个函数在定义时不能有任何的参数和返回值,Go 程序会自动调用这两个函数。
10.1 main()函数的特性
1.main() 函数是 Go 程序的唯一入口。
2.main() 函数只能应用于 main 包中,main 包中必须包含有且只有一个 main()
函数。
10.2 init()函数的特性
1.init() 函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等。
init() 函数能够应用于所有的包中,可选的,可写可不写。init() 函数会在程序执行开始的时候被调用。所有被编译器发现的 init() 函数,都会安排在 main() 函数之前执行。init() 函数通常用在设置包、初始化变量或者其他先行引导工作。
2.不同包的 init() 函数,按照包导入的依赖关系决定该初始化函数的执行顺序。
3.init() 函数不能被其他函数调用,而是在 main() 函数执行之前,自动被调用。
4.每个包可以拥有多个 init() 函数。
不推荐这么做,强烈建议每个文件只写一个 init() 函数。
10.3 包的其他特性
1.有时候一个包会被多个包同时导入,那么它只会被导入一次。(例如:很多包都会用到fmt包,但它只会被导入一次)
2.当一个包被导入时,如果该包还导入了其他的包,那么会先将其他包导入进来,然后再对这些包中的包级常量和变量进行初始化,紧接着执行 init() 函数(如果有)。等所有被导入的包都加载完毕了,就会对 main 包中的包级常量和变量进行初始化,然后执行 main 包中的 init() 函数(如果有),最后执行 main() 函数。
10.4 大致流程图
10.5 init()函数和main()函数的异同
共同点:两个函数在定义时不能有任何的参数和返回值,且由 Go 程序自动调用。
不同电:init() 函数可以应用于任意包中,且可以重复定义多个。main() 函数只能用于 main 包中,且只能定义一个。