Golang语言基础第四部分 | 青训营

62 阅读7分钟

Golang

面向对象编程

type

利用 type 可以声明某个类型的别名(理解为声明一种新的数据类型)

type myint int

func main() {
  	var a myint = 10
		fmt.Println("a = ", a)
		fmt.Printf("type of a = %T\n", a)
}
a =  10
type of a = main.myint

方法

方法:包含了接受者的函数,接受者可以是命名类型或结构体类型的值或者指针。

方法和普通函数的区别

  • 对于普通函数,参数为值类型时,不能将指针类型的数据直接传递,反之亦然。
  • 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法(反过来也可以)。
// 1.普通函数
// 接收值类型参数的函数
func valueIntTest(a int) int {
	return a + 10
}

// 接收指针类型参数的函数
func pointerIntTest(a *int) int {
	return *a + 10
}

func structTestValue() {
	a := 2
	fmt.Println("valueIntTest:", valueIntTest(a))
	// 函数的参数为值类型,则不能直接将指针作为参数传递
	// fmt.Println("valueIntTest:", valueIntTest(&a))
	// compile error: cannot use &a (type *int) as type int in function argument

	b := 5
	fmt.Println("pointerIntTest:", pointerIntTest(&b))
	// 同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
	// fmt.Println("pointerIntTest:", pointerIntTest(b))
	// compile error:cannot use b (type int) as type *int in function argument
}

struct

一道 struct 与指针面试题:

type student struct {
	name string
	age  int
}

func main() {
	m := make(map[string]*student)
	stus := []student{
		{name: "aaa", age: 18},
		{name: "bbb", age: 23},
		{name: "ccc", age: 28},
	}
	for _, stu := range stus {
		m[stu.name] = &stu
	}
	for k, v := range m {
		fmt.Println(k, "=>", v.name)
	}
}
aaa => ccc
bbb => ccc
ccc => ccc

解决方法 1:

for _, stu := range stus {
  // 方法1
  temp := stu
  m[stu.name] = &temp
}

解决方法 2:

for i, stu := range stus {
  // 方法2
  m[stu.name] = &stus[i]
}

封装

Golang 中,类名、属性名、⽅法名 首字⺟大写 表示对外(其他包)可以访问,否则只能够在本包内访问。

继承

Golang 通过匿名字段实现继承的效果

多态

Golang 中多态的基本要素:

  • 有一个父类(有接口)
// 本质是一个指针
type AnimalIF interface {
	Sleep()
	GetColor() string //  获取动物的颜色
	GetType() string  // 获取动物的种类
}
  • 有子类(实现了父类的全部接口)
// 具体的类
type Cat struct {
	color string // 猫的颜色
}

func (c *Cat) Sleep() {
	fmt.Println("Cat is Sleep")
}

func (c *Cat) GetColor() string {
	return c.color
}

func (c *Cat) GetType() string {
	return "Cat"
}
  • 父类类型的变量(指针)指向(引用)子类的具体数据变量
// 接口的数据类型,父类指针
var animal AnimalIF
animal = &Cat{"Green"}
animal.Sleep() // 调用的就是Cat的Sleep()方法, 多态

不同接收者实现接口

type Mover interface {
	move()
}

type dog struct {
	name string
}

值类型接收者实现接口:可以同时接收 值类型 和 指针类型。

Go 语言中有对指针类型变量求值的语法糖,dog 指针 dog2 内部会自动求值 *dog2

指针类型接收者实现接口:只能接收指针类型。

通用万能类型

interface{} 表示空接口,可以用它引用任意类型的数据类型。

// interface{}是万能数据类型
func myFunc(arg interface{}) {
	fmt.Println(arg)
}

type Book struct {
	auth string
}

func main() {
	book := Book{"Golang"}

	myFunc(book)
	myFunc(100)
	myFunc("abc")
	myFunc(3.14)
}

Golang 给 interface{} 提供类型断言机制,用来区分此时引用的类型:

注意断言这个操作会有两个返回值

func myFunc(arg interface{}) {
  // 类型断言
  value, ok := arg.(string)
  if !ok {
    fmt.Println("arg is not string type")
  } else {
    fmt.Println("arg is string type, value = ", value)
    fmt.Printf("value type is %T\n", value)
  }
}

一个接口的值(简称接口值)是由一个 具体类型具体类型的值 两部分组成的。

这两部分分别称为接口的动态类型和动态值。

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil

在这里插入图片描述

switch 判断多个断言:

func justifyType(x interface{}) {
    switch v := x.(type) {
    case string:
        fmt.Printf("x is a string,value is %v\n", v)
    case int:
        fmt.Printf("x is a int is %v\n", v)
    case bool:
        fmt.Printf("x is a bool is %v\n", v)
    default:
        fmt.Println("unsupport type!")
    }
}

反射

变量内置 Pair 结构

在这里插入图片描述

var a string
// pair<statictype:string, value:"aceld">
a = "aceld"

var allType interface{}
// pair<type:string, value:"aceld">
allType = a

str, _ := allType.(string)

类型断言其实就是根据 pair 中的 type 获取到 value

// tty: pair<type: *os.File, value: "/dev/tty" 文件描述符>
tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0)
if err != nil {
  fmt.Println("open file error", err)
  return
}

// r: pair<type: , value: >
var r io.Reader
// r: pair<type: *os.File, value: "/dev/tty" 文件描述符>
r = tty

// w: pair<type: , value: >
var w io.Writer
// w: pair<type: *os.File, value: "/dev/tty" 文件描述符>
w = r.(io.Writer) // 强转

w.Write([]byte("HELLO THIS IS A TEST!!\n"))

仔细分析下面的代码:

  • 由于 pair 在传递过程中是不变的,所以不管 r 还是 w,pair 中的 tpye 始终是 Book
  • 又因为 Book 实现了 Reader、Wrtier 接口,所以 type 为 Book 可以调用 ReadBook() 和 WriteBook()
type Reader interface {
	ReadBook()
}

type Writer interface {
	WriteBook()
}

// 具体类型
type Book struct {
}

func (b *Book) ReadBook() {
	fmt.Println("Read a Book")
}

func (b *Book) WriteBook() {
	fmt.Println("Write a Book")
}

func main() {
	// b: pair<type: Book, value: book{} 地址>
	b := &Book{}

	// book ---> reader
	// r: pair<type: , value: >
	var r Reader
	// r: pair<type: Book, value: book{} 地址>
	r = b
	r.ReadBook()

	// reader ---> writer
	// w: pair<type: , value: >
	var w Writer
	// w: pair<type: Book, value: book{} 地址>
	w = r.(Writer) // 此处的断言为什么成功?因为 w, r 的type是一致的
	w.WriteBook()
}

reflect

reflect 包中的两个重要方法:

// ValueOf returns a new Value initialized to the concrete value
// stored in the interface i. ValueOf(nil) returns the zero Value.
func ValueOf(i interface{}) Value {...}

// ValueOf接口用于获取输入参数接口中的数据的值,如果接口为空则返回0
// TypeOf returns the reflection Type that represents the dynamic type of i.
// If i is a nil interface value, TypeOf returns nil.
func TypeOf(i interface{}) Type {...}

// TypeOf用来动态获取输入参数接口中的值的类型,如果接口为空则返回nil

反射的应用:

  • 获取简单变量的类型和值:
func reflectNum(arg interface{}) {
	fmt.Println("type : ", reflect.TypeOf(arg))
	fmt.Println("value : ", reflect.ValueOf(arg))
}

func main() {
	var num float64 = 1.2345
	reflectNum(num)
}
type :  float64
value :  1.2345
  • 获取结构体变量的字段方法:
    • 通过type获取里面的字段
    • 1.获取interface的reflect.Type,通过Type得到NumField,进行遍历
    • 2.得到每个field,数据类型
    • 3.通过field有一个Interface()方法,得到对应的value

结构体标签

结构体标签的定义:

type resume struct {
	Name string `info:"name" doc:"我的名字"`
	Sex  string `info:"sex"`
}

func findTag(str interface{}) {
	t := reflect.TypeOf(str).Elem()

	for i := 0; i < t.NumField(); i++ {
		taginfo := t.Field(i).Tag.Get("info")
		tagdoc := t.Field(i).Tag.Get("doc")
		fmt.Println("info: ", taginfo, " doc: ", tagdoc)
	}
}

func main() {
	var re resume
	findTag(&re)
}

结构体标签的应用:JSON 编码与解码

其他应用:orm 映射关系 …

并发知识

基础知识

早期的操作系统是单进程的,存在两个问题:

1、单一执行流程、计算机只能一个任务一个任务的处理

2、进程阻塞所带来的 CPU 浪费时间

在这里插入图片描述

多线程 / 多进程 解决了阻塞问题:

在这里插入图片描述

但是多线程又面临新的问题:上下文切换所耗费的开销很大

在这里插入图片描述

进程 / 线程的数量越多,切换成本就越大,也就越浪费。

有可能 CPU 使用率 100%,其中 60% 在执行程序,40% 在执行切换…

多线程 随着 同步竞争(如 锁、竞争资源冲突等),开发设计变的越来越复杂。

多线程存在 高消耗调度 CPU高内存占用 的问题:

在这里插入图片描述


如果将内核空间和用户空间的线程拆开,也就出现了协程(其实就是用户空间的线程)

内核空间的线程由 CPU 调度,协程是由开发者来进行调度。

用户线程,就是协程。内核线程,就是真的线程。

在这里插入图片描述

然后在内核线程与协程之间,再加入一个协程调度器:实现线程与协程的一对多模型

  • 弊端:如果一个协程阻塞,会影响下一个的调用(轮询的方式)

在这里插入图片描述

如果将上面的模型改成一对一的模型,虽然没有阻塞,但是和以前的线程模型没有区别了…

在这里插入图片描述

再继续优化成多对多的模型,则将主要精力放在优化协程调度器上:

内核空间是 CPU 地盘,我们无法进行太多优化。

不同的语言想要支持协程的操作,都是在用户空间优化其协程处理器。

在这里插入图片描述

Go 对协程的处理:

在这里插入图片描述

早期调度器的处理

在这里插入图片描述


在这里插入图片描述


老调度器有几个缺点:

  1. 创建、销毁、调度 G 都需要每个 M 获取锁,形成了激烈的锁竞争
  2. M 转移 G 会造成延迟和额外的系统负载
  3. 系统调用(CPU 在 M 之前的切换)导致频繁的线程阻塞和取消阻塞操作,增加了系统开销

GMP模型

在这里插入图片描述


在这里插入图片描述


调度器的设计策略

调度器的 4 个设计策略:复用线程、利用并行、抢占、全局G队列

复用线程:work stealing、hand off

  • work stealing 机制:某个处理器的本地队列空余,从其他处理器中偷取协程来执行

注意,这里是从某个处理器的本地队列偷取,还有从全局队列中偷取的做法

在这里插入图片描述

  • hand off 机制:如果某个线程阻塞,会将处理器资源让给其他线程。

在这里插入图片描述


利用并行:利用 GOMAXPROCS 限定 P 的个数 = CPU 核数 / 2


抢占

在这里插入图片描述


全局G队列:基于 warlk stealing 机制,如果所有处理器的本地队列都没有协程,则从全局获取。

在这里插入图片描述