标题无意冒犯,就是觉得这个广告挺好玩的
上面这张思维导图喜欢就拿走
目录
Go语言的包结构
在我们的每一段程序的开头, 都要使用package
关键字来声明这段代码属于哪个包, 但是关于Go语言的包管理机制我们还没有探讨过, 因此在这一节我们讲一下关于Go语言的包(package):
3.1 包引用
Go语言的代码一般位于$GOPATH/src
下, Go语言的标准包可以直接引用, 第三方的包和自己定义的包必须放在这个目录下才可以引用.
比如: 如果我们要使用Go语言官方的strings
包中的函数, 那么我们需要在代码中这样去引用:
import (
"strings"
)
但是比如我们要引用Go语言中比较好用的ORM包: gorm
的时候, 作为一个第三方的包, 我们应该这样去引用:
import (
"github.com/jinzhu/gorm"
)
当然, 这都是使用绝对路径的引用方式, 我们还可以使用相对路径的引用方式: 使用./
, ../
这种方式去进行包的引用.
在引用之后, 我们就可以使用包名.
的方式去使用包中的方法和变量了, 比如使用Go语言中的sort
包的Ints
方法:
sort.Ints(integerSlice)
但是有的时候我们引用的包名会有冲突的状况, 又或者是这个包的名字太长, 写起来不太方便, 总之各种原因让我们需要给他一个别名, 那么这个时候就应该使用这样的方式去进行引用:
import (
S "sort" // import nickName "packagePath"
)
// ...
S.Ints(integerSlice)
// ...
或者可以直接将包中的内容和当前代码做一个合并, 让我们可以直接调用:
import (
. "sort"
)
// ...
Ints(integerSlice)
// ...
如果我们只是想调用一个包的初始化函数, 而并不会使用其中的任何一个其他的函数或者变量, 我们应该怎么办呢? 相信大家在建立数据库连接时都是这样去做的, 为了应对这样的情况, 在Go语言下可以使用下划线_
来导入一个包:
import (
_ "packagePath" // 仅执行初始化函数 init()
)
3.2 包加载
我们现在知道了该如何对包进行引用, 也就是对于import
关键字的用法. 那么Go语言是以一种怎样的顺序去加载包的呢? 在这一节就来讲一下这个问题:
所以我先画了一个图在这里, 从这里大家可以看到包加载的基本过程:
- 在加载时, 如果有引入其他的包, 那么进入该包的加载过程
- 如果对于引入的包的加载完毕了, 那么开始进行常量的声明
- 常量的声明后是变量的声明
- 如果该包有init()函数, 那么执行init()函数
- 如果该包是main包, 那么在init()之后执行main()函数
其实加载的主要思想很简单, 就是记住加载的顺序就好了.
3.3 封装
封装是面向对象编程思想中很重要的一部分, 封装的具体含义指隐藏具体的实现细节, 仅暴露出可以调用的接口和方法.
具体关于封装的细节就不详谈了, 因为一般教科书或者网络上关于封装的描述都是基于类的, 而Go语言的面向对象的实现不太一样, 因此不再赘述, 这个小节只关心如何实现封装, 也就是如何实现字段和方法对外的隐藏.
在Go语言中区分是否暴露给其他包的标志是名称是否以大写字母开头, 如果是以大写字母开头的类型, 变量, 常量或者函数, 是可以在当该包被其他包导入的时被暴露的. 反之则只能够在该包中进行使用.
同样的, 一个类型上绑定的方法, 如果是以大写字母开头, 则是可以被调用的. 类型中的字段也是一样的.
最后, Go语言中有大量的内置的包和第三方开发的包, 请大家不要重复造轮子, 灵活使用别人的轮子会事半功倍.
变量和数据类型
一般来说,强类型语言中的变量是盒子,而弱类型语言中的变量则多数是标签。
因为强类型,所以一个变量能够分配的内存空间相对来说是便于计算的,因此语言内部会在内存中为你的变量开辟一块内存空间,而这个内存空间的地址会被映射为变量名,而内存空间内存放的数据是什么并不被关心,就像是一个盒子,里面放了不同的数据。
4.1 变量的声明:
在Go语言中,声明一个新变量的方式有两种:var
关键字和:=
运算符。
1. 使用var关键字:
var hello string // 声明一个变量hello,类型为string,默认值为string的零值,也就是空字符串。
var hello string = "Hello, golang!"
var hello, world string = "Hello", "World" // 可以同时声明多个变量并且初始化
var hello = "Hello, golang!" // 根据右侧值的类型来推断变量类型
2. 使用:=运算符
:=
运算符用于简化新变量声明的步骤,不希望程序员每次用到新变量都去使用var
来声明。因此:=
运算符只能用于新变量的声明。
hello := "Hello, golang!" // 等价为 var hello = "Hello, golang",也会进行类型的推断
var hello string; hello := "Hello, golang!" // 会报错,即使声明的变量未曾使用过,也不是一个新变量,因此不能使用 := 运算符
hello, err := someFunc() // 便捷地接收函数返回的结果,前提也是一定是新的变量名
4.2 Go语言的数据类型:
Go语言的类型系统非常的简单且多变,不同的组合形式可以产生成百上千上万的类型,也就是说,通过不同方式的组合,Go语言中可以有无数个类型。
1. 基本的数据类型
类型名称 | 标识符 | 描述 |
---|---|---|
布尔类型 | bool | 布尔值仅有true 和false 两种值 |
字符串类型 | string | 在Go语言中的字符串是不可变的常量 |
字节类型 | byte | 字节类型,实质上是uint8 |
Unicode字符类型 | rune | 实质上是int32类型 |
有符号整数类型 | int, int8, int16, int32, int64 | int 是基于具体架构的类型,后面的数字代表在内存中所占的位数 |
无符号整数类型 | uint, uint8, uint16, uint32, uint64 | uint 是基于具体架构的类型,后面的数字代表在内存中所占的位数 |
浮点数类型 | float32, float64, complex64, complex128 | float32 和float64 都是遵循IEEE-754标准,数字依然表示位数,而complex64 和complex128 分别表示32位和64位的实数和虚数 |
2. 派生类型
2.1 指针类型(pointer)
Go语言是内存安全的,但是开放了灵活的指针给程序员使用,又不涉及复杂的指针运算。
在Go语言中的类型都有对应的指针类型,以*
为标识符,例如:
var stringPointer *string
var intPointer *int
指针类型的变量存储着内存地址。可以使用&
取地址符来获取一个变量的地址。
Go语言中的空指针为nil
。
2.2 数组类型(array)
Go语言中的任意类型都可以有相应确定长度的数组类型:
var fourStringArray [4]string // 一个长度为4的字符串数组
var int2dArray [4][5]int // 一个长度为4的 长度为5的int数组 的数组,也就是一个二维数组
值得注意的是:这里的数组名不代表数组的开始地址!,需要使用&
来取得首地址,也不建议对它进行指针运算。
定长的原因很简单:可以很好的确定内存空间的大小。
2.3 结构化类型(struct)
结构体,没有人会陌生,如果这不是你接触的第一门语言的话。
结构化的自定义类型被广泛应用于组织和传递数据,以及Go语言的面向对象也是以结构体作为支撑的。
例子:
var myName struct {
FirstName string // 字段名在前,类型名称在后
LastName string
} // 声明了一个结构体变量
// 定义一个新的结构体类型
type Name struct {
FirstName string
LastName string
}
// 如何初始化一个结构体变量
myName := Name {FirstName: "Shiina", LastName: "Mashiro"} // 最后不需要添加逗号
myName := Name {
FirstName: "Shiina",
LastName: "Mashiro, // 需要添加逗号
}
// 使用某个字段
lastName := myName.LastName
也可以用组合的方式定义新的结构体:
type Info struct {
Tel string
Address string
}
type Detail struct {
Name
Info
}
Go语言中没有类 (class),但是会给相应的类型绑定方法,具体的形式会在后面面向对象的部分进行讲解。
2.4 通道类型(channel)
如果你学习过CSP,或者你是仰慕Go语言的并发语义和并发模式而来,那么你一定知道什么是通道(channel):通道是一个可以带有一定缓冲的队列,用于goroutine之间的通信。
通过一个例子来看一下如何使用(channel可是Go语言的特色之一啊,睁大眼睛):
func tryChannel() {
ch := make(chan int, 10) // 使用make函数构造一个缓冲容量为10的int通道
for i:=0; i<10; i++ { ch<- i } // 循环的向通道中塞十个数据
for value := range ch { // 遍历通道(循环的取出数据)
fmt.Println(value)
if value == 9 { break } // 这里如果不结束循环的话,会引发DeakLock,具体的原因会在后面讲解,现在只是为了介绍类型
}
close(ch) // 手动关闭通道
}
如果在调用make()
方法的时候,没有传入通道的缓冲容量,则默认为0,也就是说同时传入和传出,否则的话只会阻塞。如果程序中的所有goroutine都阻塞了,则会触发DeadLock,这也解释了例子中为什么要中断循环。
channel是具有队列的性质的,也就是FIFO, First in First out,因此把channel当做队列数据结构来用的情况也是很多的(但是我更建议去手动实现和管理)。
2.5 函数类型(function)
在越来越多的支持面向对象编程语言中,函数被当做一等公民看待。因此也就有专门的函数对象,变量,类型。
函数变量的类型由几个因素组成:函数名、参数、返回值
例子:
type compareFunction func(int, int) int
var min compareFunction = func(x, y int) int {
if x < y {
return x
}
return y
}
fmt.Println(compareFunction(10, 20))
同时函数当然也可以作为参数传入函数。
func less(x, y int, comp func(int, int) bool) {
if comp(x, y) {
return x
}
return y
}
func tryLess() {
comp := func(x, y int) bool { return x < y }
fmt.Println(less(10, 20, comp))
}
匿名函数的使用等等,会在后面进行介绍。
2.6 切片类型(slice)
人们总是抱怨数组的定长不够方便,而在C++ STL的vector出现后,大家就开始疯狂使用这个名为动态数组的东西,还可以自己开辟内存空间,这真是太好用了!
动态数组出现之后,人们又在其基础之上发明了切片(slice),不仅长度可变,而且有很多利用[start:end:step]
进行的操作。最典型的便是Python中的列表。
但是这是Go语言,所以我们来看看Go语言的切片:
type intSlice []int // 在类型定义上,和数组除了长度没有区别
func trySlice() {
ints := intSlice{1, 2, 3, 4, 5}
fmt.Println(len(ints), ints[1:3])// 使用len方法获取切片的长度,也可以使用cap方法获取切片的容量
anotherInts := make(intSlice, 5) // 使用make方法创建一个长度为5的切片
copy(anotherInts, ints) // 使用copy方法复制内容到新切片
fmt.Println(append(ints, 6)) // 使用append方法增加新元素到切片,但是不是原地操作
fmt.Println(append(ints, anotherInts...))
}
2.7 接口类型(interface)
如果说能让程序员在这个强类型强到天上的语言中找到什么慰藉的,大概就是强大的接口了,至于接口是什么,可以看这个
定义接口的方式和定义结构体的方式相似:
type HumanNature interface {
Speak() string // 接口匹配方法
See(interface{}) // 空接口默认可以匹配所有的类型
}
接口的知识太多了,放在以后赘述。
2.8 映射类型(map)
映射,很方便,很实用。因为查询的效率,无序性等等。
map的派生类型是由键类型和值类型共同决定的。常见的构建方式是使用make()
方法。
func tryMap() {
newMap := make(map[int]string)
newMap[1] = "C" // 插入键值对
if value, have := newMap[2]; have { // 通常用这种方式检查是否有对应的键值对
// ...some code here
} else { fmt.Println("Key: 2 Not Found!") }
newMap[2] = "C++"; newMap[3] = "Java"
for key, value := range newMap { // 通过这种形式遍历map
fmt.Println(key, "-", value)
}
delete(newMap, 1) // 通过内置的delete方法来删除键值对
}
map是使用Go语言时非常常用的类型,不论是编写并发代码还是正常代码,都是非常重要的。
扩展阅读: