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 对协程的处理:
早期调度器的处理
老调度器有几个缺点:
- 创建、销毁、调度 G 都需要每个 M 获取锁,形成了激烈的锁竞争。
- M 转移 G 会造成延迟和额外的系统负载。
- 系统调用(CPU 在 M 之前的切换)导致频繁的线程阻塞和取消阻塞操作,增加了系统开销。
GMP模型
调度器的设计策略
调度器的 4 个设计策略:复用线程、利用并行、抢占、全局G队列
复用线程:work stealing、hand off
- work stealing 机制:某个处理器的本地队列空余,从其他处理器中偷取协程来执行
注意,这里是从某个处理器的本地队列偷取,还有从全局队列中偷取的做法
- hand off 机制:如果某个线程阻塞,会将处理器资源让给其他线程。
利用并行:利用 GOMAXPROCS 限定 P 的个数 = CPU 核数 / 2
抢占:
全局G队列:基于 warlk stealing 机制,如果所有处理器的本地队列都没有协程,则从全局获取。