Golang学习之函数与工程管理

一、基础函数

函数构成代码执行的逻辑结构。

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中,函数没有返回值,那就是真的没有任何内容会返回出来了!
例:

package main

import "fmt"

func myFunc() {
	a := 6
	fmt.Println("a = ", a)
}

func main() {
	myFunc()
}

如果有返回参数,函数体内必须写上 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 不定参数必须放在形参的最后面

//注意:不定参数,一定(只能)放在形参中的最后一个参数
func myFunc03(args ...int, a int) {

}

func main() {
	myFunc02(111, 1, 2, 3)
}

/*
运行结果:
# command-line-arguments
.\32_不定参数类型.go:34:15: syntax error: cannot use ... with non-final parameter args
*/

报错大意:... 在列表中只能作为最后一个参数。

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 maxAndMin(a int, b int) (max int, min int) {
	if a > b {
		max = a
		min = b
	} else {
		max = b
		min = a
	}

	return
}

func main() {
	max, min := maxAndMin(10, 20)
	fmt.Printf("max=%d, min=%d\n", max, min)

	a, _ := maxAndMin(10, 20)
	fmt.Printf("max=%d", a)
}

二、递归函数

2.1 普通函数调用流程

先调用后返回,先进后出(FILO)。
示例:

func funcc(c int) {
	fmt.Println("c = ", c)
}

func funcb(b int) {
	funcc(b - 1)
	fmt.Println("b = ", b)
}

func funca(a int) {
	funcb(a - 1)
	fmt.Println("a = ", a)
}

func main() {
	funca(3) //函数调用
	fmt.Println("main")
}

/*
c =  1
b =  2
a =  3
main
*/

2.2 递归函数的特性

递归函数可以直接或间接地调用自身,同样利用的是普通函数调用流程:先调用后返回、先进后出的特性。
递归函数通常有相同的结构:一个跳出条件和一个递归体。跳出条件就是根据传入的实参判断是否需要停止递归,递归体说白了就是包裹在函数体内的代码。

2.3 递归函数调用流程

func test(a int) {
	if a == 1 { //函数终止调用的条件,非常重要
		fmt.Println("a = ", a)
		return //终止函数调用
	}

	//函数调用自身
	test(a - 1)
	fmt.Println("a = ", a)
}

func main() {
	test(3)
	fmt.Println("main")
}

2.4 数字累加

示例:

//实现1+2+3+...100

func test01() (sum int) {
	for i := 1; i <= 100; i++ {
		sum += i
	}

	return
}

func test02(i int) (sum int) {
	if i == 100 {
		return 100
	}

	return i + test02(i+1)
}

func main() {
	var sum int
	sum = test01() //普通方法实现
	fmt.Println("sum = ", sum)
	sum = test02(1) //递归实现
	fmt.Println("sum = ", sum)
}

三、函数类型

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 综合示例,演示多态的思想

package main

import "fmt"

func Add(a, b int) int {
	return a + b
}

func Minus(a, b int) int {
	return a - b
}

//函数也是一种数据类型,通过type给一个函数类型起名
//FuncType它是一个函数类型
type FuncType func(int, int) int //没有函数名字,没有{}

func main() {
	var result int
	result = Add(1, 1) //传统调用方式
	fmt.Println("result = ", result)

	//声明一个函数类型的变量,变量名叫fTest
	var fTest FuncType
	fTest = Add            //是变量就可以赋值
	result = fTest(10, 20) //等价于Add(10,20)
	fmt.Println("result2 = ", result)

	fTest = Minus
	result = fTest(10, 5) //等价于Minus(10, 5)
	fmt.Println("result3 = ", result)
}

/*
运行结果:
result =  2
result2 =  30
result3 =  5
*/

四、回调函数

所谓回调函数就是函数的参数里有一个参数是函数类型。

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的执行顺序的演示:

package main

import "fmt"

func test(a int) {
	x := 100 / a
	fmt.Println("x = ", x)
}

func main() {

	/**
	fmt.Println("aaaaaaaaaaa")
	fmt.Println("bbbbbbbbbbb")

	//调用一个函数,导致内存出问题
	test(0)

	fmt.Println("ccccccccccc")

	运行结果:
	aaaaaaaaaaa
	bbbbbbbbbbb
	panic: runtime error: integer divide by zero
	**/

	/**
	defer fmt.Println("aaaaaaaaaaa")
	defer fmt.Println("bbbbbbbbbbb")

	//调用一个函数,导致内存出问题
	test(0)

	defer fmt.Println("ccccccccccc") //这个defer还没有进栈中,所以不会打印出这行的内容,只会打印上面两行的内容

	运行结果:
	bbbbbbbbbbb
	aaaaaaaaaaa
	panic: runtime error: integer divide by zero
	**/

	/*
		defer fmt.Println("aaaaaaaaaaa")
		defer fmt.Println("bbbbbbbbbbb")

		//调用一个函数,导致内存出问题
		defer test(0) //报错时,会放到最后一个被显示出来

		defer fmt.Println("ccccccccccc")


			运行结果:
			ccccccccccc
			bbbbbbbbbbb
			aaaaaaaaaaa
			panic: runtime error: integer divide by zero
	*/

	defer fmt.Println("aaaaaaaaaaa")
	defer fmt.Println("bbbbbbbbbbb")
	defer test(1) //在没有报错的情况下,defer 将按照先进后出的顺序执行
	defer fmt.Println("ccccccccccc")

	/*运行结果:
	ccccccccccc
	x =  100
	bbbbbbbbbbb
	aaaaaaaaaaa
	*/
}

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.comping 是一个可执行程序 ,www.google.com 就是参数。
命令行以空格 space 区分各个参数,且以字符串方式传递!

7.1 如何实现

需要用到 Golang 自带的 os.Args 变量,按F2即可看到官方对于这个变量的描述:

// Args hold the command-line arguments, starting with the program name.
var Args []string

Args 保持住命令行参数,从程序自身的名字开始。

7.2 最基本的例子

package main

import (
	"fmt"
	"os"
)

func main() {
	list := os.Args

	n := len(list)
	fmt.Println("n = ", n)

	for i := 0; i < len(list); i++ {
		fmt.Printf("list[%d] = %s\n", i, list[i])
	}

	fmt.Println("++++++++++++++++")

	for i, data := range list {
		fmt.Printf("list[%d] = %s\n", i, data)
	}
}

/*
go run 48_获取命令行参数.go 4 42
n =  3
list[0] = C:\Users\ADMINI~1\AppData\Local\Temp\go-build346730226\b001\exe\48_获取命令行参数.exe
list[1] = 4
list[2] = 42
++++++++++++++++
list[0] = C:\Users\ADMINI~1\AppData\Local\Temp\go-build346730226\b001\exe\48_获取命令行参数.exe
list[1] = 4
list[2] = 42
*/

第一个参数是程序自己本身的名称。
直接 go run 的方式只是人类自己看起来省力而已,Golang 底层依旧会先去编译并生成一个可执行程序,然后再运行这个可执行程序,如下图所示:

八、作用域

作用域就是:变量起作用的范围。

8.1 局部变量的特点

执行到定义变量的那句话,才开始分配内存空间,离开作用域被自动回收且被释放。

package main //必须

import "fmt"

func test() {
	a := 10
	fmt.Println("a = ", a)
}

func main() {
	//定义在{}里面的变量就是局部变量,只能在{}里面有效
	//执行到定义变量那句话,才开始分配空间,离开作用域自动释放
	//作用域,变量其作用的范围

	//a = 111
	{
		i := 10 //块级作用域,出了这对大括号的范围,i就会被自动释放
		fmt.Println("i = ", i)
	}
	//i = 111

	if flag := 3; flag == 3 { //flag同样也是块级作用域内的变量,出了if块被自动回收并释放
		fmt.Println("flag = ", flag)
	}

	flag = 4
}

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 自定义包

创建的自定义包最好放在GOPATHsrc目录下(或者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
main.go

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 init()函数和main()函数的异同

共同点:两个函数在定义时不能有任何的参数和返回值,且由 Go 程序自动调用。
不同电:init() 函数可以应用于任意包中,且可以重复定义多个。main() 函数只能用于 main 包中,且只能定义一个。


本文作者: iceH
本文链接: http://www.secice.cn/p/8fdd12c0
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!