golang基础学习
- golang
- 1. 认识golang和goland使用
- 1.1 goland插件
- 2. 数据类型
- 2.1 结构体
- 3. 流程控制
- if
- switch
- select
- for
- range
- Goto、Break、Continue
- 4. 函数
- 4.1 匿名函数
- 4.2 闭包、递归
- 4.3 延迟调用
- 什么是defer
- defer的常见用途
- 释放已经获取的资源
- 从panic中恢复
- 延迟闭包
- 参数即时求值
- 延迟调用多个函数
- 延迟调用对象的方法
- 并发时释放共享资源锁
- 延迟释放文件
- 延迟关闭tcp连接
- 延迟关闭数据库连接
- 4.4 异常处理
- 异常处理思想
- 自定义异常
- 详细的异常信息
- panic
- panic recover
- 4.5 单元测试
- 压力测试
- 5. 方法
- 什么是方法
- 方法与普通函数的区别
- 匿名字段
- 方法集
- 6. 面向对象
- 接口
- 什么是接口
- 实现接口
- 接口的作用
- 值接收者和指针接收者实现接口的区别
- **值接收者**实现接口
- **指针接收者**实现接口
- 类型和接口的关系
- 一个类型实现多个接口
- 多个类型实现一个接口
- 接口嵌套
- 空接口
- 空接口的应用
- 使用空接口实现map、slice存储任意类型值
- 类型断言
- 关于接口需要注意的
- 7. 标准库
- 反射
- 空接口和反射
- 结构体与反射
golang
1. 认识golang和goland使用
1.1 goland插件
File -> Settings -> Plugins -> Marketplace
- Gopher:将进度条变成golang的标志,更好看
- Rainbow Brackets:多彩括号
- Go Linter:代码规范
- CodeGlance:在编辑区右侧生成一个代码缩略区
2. 数据类型
2.1 结构体
如果结构体中字段首字母小写,该字段将无法正常解析。如果需要字段名首字母小写,可以使用tag的方式。
3. 流程控制
if
switch
select
select语句类似于switch语句,但是select会随机执行一个可运行的case,如果没有case可运行,它将阻塞,直到有case可运行
for
range
Goto、Break、Continue
4. 函数
4.1 匿名函数
匿名函数是指不需要定义函数名的一种函数实现方式
func main() {sum := func(a,b int) int {return a+b}fmt.Println(sum(1,2))
}
4.2 闭包、递归
函数b嵌套在函数a内部,函数a返回函数b
当函数a的内部函数b被函数a外的一个变量引用时,就创建了一个闭包。
从此可以看出闭包的作用就是在函数a执行完并返回后,闭包使得垃圾回收机制GC不会回收函数a所占用的资源。
4.3 延迟调用
什么是defer
通过使用defer
修饰一个函数,使其在外部函数"返回后"才被执行,即使外部的函数返回的是panic异常。这类函数被称作延迟调用函数
打印结果:先"first"后"later"
defer
和finally
类似,但是两者的不同是finally
的作用域在其**异常块中,而defer的作用域限于包围它的那个函数**。
defer的常见用途
释放已经获取的资源
不管NewFromFile
函数是否返回了错误,这个延迟函数都将关闭已经打开的文件。
从panic中恢复
如果defer
和被触发的**panic
**位于同一个goroutine
1中,defer
能够使程序从panic
中恢复,避免整个程序终止。
其中recover()
函数可以返回panic()
函数的参数,这就使得我们能自行处理panic
,同时也可以向panic
中传入错误或其他类型来判断引发panic
的究竟是哪一个值。
延迟闭包
一个使用了 defer
的延迟调用函数可以是任意类型的函数,因此,当 defer
作用于一个匿名函数的时候,这个函数就能够获取到外围函数体中的变量的最新状态。
参数即时求值
Go 的运行时会在延迟调用函数声明时保存任何传递到延迟调用函数中的参数,而不是当它被运行的时候。
func count(i int) (n int) {defer func(i int) {n = n + i}(i)i = i * 2n = ireturn
}func main(){count(10)
}// 输出结果为:30
在延迟调用函数声明时,就已经保存了传入到延迟调用函数中的i的值,也就是10。所以最后n = 20 + 10。
延迟调用多个函数
如果有多个延迟函数,那么这些延迟调用函数会按照**声明顺序的反序**执行。
func stacked(){defer func(){fmt.Println("last") }()defer func(){fmt.Println("first")}
}func main(){stacked()
}// 输出结果为:
// first
// last
延迟调用对象的方法
- 没有使用指针作为接收者
type Car struct {model string
}func (c Car) PrintModel() {fmt.Println(c.model)
}func main() {c := Car{model: "DeLorean DMC-12"}defer c.PrintModel()c.model = "Chevrolet Impala"
}// 输出结果:
// DeLorean DMC-12
- 使用指针作为接收者
type Car struct {model string
}func (c *Car) PrintModel() {fmt.Println(c.model)
}func main() {c := Car{model: "DeLorean DMC-12"}defer c.PrintModel()c.model = "Chevrolet Impala"
}// 输出结果:
// Chevrolet Impala
当外围函数还没有返回的时候,Go 的运行时就会立刻将传递给延迟函数的参数保存起来。
因此,当一个以值作为接收者的方法被 defer 修饰时,接收者会在声明时被拷贝(在这个例子中那就是 Car 对象),此时任何对拷贝的修改都将不可见(例中的 Car.model ),因为,接收者也同时是输入的参数,当使用 defer 修饰时会立刻得出参数的值(也就是 “DeLorean DMC-12” )。
在另一种情况下,当被延迟调用时,接收者为指针对象,此时虽然会产生新的指针变量,但其指向的地址依然与上例中的 “c” 指针的地址相同。因此,任何修改都会完美地作用在同一个对象中。
并发时释放共享资源锁
延迟释放文件
延迟关闭tcp连接
延迟关闭数据库连接
4.4 异常处理
异常处理思想
在go
语言中没有try catch
,因为try catch
会消耗更多的资源。
所以go
语言主张:
- 如果一个函数可能出现异常,那么应该把异常作为返回值,没有异常就返回nil
- 每次调用可能出现异常的函数时,都应该主动进行检查,并做出反应,这个
if
语句术语叫做卫述语句
所以,异常应该总是掌握在我们自己的手上,保证每次操作产生的影响达到最小,保证程序及时部分地方出现问题,也不会影响整个程序的运行。
及时的处理异常,这样就可以减轻上层处理异常的压力。同时,也不要让未知的异常使你的程序崩溃。
自定义异常
比如,程序有一个功能为除法的函数,除数不能为0,否则程序出现异常,我们就要提前判断除数,如果为0,则返回一个异常。
func divisionInt(a, b int) (int, error) {if b == 0 {return -1, errors.New("除数不能为0")}return a / b, nil
}func main(){a, b := 4, 0res, err := divisionInt(a, b)if err != nil {fmt.Println(err.Error())return}fmt.Println(a, "除以", b, "的结果是 ", res)
}
注意:
- 创建一个异常
errors.New("字符串")
- 获取异常信息
err.Error()
errors.New("字符串")
的形式不支持字符串格式化功能,所以可以使用fmt.Errorf()
err = fmt.Errorf("产生了一个%v的异常","除数为0")
详细的异常信息
errors
实现了一个error
的接口,这个接口里只有一个Error
方法且返回一个string
type error interface{Error() string
}
只要结构体实现了这个方法即可,源码的实现方式如下:
type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}// 多一个函数当作构造函数
func New(text string) error {return &errorString{text}
}
所以,我们只要扩充下自定义error
的结构体字段就行了。
type FileError struct{Op stringName stringPath string
}func (f *FileError) Error() string{return fmt.Sprintf("路径为%v的文件%v,在 %v 操作时出错",f.Path,f.Name,f.Op)
}func NewFileError(op string,name string,path string) *FileError{return &FileError(Op:op,Name:name,Path:path)
}
调用:
func main(){f := NewFileError("读","README.md","/home/README.md")fmt.Println(f.Error())
}// 输出结果为:
// 路径为/home/README.md的文件README.md,在 读 操作时出错
panic
程序在什么情况下会崩溃呢?
go
的类型系统会在编译时捕获很多异常,但是有些异常只能在运行时检查,如**数组访问越界、空指针引用**等。这些运行时异常会引起panic
异常(程序直接崩溃退出)。然后在退出的时候调用当前goroutine1的defer延迟调用语句。
有时候在程序运行缺少必要的资源的时候应该手动触发宕机,比如**配置文件解析出错、依赖某种独有库但该操作系统没有**的时候。
defer fmt.Println("关闭文件句柄")
panic("人工创建的运行时异常")// 输出结果为:
// 关闭文件句柄
// panic:人工创建的运行时异常
panic recover
出现panic
以后程序会终止运行,所以我们应该在测试阶段就发现这些问题,然后进行规避,但是如果在程序中产生不可预料的异常(比如在线的web或者rpc服务一般框架层),即使出现问题(一般是遇到不可预料的异常数据)也不应该直接崩溃,应该打印异常日志,关闭资源,跳过异常数据部分,然后继续运行下去,不然线上容易出现大面积血崩。
func divisionIntRecover(a, b int) (ret int, e error) {defer func() {if err := recover(); err != nil {ret = 0e = fmt.Errorf("出现一个错误")}}()return a / b, nil
}func main(){if i, v := divisionIntRecover(4, 0); i != 2 || v != nil {fmt.Println(v.Error())} else {fmt.Printf("a/b=%v\n", i)}if i, v := divisionIntRecover(4, 2); i != 2 || v != nil {fmt.Println(v.Error())} else {fmt.Printf("a/b=%v\n", i)}
}// 输出结果为:
// 出现了一个错误
// a/b=2
注意:
-
调用
panic
后,当前函数从调用点直接退出 -
捕获函数
recover
只有在延迟调用内直接调用才会终止错误,否则总是返回**nil**。任何未捕获的错误都会沿调用堆栈向外传递。-
无效示例:没有在延迟调用内直接调用
func test(){defer fmt.Printf("recover:%v\n", recover())panic("test panic") }func main(){test() }// 输出结果为: // recover:<nil> // panic: test panic
-
无效示例:在延迟调用内,但是没有直接调用
func test(){defer func(){func(){fmt.Printf("recover1:%v\n", recover())}()}()panic("test panic") }func main(){test() }// 输出结果为: // recover1:<nil> // panic: test panic
-
正确示例:在延迟调用内直接调用
func test(){defer func(){fmt.Printf("recover2:%v\n", recover())}()panic("test panic") }func main(){test() }// 输出结果为: // recover2:test panic
-
4.5 单元测试
测试文件名以_test.go的文件是go test测试的一部分,*_test.go文件中有三个类型的函数:
类型 | 格式 | 作用 |
---|---|---|
测试函数 | 函数名前缀为Test | 测试程序的一些逻辑行为是否正确 |
基准函数 | 函数名前缀为Benchmark | 测试函数的性能 |
示例函数 | 函数名前缀为Example | 为文档提供示例文档 |
go
单元测试对文件名、方法名和参数都有很严格的要求
文件名必须以xx_test.go命名
方法必须是Test[^a-z]开头
方法参数必须是t *testing.T
使用go test执行单元测试
压力测试
压力测试是用来检测函数(方法)的性能。
func Benchmark[^a-z](b *testing.B){}
5. 方法
什么是方法
方法就是一个包含了接收者的函数,接收者可以是**命名类型或者结构体类型的一个值或者是一个指针**。所有给定类型的方法属于该类型的方法集。
func (recevier type) methodName(参数列表)(返回值列表){}
注意:
-
接收者类型不能是接口类型,因为接口是一个抽象定义,而方法却是具体实现。
-
接收者类型也不能是指针类型,但是它可以是任何其他允许类型的指针。
-
参数recevier可以任意命名,参数、返回值可以省略。
-
实例value或者pointer可以调用全部的方法,编译器会自动转换。
-
因为方法是函数,所以方法不可以重载,一个类型只能有一个给定名称的方法。但是,同样名字的方法可以在多个不同的接收者类型上存在。
-
一个类型只能有一个给定名称的方法
type stu struct {name stringage string }func (s stu) method() {s.name = "zhangsan" }func (s stu) method() {}// 报错
-
同样名字的方法可以在多个不同的接收者类型上存在
type stu struct {name stringage string } type teacher struct {name stringgender string }func (s stu) method() {s.name = "zhangsan" } func (t teacher) method() {}
-
方法与普通函数的区别
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递;反之亦然。
- 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法;反之亦可。
匿名字段
go
语言支持只提供类型而不写字段名的方式,也就是匿名字段。
当匿名字段是一个struct时,那么这个struct所拥有的全部字段以及方法都被隐式地引入了当前定义的这个struct
type person struct{name stringage intgender string
}
type student struct{person // 匿名字段,那么student就包含了person的所有字段class stringid string
}func main(){stu := student{person{"zhangsan",23,"男"},"data191","001"}fmt.Printf("stu.name is %v\n",stu.name)fmt.Printf("stu.gender is %v\n",stu.gender)fmt.Printf("stu.class is %v\n",stu.class)stu.age = 22fmt.Printf("stu.age is %v\n",stu.age)
}// 输出信息:
// stu.name is zhangsan
// stu.gender is 男
// stu.class is data191
// stu.age is 22
stu还可以访问person作为字段名
stu.person = person{"lisi",21,"男"}
stu.person.age -= 1
但不仅仅是struct字段,所有的内置类型和自定义类型都可以作为匿名字段
type skills []string
type person struct{name stringage intgender string
}
type student struct{person // 匿名字段,那么student就包含了person的所有字段skills // 匿名字段,自定义类型string sliceint // 内置类型作为匿名字段class stringid string
}func main(){stu := student{person{"zhangsan",23,"男"},"data191","001"}stu.skills = []string{"吃","喝","玩"}stu.skills = append(stu.skills,"乐")fmt.Printf("zhangsan's skills are %v\n",stu.skills)stu.int = 100fmt.Printf("stu.int:%v\n",stu.int)
}// 输出结果:
// zhangsan's skills are ["吃","喝","玩","乐"]
// stu.int:100
但是如果在person中有一个字段name,在student中也有一个字段name,那么如何处理呢?
go
语言采用**最外层的优先访问**,这也就允许我们去重载通过匿名字段继承的一些字段。如果我们想访问重载后匿名类型里面的字段,也可以通过匿名字段来访问。
type person struct{name stringage intgender string
}
type student struct{person // 匿名字段,那么student就包含了person的所有字段class stringid stringname string
}func main(){p := person{"zhangsan", 23, "男"}s := student{p, "data191", "001", "lisi"}fmt.Printf("s.name: %v\n", s.name)fmt.Printf("s.person.name: %v\n", s.person.name)
}// 输出结果:
// s.name: lisi
// s.person.name: zhangsan
关于对方法的继承,对比两个代码:
方法集
在go
语言中,每个类型都有与之关联的方法,把**这个类型的所有方法**称之为类型的方法集
type student struct{name stringage int
}func (s student) showName(){fmt.Println(s.name)
}func (s *student) setName(newName string){s.name = newName
}
类型student方法集包含了showName()
方法
类型*student方法集包含了showName()
方法和setName()
方法
因为:
- 类型
T
方法集,包含所有receiver T方法 - 类型
*T
方法集,包含所有receiver T方法和receiver *T方法
6. 面向对象
接口
什么是接口
go
语言中的接口是一些方法的集合(method set),它指定了对象的行为。
接口是一种类型。
实现接口
一个对象只要实现了接口中的所有方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
如下,student实现了People接口中的所有方法,所以student就实现了这个People接口:
type People interface {Speak()
}type student struct {
}func (stu student) Speak() {fmt.Println("hello")
}
接口的作用
那么实现了接口有什么用呢?
接口类型变量可以存储**任何**实现了该接口的实例。
type People interface {Speak()
}type student struct {
}type teacher struct {
}func (stu student) Speak() {fmt.Println("i'm a student")
}func (tea teacher) Speak() {fmt.Println("i'm a teacher")
}func main(){var p Peoplestu := student{}tea := teacher{}p = stup.Speak()p = teap.Speak()
}// 输出结果为:
// i'm a student
// i'm a teacher
值接收者和指针接收者实现接口的区别
**值接收者**实现接口
type People interface {Speak()
}type student struct {
}func (stu student) Speak() {fmt.Println("i'm a student")
}func main(){var p Peoplestu := student{}p = stu // 接收student类型p.Speak()p = &stu // 接收*student类型p.Speak()
}// 输出结果:
// i'm a student
// i'm a student
使用**值接收者实现接口之后,不管是student结构体类型的变量还是student结构体指针类型的变量都可以赋值给People接口**变量。
**指针接收者**实现接口
type People interface {Speak()
}type student struct {
}func (stu *student) Speak() {fmt.Println("i'm a student")
}func main(){var p Peoplestu := student{}p = stu // 接收student类型,不可以接收!!!p = &stu // 接收*student类型,可以接收p.Speak()
}
使用**指针接收者实现接口之后,student结构体类型的变量无法赋值给People接口变量,student结构体指针类型的变量可以复制给People接口**变量。
此时,People接口变量就只能存储**student结构体指针**类型的值
类型和接口的关系
一个类型实现多个接口
一个类型可以实现多个接口,而接口之间彼此独立,不知道对方的实现。
type People interface {Speak()
}type children interface {Play()
}type student struct {
}func (stu student) Speak() {fmt.Println("i'm a student")
}func (stu student) Play() {fmt.Println("i like playing")
}func main(){s := student{}s.Speak()s.Play()
}// 输出结果:
// i'm a student
// i like playing
多个类型实现一个接口
type People interface {Speak()
}type student struct {
}type teacher struct {
}func (stu student) Speak() {fmt.Println("i'm a student")
}func (tea teacher) Speak() {fmt.Println("i'm a teacher")
}func main(){var p Peoplestu := student{}tea := teacher{}p = stup.Speak()p = teap.Speak()
}// 输出结果为:
// i'm a student
// i'm a teacher
一个接口的方法,不一定是一个类型完全实现,接口的方法可以通过在类型中嵌入其他类型或者结构体来实现。
如下,一个洗衣机包含了洗衣和烘干的功能,我们通过两个类型来将其实现
type WashingMachine interface {washing()dry()
}type wash truct {
}type Haier truct {wash
}func (w wash) washing() {fmt.Println("function is wash")
}func (h Haier) dry() {fmt.Println("function is dry")
}
接口嵌套
接口和接口之间可以通过嵌套创建新的接口
type Sayer interface {say()
}type Mover interface {move()
}type Animal interface {SayerMover
}
空接口
空接口是指没有定义任何方法的接口,因此任何类型都实现了空接口。
func main() {var x interface{}s := "nihao"i := 1x = sx = i
}
空接口的应用
使用空接口可以接收任意类型的函数参数
func show(a interface{}) {}
使用空接口实现map、slice存储任意类型值
func main() {sli2 := []interface{}{1, 2, 3}map1 := map[string]interface{}{"name": "zhangsan","age": 2,}
}
类型断言
空接口可以存储任何类型的值,那么我们怎么获取我们存储的具体数据呢?
一个接口的值由**具体类型和具体类型的值**两部分组成。这两部分分别称为接口的动态类型和动态值。
想要判断空接口中的值这个时候就可以使用类型断言,格式为x.(T)
,具体如下:
func main() {sli2 := []interface{}{1, 2, 3}_, ok := sli2[2].(string)fmt.Println(ok)
}
关于接口需要注意的
关于接口需要注意的是,只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗。
7. 标准库
反射
反射是指在程序运行期间对程序本身进行访问和修改的能力
获取类型:reflect.TypeOf(a)
获取值:reflect.ValueOf(a)
空接口和反射
设置值(函数接收者为指针类型):reflect.ValueOf(a).Elem().SetInt()
func reflect_getTypeAndValue(a interface{}) {t := reflect.TypeOf(a)fmt.Printf("类型是:", t)println()k := t.Kind() // 可以获取具体的类型fmt.Printf("具体类型是:", k)println()v := reflect.ValueOf(a)fmt.Printf("值是:", v)println()fmt.Printf("值是:", v.Int())
}
func reflect_setValue(a interface{}) {v := reflect.ValueOf(a)k := v.Kind() // 可以获取具体的类型switch k {case reflect.Int64:v.SetInt(12)println()fmt.Printf("修改后的值是:", v)case reflect.Ptr:v.Elem().SetInt(15)println()fmt.Printf("修改后的值是:", v)}
}func main(){reflect_getTypeAndValue(num)reflect_setValue(&num)
}
结构体与反射
获取所有字段的个数:reflect.TypeOf(o).NumField()获取字段:reflect.TypeOf(o).Field(index)获取字段的名字:reflect.TypeOf(a).Field(index).Name获取字段的类型:reflect.TypeOf(a).Field(index).Type获取字段的值:reflect.ValueOf(o).Field(index).Interface()修改字段的值(函数接收者为<font color='red'>指针类型</font>):reflect.ValueOf(0).Elem().FieldByName(fieldName).SetString()获取字段的tag:reflect.ValueOf(o).Type().Elem().Field(index).Tag.Get(tagName)获取方法的个数:reflect.TypeOf(o).NumMethod()获取方法:reflect.TypeOf(o).Method(index)获取方法的名字:reflect.TypeOf(o).Method(index).Name获取方法的调用类型和参数类型:reflect.TypeOf(o).Method(index).Type调用方法:reflect.ValueOf(o).MethodByName(methodName).Call( []reflect.Value{reflect.ValueOf(value) )
type User struct {Id intName stringAge int
}type Boy struct {UserAddr string
}func (u User) Hello(name string) {fmt.Println("hello:", name)
}
// 查看所有字段的名字、类型和值
func Poni(o interface{}) {t := reflect.TypeOf(o)fmt.Println("类型:", t)fmt.Println("字符串类型:", t.Name())v := reflect.ValueOf(o)fmt.Println("值:", v)for i := 0; i < t.NumField(); i++ {f := t.Field(i)fmt.Printf("%s : %v\t", f.Name, f.Type)val := v.Field(i).Interface()fmt.Println("val:", val)}fmt.Println("=================================")for i := 0; i < t.NumMethod(); i++ {m := t.Method(i)fmt.Print(m.Name)fmt.Println(" ", m.Type)}
}
// 查看匿名字段
func Poni2(b interface{}) {t := reflect.TypeOf(b)v := reflect.ValueOf(b)//fmt.Println(v)fmt.Printf("v.Field(0):%v\n", v.Field(0))for i := 0; i < t.NumField(); i++ {f := t.Field(i)fmt.Printf("%s %s\t", f.Name, f.Type)val := v.Field(i).Interface()fmt.Printf("%s \n", val)}fmt.Println("=========================================")for i := 0; i < t.NumMethod(); i++ {m := t.Method(i)fmt.Printf("%s %s\n", m.Name, m.Type)}
}
// 修改结构体的值
func struct_SetValue(o interface{}) {v := reflect.ValueOf(o)v = v.Elem()f := v.FieldByName("Name")if f.Kind() == reflect.String {f.SetString("wangwu")}
}
// 调用方法
func struct_methodXfer(o interface{}) {v := reflect.ValueOf(o)m := v.MethodByName("Hello")args := []reflect.Value{reflect.ValueOf("kugou")}m.Call(args)
}
// 获取字段的tag
func struct_getFieldTag(o interface{}) {t := reflect.TypeOf(o)f := t.Elem().Field(0)fmt.Println(f.Tag.Get("json"))fmt.Println(f.Tag.Get("db"))
}
goroutine是Go语言中的轻量级线程实现。 ↩︎ ↩︎