zl程序教程

您现在的位置是:首页 >  工具

当前栏目

Golang M 2023 1

Golang 2023
2023-09-11 14:14:56 时间

数据定义

函数返回值问题

1 下面代码是否可以编译通过?

package main

/*
    下面代码是否编译通过?
*/
func myFunc(x,y int)(sum int,error){
    return x+y,nil
}

func main() {
    num, err := myFunc(1, 2)
    fmt.Println("num = ", num)
}

答案:编译报错理由

# command-line-arguments
./test1.go:6:21: syntax error: mixed named and unnamed function parameters

考点:函数返回值命名

结果:编译出错。

在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。

如果返回值有多个返回值必须加上括号;

如果只有一个返回值并且有命名也需要加上括号;

此处函数第一个返回值有 sum 名称,第二个未命名,所以错误。

可以这么写:

package main

import "fmt"

/*
   下面代码是否编译通过?
*/
func myFunc(x, y int) (sum int, err error) {
	return x + y, nil
}

func main() {
	num, err := myFunc(1, 2)
	if err != nil {
		fmt.Println("错误:", err)
	}
	fmt.Println("num = ", num)
}
PS E:\TEXT\test_go\one> go run .\test1.go
num =  3
PS E:\TEXT\test_go\one>

切片

1 切片是引用数据类型 – 注意切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

package main

import "fmt"

func myFunc() {
	// a := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
	// s1 := a[:5] // [0 1 2 3 4]
	// s2 := a[3:6] // [3 4 5]
}

func Zi() {
	s1 := make([]int, 3)
	//[0 0 0]

	s2 := s1
	//将s1 直接赋值给s2,s1 和s2 共用一个底层数组

	s2[0] = 100
	fmt.Println(s1)
	//[100 0 0]

	fmt.Println(s2)
	//[100 0 0]
}

func main() {
	myFunc()
	Zi()
}

结构体比较问题

1 下面代码是否可以编译通过?

为什么?

package main

import "fmt"

func main() {

	sn1 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}

	sn2 := struct {
		age  int
		name string
	}{age: 11, name: "qq"}

	if sn1 == sn2 {
		fmt.Println("sn1 == sn2")
	}

	sm1 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	sm2 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	if sm1 == sm2 {
		fmt.Println("sm1 == sm2")
	}
}

在这里插入图片描述

结果:编译不通过

PS E:\TEXT\test_go\one> go run .\test1.go
# command-line-arguments
.\test1.go:31:5: invalid operation: sm1 == sm2 (struct containing map[string]string cannot be compared)
PS E:\TEXT\test_go\one>

考点:结构体比较

结构体比较规则注意1:

只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关。

比如:顺序不一致报错

sn1 := struct {
	age  int
	name string
}{age: 11, name: "qq"}

sn3:= struct {
    name string
    age  int
}{age:11, name:"qq"}

在这里插入图片描述
sn3sn1 就不是相同的结构体了,不能比较。

结构体比较规则注意2:

结构体是相同的,但是结构体属性中有不可以比较的类型,如 map,slice,则结构体不能用 == 比较。

可以使用 reflect.DeepEqual 进行比较:

package main

import (
	"fmt"
	"reflect"
)

func main() {

	sm1 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	sm2 := struct {
		age int
		m   map[string]string
	}{age: 11, m: map[string]string{"a": "1"}}

	if reflect.DeepEqual(sm1, sm2) {
		fmt.Println("sm1 == sm2")
	} else {
		fmt.Println("sm1 != sm2")
	}
}
PS E:\TEXT\test_go\one> go run .\test1.go
sm1 == sm2
PS E:\TEXT\test_go\one>

string 与 nil 类型

1 下面代码是否能够编译通过?

为什么?

在这里插入图片描述
考点:函数返回值类型

答案:编译不会通过。

PS E:\TEXT\test_go\one> go run .\test1.go
# command-line-arguments
.\test1.go:11:9: cannot use nil as string value in return statement
PS E:\TEXT\test_go\one>

分析:

nil 可以用作 interface、function、pointer、map、slice 和 channel 的 “空值”

但是如果不特别指定的话,Go 语言不能识别类型,所以会报错。通常编译的时候不会报错,但是运行是时候会报: cannot use nil as type string in return argument

常量

1 下面函数有什么问题?

在这里插入图片描述
在这里插入图片描述
解析考点:常量
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。

内存四区概念:

A 数据类型本质:固定内存大小的别名。

B 数据类型的作用:编译器预算对象(变量)分配的内存空间大小。

在这里插入图片描述
流程说明

1、操作系统把物理硬盘代码 load 到内存。

2、操作系统把 c 代码分成四个区。

3、操作系统找到 main 函数入口执行。

栈区(Stack):

空间较小,要求数据读写性能高,数据存放时间较短暂。

由编译器自动分配和释放,存放函数的参数值、函数的调用流程方法地址、局部变量等(局部变量如果产生逃逸现象,可能会挂在在堆区)

堆区(heap):

空间充裕,数据存放时间较久。

一般由开发者分配及释放(但是Golang中会根据变量的逃逸现象来选择是否分配到栈上或堆上),启动Golang的GC由GC清除机制自动回收。

全局区-静态全局变量区

全局变量的开辟是在程序在main之前就已经放在内存中。

而且对外完全可见。

即作用域在全部代码中,任何同包代码均可随时使用,在变量会搞混淆,而且在局部函数中如果同名称变量使用 := 赋值会出现编译错误。

全局变量最终在进程退出时,由操作系统回收。我们在开发的时候,尽量减少使用全局变量的设计。

全局区-常量区:

常量区也归属于全局区,常量为存放数值字面值单位,即不可修改。或者说的有的常量是直接挂钩字面值的。

比如:

const cl = 10

cl 是字面量 10 的对等符号。

所以在golang中,常量是无法取出地址的,因为字面量符号并没有地址而言。

数组和切片

切片的初始化与追加

package main

import (
	"fmt"
)

func main() {
	s := make([]int, 10)

	s = append(s, 1, 2, 3)

	fmt.Println(s)
	// [0 0 0 0 0 0 0 0 0 0 1 2 3]
}

考点:切片追加, make 初始化均为 0。

slice 拼接问题

在这里插入图片描述
编译失败

PS E:\TEXT\test_go\one> go run .\test1.go
# command-line-arguments
.\test1.go:8:18: cannot use s2 (variable of type []int) as type int in argument to append
PS E:\TEXT\test_go\one>

两个 slice 在 append 的时候,记住需要进行将第二个 slice 进行 ... 打散再拼接。

package main

import "fmt"

func main() {
	s1 := []int{1, 2, 3}
	s2 := []int{4, 5}
	s1 = append(s1, s2...)
	fmt.Println(s1)
}
PS E:\TEXT\test_go\one> go run .\test1.go
[1 2 3 4 5]
PS E:\TEXT\test_go\one>

slice 中 new 的使用

package main

import "fmt"

func main() {

	list := new([]int)
	list = append(list, 1)
	fmt.Println(list)
}

编译失败:

PS E:\TEXT\test_go\one> go run .\test1.go
# command-line-arguments
.\test1.go:9:16: first argument to append must be a slice; have list (variable of type *[]int)
PS E:\TEXT\test_go\one>

分析:

  • 切片指针的解引用。
  • 可以使用 list:=make([]int,0) list 类型为切片。
  • 或使用 *list = append(*list, 1) list 类型为指针。

在这里插入图片描述

new 和 make 的区别

二者都是内存的分配(堆上)。

make 只用于 slice、map 以及 channel 的初始化(非零值);

而 new 用于类型的内存分配,并且内存置为零。

所以在我们编写程序的时候,就可以根据自己的需要很好的选择了。

make 返回的还是这三个引用类型本身;

而 new 返回的是指向类型的指针。

Map

Map 的 Value 赋值

在这里插入图片描述

编译失败,
./test7.go:18:23: cannot assign to struct field list["student"].Name in map

分析

map[string]Student 的 value 是一个 Student 结构值,所以当list["student"] = student,是一个值拷贝过程。

list["student"] 则是一个值引用。

那么值引用的特点是只读。

所以对 list["student"].Name = "LDB" 的修改是不允许的。

方法一

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]Student

func main() {

	list = make(map[string]Student)

	student := Student{"Aceld"}

	list["student"] = student
	//list["student"].Name = "LDB"

	/*
	   方法1:
	*/
	tmpStudent := list["student"]
	tmpStudent.Name = "LDB"
	list["student"] = tmpStudent

	fmt.Println(list["student"]) // {LDB}
}

其中

/*
  方法1:
*/
tmpStudent := list["student"]
tmpStudent.Name = "LDB"
list["student"] = tmpStudent

是先做一次值拷贝,做出一个 tmpStudent 副本,然后修改该副本,然后再次发生一次值拷贝复制回去,list["student"] = tmpStudent,但是这种会在整体过程中发生2次结构体值拷贝,性能很差。

方法二

package main

import "fmt"

type Student struct {
	Name string
}

var list map[string]*Student

func main() {

	list = make(map[string]*Student)

	student := Student{"Aceld"}

	list["student"] = &student
	list["student"].Name = "LDB"

	fmt.Println(list["student"]) // &{LDB}
}

我们将 map 的类型的 value 由 Student 值,改成 Student 指针。

var list map[string]*Student

这样,我们实际上每次修改的都是指针所指向的 Student 空间,指针本身是常指针,不能修改,只读属性,但是指向的 Student 是可以随便修改的,而且这里并不需要值拷贝。

只是一个指针的赋值。

map 的遍历赋值

package main

import (
	"fmt"
)

type student struct {
	Name string
	Age  int
}

func main() {
	//定义map
	m := make(map[string]*student)

	//定义student数组
	stus := []student{
		{Name: "zhou", Age: 24},
		{Name: "li", Age: 23},
		{Name: "wang", Age: 22},
	}

	//将数组依次添加到map中
	for _, stu := range stus {
		m[stu.Name] = &stu
	}

	//打印map
	for k, v := range m {
		fmt.Println(k, "=>", v.Name)
	}
}

错误结果

PS E:\TEXT\test_go\one> go run .\test1.go
zhou => wang
li => wang
wang => wang
PS E:\TEXT\test_go\one>

map 中的 3 个 key 均指向数组中最后一个结构体。

分析

foreach 中,stu 是结构体的一个拷贝副本,所以 m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个 struct 的值拷贝。

在这里插入图片描述

正确写法 1

package main

import (
	"fmt"
)

type student struct {
	Name string
	Age  int
}

func main() {
	//定义map
	m := make(map[string]*student)

	//定义student数组
	stus := []student{
		{Name: "zhou", Age: 24},
		{Name: "li", Age: 23},
		{Name: "wang", Age: 22},
	}

	// 遍历结构体数组,依次赋值给map
	for i := 0; i < len(stus); i++ {
		m[stus[i].Name] = &stus[i]
	}

	//打印map
	for k, v := range m {
		fmt.Println(k, "=>", v.Name)
	}
}
PS E:\TEXT\test_go\one> go run .\test1.go
zhou => zhou
li => li
wang => wang
PS E:\TEXT\test_go\one>

在这里插入图片描述

正确写法 2

package main

import "fmt"

type student struct {
	Name string
	Age  int
}

func main() {
	//定义map
	m := make(map[string]*student)

	//定义student数组
	stus := []student{
		{Name: "zhou", Age: 24},
		{Name: "li", Age: 23},
		{Name: "wang", Age: 22},
	}

	//将数组依次添加到map中
	for key, _ := range stus {
		m[stus[key].Name] = &stus[key]
	}

	//打印map
	for k, v := range m {
		fmt.Println(k, "=>", v.Name)
	}
}

interface

interface 的赋值问题

继承与多态的特点,在golang中对多态的特点体现从语法上并不是很明显。

我们知道发生多态的几个要素:

1、有 interface 接口,并且有接口定义的方法。
2、有子类去重写 interface 的接口。
3、有父类指针指向子类的具体对象。

那么,满足上述3个条件,就可以产生多态效果,就是,父类指针可以调用子类的具体方法。

所以上述代码报错的地方在 var peo People = Stduent{} 这条语句, Student{} 已经重写了父类 People{} 中的 Speak(string) string 方法,那么只需要用父类指针指向子类对象即可。

所以应该改成 var peo People = &Student{} 即可编译通过。
(People为interface类型,就是指针类型)

package main

import (
	"fmt"
)

type People interface {
	Speak(string) string
}

type Stduent struct{}

func (stu *Stduent) Speak(think string) (talk string) {
	if think == "love" {
		talk = "You are a good boy"
	} else {
		talk = "hi"
	}
	return
}

func main() {
	var peo People = &Stduent{}
	think := "loves"
	fmt.Println(peo.Speak(think))
}
PS E:\TEXT\test_go\one> go run .\test1.go
hi
PS E:\TEXT\test_go\one>

interface 的内部构造(非空接口 iface 情况)

在这里插入图片描述
分析:

我们需要了解 interface 的内部结构,才能理解这个题目的含义。

interface 在使用的过程中,共有两种表现形式:

1 一种为空接口(empty interface),定义如下:

var MyInterface interface{}

2 非空接口(non-empty interface), 定义如下:

type MyInterface interface {
  function()
}

这两种 interface 类型分别用两种struct表示,空接口为eface, 非空接口为iface.

在这里插入图片描述

空接口 eface

空接口 eface 结构,由两个属性构成,一个是类型信息 _type,一个是数据信息。

其数据结构声明如下:

type eface struct {      //空接口
    _type *_type         //类型信息
    data  unsafe.Pointer 
    //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}

_type属性:

是GO语言中所有类型的公共描述,Go语言几乎所有的数据结构都可以抽象成 _type,是所有类型的公共描述,type负责决定data应该如何解释和操作,type的结构代码如下:

type _type struct {
    size       uintptr  //类型大小
    ptrdata    uintptr  //前缀持有所有指针的内存大小
    hash       uint32   //数据hash值
    tflag      tflag
    align      uint8    //对齐
    fieldalign uint8    //嵌入结构体时的对齐
    kind       uint8    //kind 有些枚举值kind等于0是无效的
    alg        *typeAlg //函数指针数组,类型实现的所有方法
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

data属性:
表示指向具体的实例数据的指针,他是一个 unsafe.Pointer 类型,相当于一个 C 的万能指针 void*。

在这里插入图片描述

非空接口 iface

iface 表示 non-empty interface 的数据结构,非空接口初始化的过程就是初始化一个iface类型的结构,其中data的作用同eface的相同,这里不再多加描述。

type iface struct {
  tab  *itab
  data unsafe.Pointer
}

iface结构中最重要的是 itab 结构(结构如下),每一个 itab 都占 32 字节的空间。

itab可以理解为pair<interface type, concrete type> 。

itab里面包含了interface的一些关键信息,比如method的具体实现。

type itab struct {
  inter  *interfacetype   // 接口自身的元信息
  _type  *_type           // 具体类型的元信息
  link   *itab
  bad    int32
  hash   int32            // _type里也有一个同样的hash,此处多放一个是为了方便运行接口断言
  fun    [1]uintptr       // 函数指针,指向具体类型所实现的方法
}

在这里插入图片描述

所以,People拥有一个Show方法的,属于非空接口,People的内部定义应该是一个iface结构体。

type People interface {
    Show()  
}

在这里插入图片描述

func live() People {
    var stu *Student
    return stu      
}

stu 是一个指向 nil 的空指针,但是最后 return stu 会触发匿名变量 People = stu 值拷贝动作,所以最后 live() 放回给上层的是一个 People insterface{} 类型,也就是一个 iface struct{} 类型。

stu 为 nil,只是 iface 中的 data 为 nil 而已。

但是 iface struct{} 本身并不为 nil。

在这里插入图片描述

所以如下判断的结果为 BBBBBBB:

interface 内部构造(空接口eface情况)

package main

import "fmt"

func Foo(x interface{}) {
	if x == nil {
		fmt.Println("empty interface")
		return
	}
	fmt.Println("non-empty interface")
	// non-empty interface
}
func main() {
	var p *int = nil
	Foo(p)
}

分析

不难看出,Foo() 的形参 x interface{} 是一个空接口类型 eface struct{}。

在这里插入图片描述

在执行 Foo§ 的时候,触发 x interface{} = p 语句,所以此时 x 结构如下。

在这里插入图片描述

所以 x 结构体本身不为 nil,而是 data 指针指向的 p为 nil。

inteface{} 与 *interface{}

在这里插入图片描述

看到这道题需要第一时间想到的是Golang是强类型语言,interface 是所有 golang 类型的父类函数中 func f(x interface{}) 的 interface{} 可以支持传入golang的任何类型,包括指针,但是函数 func g(x *interface{}) 只能接受 *interface{}。

channel

Channel读写特性(15字口诀)

首先,我们先复习一下 Channel 都有哪些特性?

  • 给一个 nil channel 发送数据,造成永远阻塞
  • 从一个 nil channel 接收数据,造成永远阻塞
  • 给一个已经关闭的 channel 发送数据,引起 panic
  • 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值
  • 无缓冲的channel是同步的,而有缓冲的channel是非同步的

以上5个特性是死东西,也可以通过口诀来记忆:“空读写阻塞,写关闭异常,读关闭空零”。

在这里插入图片描述
15字口诀:“空读写阻塞,写关闭异常,读关闭空零”,往已经关闭的channel写入数据会panic的。

因为main在开辟完两个goroutine之后,立刻关闭了ch, 结果报错。

在这里插入图片描述

WaitGroup

在这里插入图片描述
这是使用 WaitGroup 经常犯下的错误!

请各位同学多次运行就会发现输出都会不同甚至又出现报错的问题。

这是因为go执行太快了,导致 wg.Add(1) 还没有执行 main 函数就执行完毕了。

改为如下试试…

在这里插入图片描述