Go语言学习15-defer、系统函数与内置函数

defer

0x00 defer概述

定义

defer,顾名思义,延迟的意思。

将defer后面的语句延迟执行,那么延迟到啥时候?等到函数即将返回结束的时候执行。

逐行分析:先执行代码,遇到了defer,先将哈哈哈这句话找一个地方存起来,执行其他两句。快要结束的时候,突然想起来还有一个defer,于是再执行。

func deferdemo() {
	defer fmt.Println("哈哈哈哈哈")
	fmt.Println("不不不不不")
	fmt.Println("对对对对对")
}
func main() {
	deferdemo()
}

image-20220215223447819

使用场景

多用于函数结束前释放资源。

1、当函数执行完毕释放资源时,例如一个函数用来打开一个文件,然后写入一些内容,退出的时候一定要关闭文件。为了防止忘了,先通过defer将关闭的语句登记一下,等函数要退出时。

2、还有类似打开网络连接socket的时候

3、连接数据库的时候

多个defer的顺序

多个defer语句,按照先进后出,后进先出的顺序延迟执行。

比如上课交手机,一个一个摞起来,最后取得时候,先取最后一个放的。

func deferdemo() {
	defer fmt.Println("哈哈哈哈哈")
	defer fmt.Println("xxxxxxxxx")
	defer fmt.Println("aaaaaaaaa")
	fmt.Println("不不不不不")
	fmt.Println("对对对对对")
}
func main() {
	deferdemo()
}

image-20220215224058522

0x01 defer执行的时机

在Go语言函数中的return不是原子操作(原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”),它分为给返回值赋值和RET指令两步。

第一步:返回值赋值

第二步:真正的RET返回

函数中如果存在defer,那么defer执行的时机就在第一步和第二步之间。如下图:

image-20220215224314586

0x02 经典面试题分析

这个题在面试的时候出而已,根本不会在实际上用,谁闲的没事折磨自己?

请问,下方代码,4个函数中的x分别输出为多少?

package main 

import "fmt"

func f1() int {
	x := 5
	defer func(){
		x++
	}
	return x
}
func f2()(x int){
	defer func(){
		x++
	}()
	return 5
}
func f3() (y int){
	x :=5
	defer func(){
		x++
	}()
	return x
}

func f4()(x int){
	defer func(x int){
		x++
	}(x)
}
func main(){
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
	fmt.Println(f4())
}

第一题

开始进入函数体,x被赋值为5;随后先忽略defer;到return的时候,切割开步骤:

第一步:return =x =5

第二步 defer:x变成6,并没有给return赋值的操作吧,return所在的内存没有任何变化。

第三步,返回return=5

func f1() int {
	x := 5
	defer func(){
		x++
	}
	return x
}

第二题

首先函数体也就是花括号外面事先声明了返回值为x,即return=x;遇到defer先忽视;到return的时候切开步骤:

第一步return=x=5;

第二步,defer:x=x+1,即x=6,此时影响了return的值,它俩是真的一样的值。return=6

第三步,返回return=6

func f2()(x int){
	defer func(){
		x++
	}()
	return 5
}

第三个

与第一题同理,返回值没有变,x变了。

第四个

函数传参数进去,改的是副本,到时候讲作用域和匿名函数就懂了。

系统函数

0x00 前言

我们之前学了那么多函数,都是自定义的。但是go语言中给我们提供了一些内置的函数。我们先来学习字符串函数

0x01 字符串类系统函数

【1】统计字符串(不是字符)长度len(str)

image-20220217103713358

【2】字符串遍历

利用for-range遍历,如果只用for,打印不出来汉字

image-20220217103856991

第二种方式:利用r=[]rune(str)将字符串转换成切片,再将切片遍历

【3】字符串转整数+整数转字符串

需要调用strconv

image-20220217105623124

【4】查看字符串中有多少子串

image-20220217105946996

【5】不区分字符串比较

image-20220217110630553

【6】区分大小写就很简单了

image-20220217110801165

【7】返回子串在字符串中第一/最后一次出现的索引数

strings.Index("kslajdkasjd","asda")
strings.LastIndex("asdad","asdasd")

image-20220217111355415

【8】字符串替换

strings.Replace("渗透测试渗透工程渗透师NEWX666","渗透","肾透",n)		//n代表从左往右依次替换几个,-1表示全部替换

image-20220217111907542

【9】按照某个字符切割:输出的是切片

fmt.Println(strings.Split("qq-gg-dd-dd","-"))

image-20220217151815408

【10】将字符串的字母进行大小写转换

strings.ToLower("Go")	//go
strings.ToUpper("go")	//GO

【11】将字符串左右两边的空格去掉

fmt.Println(strings.TrimSpace("    ddd    "))

【12】将字符串左右两边指定的字符去掉

fmt.Println(strings.Trim("~root@localhost~","~"))

【13】将字符串左边指定的字符去掉

fmt.Println(strings.TrimLeft("~root@localhost~","~"))

【14】将字符串右边指定的字符去掉

fmt.Println(strings.TrimRight("~root@localhost~","~"))

【15】判断字符串是否以指定的字符串开头

fmt.Println(strings.HasPrefix("https://github.com","https"))

image-20220217152710583

0x02 日期和时间函数

【1】举个例子讲解,日期和时间的函数,必须导入time包。

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	fmt.Printf("now函数的数值为:%v, now函数输出的类型为%T", now, now)

}

time.Now()函数,返回的是一个结构体!

image-20220217163100946

【2】只要年月日,时分秒怎么办

在开发文档中可以看到,结构体中含有年月日等方法,进行调用即可。

image-20220217163458281

image-20220217163813726

解决月输出为字符串的问题:int(now.Month())

image-20220217163955409

【3】日期的格式化

第一种方式:直接通过字符串的形式输出出来。

第二种方式:将字符串输出出来的结果赋予给一个变量。fmt.Sprintf()的值,可以一直变化,而不是像一个变量,里面的值还需要自己更改可能。

date := fmt.Sprintf("当前时间为:%d年%d月%d日 %d时%d分%d秒", now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Second())	//可以得到字符串,以便后续使用
fmt.Println(date)

image-20220217164900993

第三种方式:

十分神奇!必须这么写:才会显示当前的时间。不过你只能改格式,或者删除数字,不能改数字!

这里有一个巧记方式,就是2006年12345,即2006年1月2日,下午3点4分5秒

now.Format("2006/01/02 15/04/05")

image-20220217170132915

image-20220217170443252

内置函数 介绍
close 主要用来关闭channel
len 用来求长度,比如string、array、slice、map、channel
new 用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make 用来分配内存,主要用来分配引用类型,比如chan、map、slice
append 用来追加元素到数组、slice中
panic和recover 用来做错误处理

内置函数

【1】什么是内置函数/内建函数:

Golang设计者为了编程方便,提供了一些函数,这些函数不用导包就可以直接使用,我们称之为Go的内置函数/内建函数。

这些函数调用的时候,不需要导包,直接用就可以了,通过api可以看到这些内置工具是在builtin这个包中的。

image-20220217174403974

【2】len函数

【3】new函数

主要是用来分配内存的。其第一个实参为类型,而非值。其返回值为指向该类型的新分配的零值的指针。

用于给int、float、bool、string、数组和结构体struct

image-20220217175217929

【4】make函数

也是分配内存,只不过主要是用来分配引用类型(指针、slice切片、map、管道chan、interface等)

defer和recover机制处理错误

在Go语言最让人诟病的一点,就是认为所有的错误都是一个值,这个值需要我们来判断是否存在。做Go语言开发经常能看到if err != nil这样的语句,为什么?就是因为Go语言将每一个可能出现error的值都显示返回,调用方就要判断是否为空。据说Go2.0就要重新定义这个错误机制了。

panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。现在有一个例子,就是当程序出现panic后,是非常危险的,会导致后面的代码等打开的资源并没有释放,从而造成内存出问题等等。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

这个例子就是,在说明程序运行期间funcB中引发了panic导致程序崩溃,异常退出了。

image-20220220173400794

这个时候我们就可以通过recover将程序恢复回来,继续往后执行。

func funcA() {
	fmt.Println("func A")
}

func funcB() {
	defer func() {
		err := recover()
		if err != nil {
			fmt.Println("程序错误!正在回收错误... ... ...")
		}
	}()
	panic("panic in B")
}

func funcC() {
	fmt.Println("func C")
}
func main() {
	funcA()
	funcB()
	funcC()
}

image-20220220173809202

再举一个例子

没有加defer+匿名函数+recover的代码,后面根本就不会执行,而加了的代码,就会正常执行下面的语句。

image-20220217182455564

注意

1、recover()必须搭配defer使用

2、defer一定要在可能引发panic的语句之前定义,因为出现了错误的语句直接会终止程序,根本就到不了defer关键词

image-20220217211645195

image-20220217180256420

func test() {
	s1 := 10
	s2 := 0
	s3 := s1 / s2	
	fmt.Println(s3)	//10不能除以0,会报错
	defer func() {
        err := recover()	//recover()函数会捕获到错误信息,即panic之后的,如下图
		if err != nil {
			fmt.Println("something wrong!")
		}
	}()
}

func main() {
	test()
}

注意一点,就是defer尽量在函数的前面。不然函数执行的时候,不会执行defer函数,直接报错导致后方代码不运行了,也就根本不涉及到defer了。

自定义错误

上面的这个例子,看到了,就算我们捕获了错误,输出出来的错误感觉不是很容易理解,我们能不能自定义错误呢?

image-20220217205207010

通过new函数,注意这个new函数不是上述新建类型的new函数,而是errors包下的new函数来操作。这个函数返回一个error类型。

image-20220217205324788

image-20220217210255111

package main

import (
	"errors"
	"fmt"
)

func main() {
	err := test()
	if err != nil {
		fmt.Println("自定义错误:", err)
		panic(err)
		//return
	}
	fmt.Println("12345")
	fmt.Println("12345")

}
func test() (err error) {
	num1 := 10
	num2 := 0
	if num2 == 0 {
		//抛出自定义错误:
		err := errors.New("除数不能为0哦~~")
		return err

	} else { //如果除数不为0,那么正常执行就可以了
		result := num1 / num2
		fmt.Println(result)
	}

	//如果没有错误,返回nil
	return nil
}

请将下面这段代码默写5遍:

package main

import (
	"errors"
	"fmt"
)

func test() (err error) {
	num1 := 10
	num2 := 0

	if num2 == 0 {
		//自定义
		err := errors.New("不好意思,整数不能为0!!")
		return err

	} else { //正常执行
		result := num1 / num2
		fmt.Println(result)
	}
	return nil
}

func main() {
	err := test()
	if err != nil {
		fmt.Println("自定义错误为:", err)
		panic(err)
	}
	r := []string{"beijing", "shanghai", "guangdong", "shenzhen"}
	for i, j := range r {
		fmt.Printf("键为:%d,值为:%v", i, j)
	}
}

image-20220217214137441

快速查看内置函数api的方法

因为内置函数基本都在builtin这个包文件中,所以直接使用此方法即可查看。

go doc builtin.delete

image-20220220230006809

posted @ 2022-02-22 21:21  sukusec不觉水流  阅读(68)  评论(0编辑  收藏  举报