Go基础知识总结
1. 变量
变量的声明有四种方式:
-
声明一个变量,默认的初始化值为0:
var a int
-
声明一个变量,初始值为100:
var a int = 100
-
初始化时候省略数据类型,通过值自动推导变量的数据类型:
var a = 100
-
省略掉var关键字,直接自动匹配,但要使用
:=
a := 100
一个注意的点:第四种声明变量的方式
a := 100
只能在局部方法中使用,全局变量不支持这种写法
多个变量一起声明的写法:
-
单行写法
var a, b int = 100, 200
var a, b = 100, "abc"
a, b := 100, "abc"
-
多行写法
var ( a int = 100 b string = "abc" )
匿名变量
go中使用下划线_
来作为匿名变量。
go支持函数多返回值,而当我们对于某个函数的返回值是不关心的时候,可以使用匿名变量来接收
比如:fd, _ := os.Open(xxx)
,对于第二个返回值我们并不想要,就可以直接用_
接收
2. 常量
go中常量使用关键字const
定义常量与定义变量方式类似,只是将关键字var
换成了const
,但常量定义没有:=
这种写法
比如:
const a int = 100
const (
a = 10
b = 20
)
3. iota
关键字
iota
用于与const
表示枚举类型
go中定义枚举使用的是iota
和const
,如下代码,定义一个枚举
const (
RED = iota
BLUE
BLACK
....
)
注意:在
const
中添加一个关键字iota
,每一行的iota都会累加1,第一行的iota默认值是0
因此上面的,RED=0,BLUE=1,BLACK=2
但是如果第一行的RED我们赋值为5 * iota,那么RED=5 * 0=0,BLUE=5 * 1=5,BLACK=5 * 2=10
因为每一行的iota
自动累加1,每一行相当于是5 * iota
因此有一个常见的实例,使用iota
来进行左移运算实现存储单位的常量枚举:
const (
_ = iota // 赋值给_忽略这个值
B = 1 << (10 * iota)
KB
MB
GB
TB
...
)
4.函数
go函数是允许有多个返回值的。go的函数定义可以有以下几种写法:
-
返回多个值,使用匿名变量
func test(a string, b int) (int, int) { .... return 100, 200 }
-
返回多个值,有参数名称的
func test(a string, b int) (c, d int) { ... c = 100 d = 200 return }
注意:
-
c和d属于test方法的形参,初始值默认为0,他们的作用空间也仅限于test方法,当已经给返回值变量赋值后,可以直接return就好了。
-
也可以返回别的变量, 比如内部在定义一个
e := 300
,最后return c, e
-
5. init函数
init函数是go在每个包初始化后自动执行的,而且在main函数之前执行
因此,init函数常用来:对变量初始化,注册等。
init函数的几个特点:
-
init函数用于包的初始化,是在
package xxxx
的时候完成的,在main
之前完成 -
每个包中是可以拥有多个init函数的,每个包的源文件也是可以有多个init函数的
-
不同包的init函数是需要根据包导入的依赖关系决定的(因为init是在package xxx之后完成)
所以是类似栈的结构,最后的包的init方法先执行
-
init函数不能被其他函数调用,也不需要传入参数,也无返回值
package main
import "fmt"
func int() {
fmt.Println("init ok")
}
func main() {
fmt.Println("main...")
}
6. import 导包
go中使用import
进行导包操作,有几种情况需要了解下:
-
import _ "fmt"
这种使用
_
的方式,是给fmt包起一个别名,是一个匿名,这样子会无法使用包中的方法,但是一旦导包,就会执行包里的init()
方法 -
import aa "fmt
"这种方式是给fmt包起一个别名aa,调用包中方法时候,就可以使用aa,比如
aa.Println()
-
import . "fmt"
这种方式是将fmt包中的所有方法全部导入到当前包中,那么fmt包中的所有方法都可以直接当成本包的方法来调用了,不用再加包名fmt(但这样本包就不能定义与fmt包所有函数的函数名相同的函数了)
7. defer
defer关键字是go独有的,是一种延迟语句,在函数return前执行defer。
一个函数中可以添加多个defer语句,执行顺序是逆序的,先定义的defer最后执行
一般defer用于资源的关闭操作比较多。
有个文章可以看看Golang中defer、return、返回值之间执行顺序的坑
结论就是:return最先执行,return负责将结果写入返回值中;接着defer开始执行一些收尾工作;最后函数携带当前返回值退出。
8. 数组
-
声明数组的方式
var myArray1 [10]int
myArray1 := [5]int{1,2,3,4}
-
数组长度是固定的
-
固定长度的数组在传参的时候,是严格匹配数组类型的
func add(array [4]int) { fmt.Println(array[0], array[1], array[2], array[3]) } func main() { arr := [5]int{1, 2, 3, 4} add(arr) }
这样子传参是不行的,报错:
cannot use arr (variable of type [5]int) as type [4]int in argument to add
,参数是[4]int类型,传参是[5]int -
需要注意的是,数组是一个值类型,在赋值和作为参数传递时将产生一次复制动作。
9. 数组切片(slice)
数组切片slice,也叫动态数组。
创建数组切片有两种方式:基于数组和直接创建
-
基于数组创建
func main() { // 先定义一个数组 var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10} // 基于数组创建一个数组切片 var mySlice []int = myArray[:5] }
注意:go语言支持用
myArray[first:last]
这样的方式基于数组生成一个数组切片,这种[first,last]
是左闭右开的。如果基于myArray的所有元素创建数组切片:
mySlice := myArray[:]
基于myArray的前5个元素创建数组切片:
mySlice := myArray[:5]
基于myArray的第5个元素开始到所有元素创建切片:
mySlice := myArray[5:]
-
直接创建
使用Go提供的内置函数make(),比如:
-
创建一个初始元素个数为5的数组切片,元素初始值为0:
mySlice := make([]int, 5)
-
创建一个初始元素个数为5的数组切片,初始值为0,并预留10个元素的存储空间:
mySlice := make([]int, 5, 10)
-
元素的遍历
-
使用len()函数获取元素个数
for i := 0; i < len(mySlice); i++ { .... }
-
使用
range
关键字遍历for i, v := range mySlice { .... }// i 是index v是元素值
动态增减元素:
-
数组切片支持内置函数
cap()
和len()
,cap()
函数返回的是数组切片分配的空间大小,而len()
函数返回的是数组切片中当前所存储的元素个数。 -
如果需要新增元素,可以使用
append()
函数,生成一个新的数组切片mySlice = append(mySlice, 1, 2, 3)
注意:
-
函数
append()
的第二个参数开始是一个不定参数,可以添加若干个元素 -
也可以将一个数组切片追加到另一个数组切片的末尾
mySlice2 := []int{8, 9, 10} mySlice = append(mySlice, mySlice2...)
这里需要注意,第二个参数mySlice2后面加了三个点,也就是一个省略号,如果没有这个省略号的话会编译错误,因为append方法从第二个参数开始的所有参数都是待添加的元素,加上省略号相当于将mySlice2包含的元素逐个打散再加入
-
-
数组切片扩容的机制
在append的时候,如果长度增加后超过容量,比如
mySlice := make([]int, 3, 4)
,切片mySlice的容量是4个,当前长度是3个元素,那么在执行append,mySlice = append(mySlice, ,3, 4, 5)
后,新增3个元素,加上之前的元素就总共有6个了,超过了容量4,所以这时候切片需要扩容,而扩容的机制就是原始容量的2倍,也就是在新增元素后发现超过了原始的容量的话,会自动以初始容量的2倍去扩容 -
切片复制
使用内置函数
copy()
,用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片没有一样大,就会按其中较小的那个数组切片的元素个数进行复制。
slice1 := []int{1, 2, 3, 4, 5} slice2 := []int{6, 7, 8} copy(slice2, slice2) // 只会复制slice1的前三个元素到slice2中 // slice2 = {1,2,3} slice1 = {1,2,3,4,5} copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置 // slice2 = {6,7,8} slice1 = {6,7,8,4,5}
-
动态数组在传参上是引用传递的,而且不同元素长度的动态数组他们的形参是一致的
func printArray(myArray []int) { ... }
10. map
-
map的声明
var myMap map[int]string
其中myMap是变量名,int是键的类型,string是值的类型
只声明没有创建的map还不可用!
-
map的创建
使用make()函数创建:
myMap = make(map[int]string, 10)
10表示的是map的容量,与切片的容量类似
-
map的赋值
-
可以先声明,再创建,最后赋值
var myMap map[int]string myMap = make(map[int]string, 10) myMap[0] = "java" myMap[1] = "Go"
-
直接使用
:=
myMap := make(map[int]string) myMap[0] = "java" myMap[1] = "Go"
-
声明时赋值
myMap := map[int]string{ 0: "java", 1: "Go", }
-
-
元素删除
使用内置函数
delete()
,用于删除容器内的元素delete(myMap, 0)
,第二个参数是键,如果这个键不存在,啥也不会发生,也不会有影响。但如果传入的map是nil,则会抛出异常panic
-
元素查找
从map中查找一个特定的键,可以使用如下代码:
value, ok := myMap[1] if ok { // 找到了 .... }
只需要查看第二个返回值ok是否为true就知道找没找到,不需要像其他语言那样检查取到的值是不是为nil
11. 面向对象
我们都知道面向对象三个特点:封装,继承,多态。
但是go中并不像其他面向对象语言那样有很多的概念,go语言的面向对象编程是基于语言类型系统的,整个类型系统通过接口串联。
1. 类型系统
go语言中的类型是可以添加方法的,可以给任何类型,包括内置类型增加新方法。比如:
type Integer int
func (a Integer) Less(b Integer) bool {
return a < b
}
// 可以这样使用
func main() {
var a Integer = 1
if a.Less(2) {
fmt.Println(a, "Less 2")
}
}
上面代码使用type
定义了一个新的类型Integer
,实质上它就是一个int类型,然后就给这个新类型增加了个新方法Less()。
新增方法这个语法可以以java类的概念来理解为:Integer就是一个类,而a就相当于类中的this,而Less是类里的一个方法,当然a就可以调用到类里的成员了,但是这里的类实质是一个int,所以也就成员变量就是自身int值变量了,但如果a是一个结构体那就有成员变量了。
注意:当我们需要修改到对象的成员时,需要用到指针。比如代码修改为如下:
func (a *Integer) Add(b Integer) { *a += b }
这里需要修改到对象a的值,所以需要用指针引用。
如果没有需要修改对象的值,go并不要求一定要用指针的,有时候对象很小,用指针传递反而不划算
其实上面用指针和不用指针的具体原因,归根结底就是:Go语言的类型是基于值传递的,要修改变量的值,就需要传递指针。
2. 结构体
结构体的定义很简单,基本和C一样:
type Person struct {
name string
age int
}
// 新增一个方法
func (p *Person) setAge(age int) {
p.age = age
}
func (p Person) getAge() {
return p.age
}
当然,结构体也是go的一种类型,也是可以添加方法的,按我的理解,其实结构体就相当于是面向对象的类,添加的方法就是成员方法,而本身的成员变量就是类中的成员变量。
结构体初始化:
结构体初始化有以下几种实现:
p := new(Person)
p := &Person{}
p := &Person{"zhangsan", 18}
p := &Person{name: "zhangsan", age: 20}
Go语言中没有构造函数这种概念,对象的创建通常做法是交给一个全局的创建函数来完成,以NewXXX
命名,表示构造函数:
func NewPerson(name string, age int) *Person {
return &Person{name, age}
}
3. 封装
回到面向对象三要素,封装,其实结构体就已经是封装的实现了。
这里有个注意的点就是:
类名,属性名,方法名,首字母大写表示对外(也就是其他包)可以访问,否则只能在本包内访问
4. 继承
go语言其实也是提供了继承的,只是采用的是组合的写法,比如以下例子:
// 定义父类
type Animal struct {
name string
age int
}
// 父类方法
func (a *Animal) Say() {
fmt.Println("animal say...")
}
// 定义子类继承父类
type Dog struct {
Animal
weight int
}
func main() {
d := &Dog{}
d.Say()
d.name = "旺财"
fmt.Println(d)
}
输出:
animal say...
&{{旺财 0} 0}
没有初始化值的变量会默认为对应类型的零值。
5. 多态
在理解go语言的多态之前,得先了解go语言的接口类型。
先来了解下其他语言的接口,在java中,对于接口的实现是必须在实现类中声明要实现的接口的,如果要实现一个接口,需要像下面代码这样编写代码:
// 定义一个接口类
public interface Person {
// 接口方法
public void say();
}
// 定义实现类,需要使用关键字implements显式的说明实现哪一个接口
class Teacher implements Person {
public void say() {
system.out.println("Hello 我是老师")
}
}
而在go语言中,一个类只要实现了接口要求的所有函数,就可以说这个类实现了这个接口,当然go中接口使用的关键字还是interface
比如:
有一个File类,并且该类有四个方法,Read(),Write(),Seek(),Close()
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error)
func (f *File) Write(buf []byte) (n int, err error)
func (f *File) Seek(off int64, whence int) (pos int64, err error)
func (f *File) Close() error
然后有以下一些接口:
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
代码中可以看出,File类并没有明确表示从这些接口中继承,甚至对于File类来说都不知道有这些接口的存在,但是在go里,认为File类实现了这些接口。
因此可以这样子进行赋值:
var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)
实质上,这样子不就是多态么!
接口的赋值:
go语言中接口赋值分为以下两种情况:
-
将对象实例赋值给接口
这种情况要求对象实例实现了接口的所有方法,就比如上面的例子:
var file1 IFile = new(File)
-
将一个接口赋值给另一个接口
在go语言中,只要两个接口有相同的方法列表(次序不要求),那么它们就是等同的,可以相互赋值。
但接口的赋值也不要求必须等价,如果接口A的方法列表是接口B的方法列表的子集,那么接口B可以赋值给接口A,而接口A无法赋值给接口B,因为接口B中并没有接口A中的其他方法,如果赋值给接口A了,当接口A调用一个存在于接口A而接口B不存在的方法,那就找不到了
比如:
假设有一个Writer接口和ReadWriter接口,实体类还是上面的File类
type Writer interface { Write(buf []byte) (n int, err error) } type ReadWriter interface { Read(buf []byte) (n int, err error) Write(buf []byte) (n int, err error) }
可以将ReadWriter接口的实例赋值给Writer接口:
var file ReadWriter = new(File) // 接口ReadWriter 赋值给 接口Writer var file1 Writer = file // 这样子是可以的,这样file1是Writer接口的实例,只有一个Write方法可以调用是正常的
但是反过来就不行了:
var file Writer = new(File) // 接口ReadWriter 赋值给 接口Writer var file1 ReadWriter = file // 这样子是不可以的,这样file1是ReadWriter接口的实例,当file1调用read方法时候,并没有这个方法,因为他实质是Writer接口类型
接口查询
接口查询可以检查接口所指向的对象实例是否实现了某个接口,从而进行接口转换,比如:
var file Writer = new(File)
if file1, ok := file.(ReadWriter); ok {
...
}
这里是Writer接口所指向的对象实例是File类,是实现了ReadWriter的,所以这里ok会为true,file1是ReadWriter接口的实例,所以相当于是从Writer接口转为了ReadWriter接口了。
万能类型
在Go语言中,有这么一种空接口,源码里是这样的:type any = interface{}
,是一个空接口,根据之前对接口实现的理解,空接口里没有任何方法,那么就可以认为所有的类型其实都是实现了这个接口的,因此这个interface{}
可以指向任何对象,称为Any类型,也叫万能类型。
var a interface{} = new(int)
var b interface{} = new(string)
var c interface{} = struct{X int}{1}
a = 10
b = "hello"
fmt.Println(a, b, c) // 输出:10 hello {1}
任何对象实例都实现了interface{}
,就类似于Java中的Object类一样,那我们就可以用interface{}
类型引用任意的数据类型了,像上面的代码那样,这用在函数中传参就很有用了!
类型查询(类型断言)
基于Go语言所有的对象实例都实现了空接口interface{}
这个前提,那我们便可以直接了当的询问接口指向的对象实例的类型:xxx.(type)
func test(arg interface{}) {
switch arg.(type) {
case int:
fmt.Println("int type")
case string:
fmt.Println("string type")
default:
fmt.Println("unknown type")
}
}
func main() {
var v1 interface{} = "hello"
var v2 int = 100
v3 := struct{ X int }{1}
test(v1)
test(v2)
test(v3)
}
12. 学习文档
《Go语言编程》