Golang知识总结

高总 2024-7-21 203 7/21

1.go语言变量

两种声明方式 var a (int) 10 或在func内可用a:=10

2.go语言常量

const a [int] =0

常量枚举可定义多个常量

如 const(

nums1=100

nums2 //100

nums3  //100

)
其次还有iota计数器用来重置常量

const(

a=iota //0
b         //1

c       //2

-

d //4

const e =iota //const再次出现,重置为0

a="abc"

b=unsafe.sizeof(a)

此时输出b会是16,因为字符串类型对应一个结构体,该结构体有两个域,第一个域是指向该字符串的指针,第二个域是字符串的长度,每个8字节,共计1个字节,但是并不包含指向该指针的具体内容,因此sizeof(字节大小)返回一直会是16

iota的两种特殊使用:

(1)

const(
a=iota
b
c="haha"
d
e=iota
f)
此时输出0,1,haha,haha,4,5

(2)

const(
a=1<<iota
b=3<<iota
c
d
)
此时a为1左移0位,b为3左移1位,后面均为3左移iota位
故输出1,6,12,24

3.运算符

主要注意一下几个赋值运算符,&=按位与,|=按位或,^=按位异或

以及地址运算符&取地址,*指针变量

一级:*/%<<>>&&^

二级:+-|^

三级:== != <<=>>=

四级:&&

五级:||

注:^相同为0,不同为1

4.结构体

定义:type name struct{

ID int

Age int

.......}

初始化

st:=name{

ID:1000

Age:18

...}

访问结构体成员:结构体变量.成员变量,ex:st.Age

并且结构体内部可以嵌套,例如

type Geo struct{

loc string

}

type name struct{

ID int

Age int

Geo

.......}

此时再初始化st后即可调用st.loc

5.数组与切片

var nums=[10]int {}或var nums=[...]int {}(自动推断元素个数)

var nums=[]int{}或...

var nums=make([]int,2,10),第二三个分别代表初始长度(0填充),最大长度

说白了就是定没定初始长度,可不可以扩容的问题

6.条件语句: if eles if else 以及switch {case1: case2 ... default}

7.循环:golang中只有for 三种,一种经典像java一样,另外一种语法糖for i,v:=range list{},另外模仿while,for i<100...,或可以无限循环for {}

在for range 中有一种特殊的情况需要考虑,有如下代码

arr:=[]int{1,2}
nums:=[]*int{}
for i,v :=range arr{
nums=append(nums,&v)}
此时输出*nums[0]和*nums[1]
希望得到1,2
实际得到2,2

每次打印出v的地址,发现v均不会改变,即为v最后一次的地址,因为在遍历过程中实际是将遍历值存在了临时变量当中,遍历1时存1,遍历2时存2,所以输出的一直是临时变量的地址

那么如何解决这种问题呢,首先可以用一个临时变量存储v,这样输出的会是临时变量的地址,而不会总是v的地址,而v的值每次都不同,所以相当于每次添加了不同的temp地址,也就不会出现上述问题。

代码如下

package main

import "fmt"

func main() {
	nums := []int{1, 2, 3, 4, 5}
	res := []*int{}
	for _, v := range nums {
		temp := v
		res = append(res, &temp)

	}
	fmt.Println(*res[0], *res[1])
}

或者是用索引来存储,而不是用值

package main

import "fmt"

func main() {
	nums := []int{1, 2, 3, 4, 5}
	res := []*int{}
	for i, _ := range nums {
		res = append(res, &nums[i])

	}
	fmt.Println(*res[0], *res[1])
}

 

8.函数:主要就是记住结构func main(入参)返回值{函数体},注意go是支持多个返回值的,并且在go中一切皆变量,函数可以做函数的参数,函数可以做函数的返回值

另外就是函数闭包,函数闭包=匿名函数+运行环境,函数闭包其实就是引用了一些变量的匿名函数,这些变量不会随原环境而释放,生命周期同匿名函数

注意Go中函数参数只是值传递,举个例子来说对于数组来说,函数不可以修改其中的数值,而切片就可以,因为切片其实是一个结构体,其中有一个指向切片内容的指针,函数中对切片的修改实际修改了指针,但数组就是值做参数,而不会修改相应的引用指针地址。有如下代码:

package main

import "fmt"

func change(arr []int) {
	arr[0] = 100
}
func change1(arr [2]int) {
	arr[0] = 100
}
func main() {
	arr := [...]int{1, 2}
	arr1 := []int{1, 2}
	change(arr1)
	fmt.Println(arr1[0])
	change1(arr)
	fmt.Println(arr[0])
}

 

匿名函数:就是没有函数名的函数,调用的时候直接使用()(就像平时调用某个函数change(arr1)),匿名函数内部可以使用全局变量,内部变量。有代码如下:

package main

import "fmt"

func main() {
	logo := "hello"
	func(i int) {
		logo1 := "hi"
		fmt.Println(i + 1)
		fmt.Println(logo)
		fmt.Println(logo1)

	}(100)
}

 

注意这个()要紧跟在}之后,否则会报错

package main

import "fmt"

func test() func() int {
	i := 0
	return func() int {
		i += 1
		return i
	}
}
func main() {
	getnum := test() //此时相当于调用了test,返回了该匿名函数
	fmt.Println(getnum)
	fmt.Println(getnum())
	fmt.Println(getnum())
	fmt.Println(getnum()) //此时相当于加()调用了匿名函数,返回i,并且注意,此时只调用了匿名函数,所以i不会被重置,而会一直递增,所以匿名函数的生命周期同匿名函数,而不同外部函数
	getnum1 := test()     //直到此时重新调用,重置i
	fmt.Println(getnum1)
	fmt.Println(getnum1())
}

 

最后还有可以将函数直接赋值给变量,称为函数变量,如下:

package main

import (
	"fmt"
	"math"
)

func main() {
	test := func(x float64) float64 {
		return math.Sqrt(x)
	}
	fmt.Println(test(9))
}

 

9.指针

go中创建指针有三种方法:var a*int,a=new(int)(此时会开辟空间,即已经有了内存地址,而第一种此时还为nil,此时取*会默认为0,后续赋值地址会变成新值地址),以及直接取地址a=&b,注意go中指针不能运算

注意,地址只能赋值给指针变量,并且对于指针变量取*,会得到该变量存储的值的地址,具体如下:

package main

import "fmt"

func main() {
	var a *int
	b := 3
	a = &b
	fmt.Println(a)
	fmt.Println(*a)
}

 

 

10.方法:绑定在任一具体数据类型上的函数,数据类型可以是type定义的任意数据类型或者其对应的指针类型。

具体来说 func(p *Name) func getname () string{return p.name}这样

另外在go中,如果一个结构体想继承另一个结构体,只需要在当前结构体中写入想继承的结构体名即可,具体如下:

type People struct{
	Name string
	Age int
}

type Student struct{
	Id int
	Score int
	Sex string
	People
}

 

具体方法的示例,如下:

package main

import "fmt"

type People struct {
	Name string
	Age  int
}

type Student struct {
	Id    int
	Score int
	Sex   string
	People
}

func (p *People) Getname() string {
	return p.Name
}

func (s Student) Getscore() int {
	return s.Score
}
func main() {
	st := &Student{
		Id:    1,
		Score: 100,
		Sex:   "man",
		People: People{
			Name: "Gary",
			Age:  20,
		},
	}
	fmt.Println(st.Getscore())
}

 

在goland中测试,无论是否*或&,都能返回正确结果,但以下情况就不可以了。

要修改结构体内容时,必须用引用,原因和上面说的数组一样

package main

import "fmt"

type People struct {
	Name string
	Age  int
}

func (p People) SetName(name string) {
	p.Name = name
}

func main() {
	p := People{Name: "Gary", Age: 20}
	fmt.Println("Before:", p.Name) // 输出: Gary
	p.SetName("John")
	fmt.Println("After:", p.Name)  // 输出: Gary (没有修改)
}

 

指针接收者通常用于以下情况:

  1. 方法需要修改接收者的状态。
  2. 拷贝大的结构体会影响性能。
  3. 保证一致性和避免拷贝。

值接收者通常用于以下情况:

  1. 方法不需要修改接收者的状态。
  2. 接收者是一个小的结构体。
  3. 你希望确保拷贝一个结构体。

不考虑实际开发情况,做题时大概率应该是使用*

11.接口:接口是一组函数的声明,如果某种数据类型绑定了这个接口中的所有函数,则称这种类型为该接口的一种实现。

结构:type name interface{

func [] ()//注意没有定义,即没有函数体,只有声明}

空接口:即接口内没有任何函数声明,因此任何类型都是空接口的实现,因此任何类型都可以赋值给空接口,即

var a interface {}
var b int=3
a=b

故此引出函数断言:

value,ok:=x.(T),,x为接口类型,ok为boool类型。若断言成功,value为x,类型为T,ok为true。否则,value为0,ok为false。这里可以这么用:

package main

import "fmt"

type Phone interface {
	call()
	send()
}
type Apple struct {
	Name string
}

func (a *Apple) call() {
	fmt.Println(a.Name + "Apple call success")
}
func (a *Apple) send() {
	fmt.Println(a.Name + "Apple send success")
}
func main() {
	var phone Phone//声明接口
	phone = new(Apple)//实现接口,因为Apple绑定了所有接口中的函数,将*Apple类型的值赋值给接口类型Phone的变量phone。在这个过程中,Go会自动将*Apple类型的值转换为Phone接口类型。这个转换包括存储指向具体类型实例的指针以及类型信息,使得接口变量能够在需要时调用具体类型的方法。
	phone.(*Apple).Name = "Apple"//使用断言赋值,过程是,phone通过new变成了*Apple类型,然后在这里判断是否和*Apple相等,相等将转换,
                                     //类型Phone不知道它持有的具体值的类 型,所以不能直接访问具体类型的字段。为了访问具 体类型AppleName字段,我们需要使用类型断言将接口变量转换为具体类型,而第一步实例化结束,还是接口类型,需要具体类型转化 phone.call() }

其实上面这种断言方式比较容易出错,还有一种形式是:

package main

import (
	"fmt"
)

func main() {
	var phone interface{}
	var a = 0
	var b *int
	phone = b
	value, ok := phone.(*int)
	fmt.Println(value, ok)
	value1, ok1 := phone.(int)
	fmt.Println(value1, ok1)
	fmt.Println(a)
}

同一个数据结构,可以实现不同的接口,如下所示:

package main

import "fmt"

type Myread interface {
	Myreader()
}
type Mywriter interface {
	Mywriter()
}
type Myreadwriter struct {
}

func (r *Myreadwriter) Myreader() {
	fmt.Println("有输入功能")
}
func (w *Myreadwriter) Mywriter() {
	fmt.Println("有输出功能")
}
func main() {
	var myread Myread
	myread = new(Myreadwriter)
	myread.Myreader()

	var mywriter Mywriter
	mywriter = new(Myreadwriter)
	mywriter.Mywriter()
}

接口还可以做函数参数,如:

package main

import "fmt"

type Reader interface {
	Add() int
}
type nums struct {
	a, b int
}

func (n *nums) Add() int {
	return n.a + n.b
}
func Dojob(reader Reader) {
	fmt.Printf("num is  %d\n", reader.Add())
}
func main() {
	num := &nums{1, 2} //实现了一个nums类型的接口,因为nums为Reader的一个实现,所以可以在函数Dojob中,作为实参传入
	Dojob(num)
}

 

接口还可以嵌套使用,但是要实现被嵌套的接口,需要实现接口中的全部方法,如下所示:

package main

import "fmt"

type A interface {
	run1()
}
type B interface {
	run2()
}
type C interface {
	A
	B
	run3()
}
type run struct {
}

func (r *run) run1() {
	fmt.Println("run1已实现")
}
func (r *run) run2() {
	fmt.Println("run2已实现")
}
func (r *run) run3() {
	fmt.Println("run3已实现")
}
func main() {
	var test C
	test = &run{} //run 实现了C的全部方法,如果有一个方法没实现,就不能执行,因为C类型接口中的方法没有全部实现
	test.run1()
	test.run2()
	test.run3()
}

 

12.error:Go语言中的error是一个普通的接口类型,用来表示程序执行过程中的错误信息。

type error interface{
Error() string
}

error对象的创建上:

1.eerrors.New(msg string)//导入errors包

2.fmt.Errorf(msg string)//导入fmt包

用处:一般用在多返回值函数的最后一个返回值,表示函数执行出错的错误信息,一般用if语句判断一下,出错了怎么办,不出错怎么办,如下:

package main

import (
	"errors"
	"fmt"
)

func increself(i int) (int, error) {
	if i > 0 {
		return i + 1, nil
	}
	return i, fmt.Errorf("输入错误")
}
func main() {
	fmt.Println(increself(10))
	fmt.Println(increself(-1))

	error2 := errors.New("hello")
	error3 := errors.New("hello")
	fmt.Println(error2 == error3)                 //这里是不相等的,因为在errors.New的源码中,返回的是hello的地址,是不相同的
	fmt.Println(error2.Error() == error3.Error()) //相同,因为Error()方法返回的是string,两个字符串是一样的,所以两个error比较不能直接比较,而是应该比较Error()
}

但是有些时候,我们可能不止需要返回错误信息,我们可能还想知道错误码,如error code,所以很多时候我们可以自定义error对象,而error是一个接口,所以我们可以自定义结构体,然后绑定到Error方法,这样就可以实现自定义错误返回,如下:

package main

import (
	"fmt"
)

type MyError struct {
	msg  string
	code int
}
type MyError2 struct {
	msg1  string
	code1 int
}

func (m MyError) Error() string {
	return fmt.Sprintf("msg is %s, code is %d", m.msg, m.code)
}

func (m2 MyError2) Error() string {
	return fmt.Sprintf("msg1 is %S,code1 is %D", m2.msg1, m2.code1)
}

func Trans(msg string, code int) MyError {
	return MyError{msg: msg, code: code}
} //直接将输入的信息转换格式
func Trans2(msg2 string, code2 int) MyError2 {
	return MyError2{msg1: msg2, code1: code2}
}
func code(err error) int {
	if value, ok := err.(MyError); ok {//这里用断言做判断,同样的情况下,MyError2也实现了Error方法从而实现了error接口,但如果这里是用MyError2做断言,main函数中就无法执行出正确信息
		return value.code
	}
	return -1
}
func msg(err error) string {
	if value, ok := err.(MyError); ok {
		return value.msg
	}
	return "错误的信息"
}
func main() {
	error := Trans("hello world", 0)
	fmt.Println(code(error))
	fmt.Println(msg(error))
}

13.defer

defer主要用在函数调用或方法调用前,延迟函数的执行。

延迟到什么时候:1.发生return 2.发生panic 通俗地讲,函数正常或异常退出的时候

语法规则:1.defer后必须是函数或方法调用 2.延迟内容不能被括号括起来

多个defer的执行顺序:LIFO 先调用 后执行,演示如下:

package main

import "fmt"

func defer1() {
	fmt.Println("defer1")
}
func defer2() {
	fmt.Println("defer2")
}
func defer3() {
	fmt.Println("defer3")
}
func main() {
	defer defer1()
	defer defer2()
	defer defer3()
}//输出顺序为3,2,1

defer的使用场景:1.资源释放 (因为在退出前一定会执行defer)2.配合recover捕获异常,举例如下:

package main

import "fmt"

func main() {
	defer func() {
		if error := recover(); error != nil {
			fmt.Println(error)
		}
	}()
	a := 0
	b := 1
	fmt.Println(b / a)
}

defer的考点:通过defer修改返回值,考察输出,举例如下:

(1)defer虽然是延迟调用,但在调用之前,其参数就已经确定,因此后续对参数的修改不影响defer的输出,比如下面的输出应该是1

package main

import "fmt"

func main() {
	num := 1
	defer fmt.Println(num)
	num = 2
	return
}

 

(2)但是如果一开始指定的是地址,那就不一样了,因为此时不变的是地址,但地址中的内容是可能会变得,因此下面输出100,2,3,4,5

package main

import "fmt"

func main() {
	nums := [5]int{1, 2, 3, 4, 5}
	defer printarr(&nums)//此时传入的是地址,地址是不变的
	nums[0] = 100//但是在结束前,地址中的内容变了
	return
}
func printarr(arr *[5]int) {
	for i := range arr {
		fmt.Println(arr[i])
	}
}

(3)结合return 有几种不同的情况:

package main

import "fmt"

func defer1() (res int) {
	nums := 1
	defer func() {
		res++
	}()
	return nums
}
func main() {
	fmt.Println(defer1())
}

 

这个例子返回2.因为return这种绑定的例子,可以分三步走,首先nums是1,接着return的时候,nums的值赋给了res,此时res为1.然后defer  此时res为2,接着返回真实的res

而下面这个,

package main

import "fmt"

func defer1() int {
	nums := 1
	defer func() {
		nums++
	}()
	return 1
}
func main() {
	fmt.Println(defer1())
}

 

返回的是1,因为这种无返回值参数的情况可以看作赋值给tmp变量,赋值后tmp为1,然后defer nums变成2,但是tmp没变,最后返回的是tmp,因此还是1 。

下面这种情况是一样的分析,也返回1,因为最后返回的是res,和nums没什么关系

package main

import "fmt"

func defer1() (res int) {
	nums := 1
	defer func() {
		nums++
	}()
	return nums
}
func main() {
	fmt.Println(defer1())
}

 

14.recover:Recover 用于捕获程序中的panic,使程序发生panic时不会终止,而是继续运行。

关于recover的位置,如果放在开头,可能后面有异常而无法捕获,放在结尾,中间有异常而无法捕获,因此配合defer放在开头(方便代码阅读)

defer会在函数退出的时候执行,不管是正常return 还是异常panic,具体示例如下:

package main

import "fmt"

func main() {
	defer func() {
		if error := recover(); error != nil {
			fmt.Println("出现了panic,使用recover获取信息 ", error)
		}
	}()
	fmt.Println(111)
	panic("异常")
	fmt.Println(222)
}

 

至于recover的调用顺序, 当函数发生panic,会优先呗当前函数的recover捕获,当前函数没有,则依次向外,如果main也没有,就退出。具体代码如下:

package main

import "fmt"

func testpanic1() {
	fmt.Println("panic1的上半部分")
	testpanic2()
	fmt.Println("panic1的下半部分")
}

func testpanic2() {
	defer func() {
		recover()
	}()
	fmt.Println("panic2的上半部分")
	testpanic3()
	fmt.Println("panic3的下半部分")
}

func testpanic3() {
	fmt.Println("panic3的上半部分")
	panic("出现异常")
	fmt.Println("panic3的下半部分")
}
func main() {
	fmt.Println("程序开始")
	testpanic1()
	fmt.Println("程序结束")
}

testpanic运行到3发现异常,但是没有recover,因此向上抛出,2中有,捕获异常,因此1中的下半部分能够正常运行,而3挂掉了,所以2,3的下半部分都无法运行,但异常捕获完了,就可以继续运行到结束

15.依赖管理:用gomodule来做依赖管理,go version>go 1.13

GO111MODULE =OFF 禁用模块,查找依赖的时候在GOPATH下和VENDER文件夹,ON为启用,这时候会根据go.mod下载依赖,AUTO时,项目不在gopath/SRC目录下时,且项目有go.mod文件,则开启模块。

常见的命令有两条,go mod init   初始化文件夹为一个module项目,创建go.mod文件

go mod tidy 下载缺少的module,删除无用的module

在以下两个场景中使用go module 主要需要:

1.新项目中:cd 到主目录 执行go mod init 然后执行 go module tidy

2.修改项目依赖

修改go mod  然后执行go mod tidy

注意可以首先查看一下GO111MODULE 是否开启,命令为go env |grep(windows下为findstr) GO111MODULE

在执行完命令之后会生成一个go.sum文件,用来校验依赖包的正确性。

- THE END -

高总

7月21日09:36

最后修改:2024年7月21日
0

非特殊说明,本博所有文章均为博主原创。