day04 课上笔记-面向对象、错误、io、反射


面向对象

  • go没有class关键字,oop,但是我们可以把go当做面向对象的方式来编程
  • go没有类,可以把struct作为类看待
  • 类的方法是啥: 给struct绑定的方法

继承

  • 通过结构体的匿名嵌套,继承对应的字段和方法,举例 ```go package main

import ( "log" )

type Person struct { Name string Age int }

type Student struct { Person StudentId int }

// 给Person结构体绑定一个SayHello func (p Person) SayHello() { log.Printf("[Person.SayHello][name:%v]", p.Name) }

func main() { p1 := Person{ Name: "小乙", Age: 123, }

s1 := Student{
    Person:    p1,
    StudentId: 99,
}
s1.SayHello()

}

##  结构体单例绑定  
```go
package main

import (
   "log"
)

type Person struct {
   Name string
   Age  int
}

type Student struct {
   Person
   StudentId int
}

// 给Person结构体绑定一个SayHello
func (p Person) SayHello() {
   log.Printf("[Person.SayHello][name:%v]", p.Name)
}

func (p *Person) ChangeAge1() {
   p.Age += 10
   log.Printf("[单实例绑定方法][Person.ChangeAge1][p.Age:%v]", p.Age)
}
func (p Person) ChangeAge2() {
   p.Age += 10
   log.Printf("[非指针型绑定][Person.ChangeAge2][p.Age:%v]", p.Age)
}

func main() {
   p1 := Person{
      Name: "小乙",
      Age:  123,
   }

   s1 := Student{
      Person:    p1,
      StudentId: 99,
   }
   s1.SayHello()
   log.Println(s1.Age)
   s1.ChangeAge1()
   log.Println(s1.Age)


   log.Println(s1.Age)
   s1.ChangeAge2()
   log.Println(s1.Age)
}

多态 :通过接口做多态

go接口

  • interface{} 定义方法的集合
  • 多态体现在,各个结构体对象要实现 接口中定义的所有方法
  • 统一的函数调用入口 ,传入的接口
  • 各个结构体对象中 绑定的方法只能多不能少于 接口定义的
  • 方法的签名要一致:参数类型、参数个数,方法名称,函数返回值要一致

多态的灵魂 是什么

  • 是有一个承载的容器,先把所有实现了接口的对象添加进来。遍历容器调用对应的方法

  • 举例 ```go package main

import "log"

// 体现多态 // 告警通知的函数,根据不同的对象进行通知 // type notifer interface { // 通知方法 notify() }

type user struct { name string email string }

func (u *user) notify() { log.Printf("[普通用户的通知][notify to user :%s]", u.name) }

type admin struct { name string age int }

func (u *admin) notify() { log.Printf("[管理员的通知][notify to user :%s]", u.name) }

// 多态的统一调用入口 func sendNotify(n notifer) { n.notify() }

func main() {

u1 := user{
    name:  "小乙",
    email: "xy@qq.com",
}
a1 := admin{
    name: "燕青",
    age:  18,
}
// 直接调用结构体绑定的方法
log.Println("直接调用结构体绑定的方法")
u1.notify()
a1.notify()
// 体现多态
log.Println("体现多态")
sendNotify(&u1)
sendNotify(&a1)
// 灵魂
log.Println("多态灵魂承载容器")
ns := make([]notifer, 0)
ns = append(ns, &a1)
ns = append(ns, &u1)
for _, n := range ns {
    n.notify()
}

}

### 实际应用 prometheus 的alert和record
![image](./pic/prometheus01.png)



## 常用举例 ,多个数据源推送数据和查询数据
- 举例
```go
package main

import (
    "fmt"
    "log"
)

// 多个数据源推送数据和查询数据
// query 查询数据
// push 方法写入数据

type DataSource interface {
    Push(data string)
    Query(name string) string
}

type redis struct {
    Name string
    Addr string
}

func (r *redis) Push(data string) {
    // 真实应该推入它消息队列
    log.Printf("[Pushdata][ds.name:%s][data:%s]", r.Name, data)
}
func (r *redis) Query(name string) string {
    log.Printf("[Query.data][ds.name:%s][name:%s]", r.Name, name)
    return name +"-"+ r.Name
}

type kafka struct {
    Name string
    Addr string
}

func (k *kafka) Push(data string) {
    // 真实应该推入它消息队列
    log.Printf("[Pushdata][ds.name:%s][data:%s]", k.Name, data)
}
func (k *kafka) Query(name string) string {
    log.Printf("[Query.data][ds.name:%s][name:%s]", k.Name, name)
    return name +"-"+ k.Name
}

// 灵魂容器
var DataSourceManager = make(map[string]DataSource)

// 注册方法
func register(name string, ds DataSource) {
    DataSourceManager[name] = ds
}

func main() {
    r := redis{
        Name: "redis-6.0",
        Addr: "1.1",
    }
    k := kafka{
        Name: "kafka-2.11",
        Addr: "2.2",
    }
    // 将数据源注册到承载的容器中
    register("redis", &r)
    register("kafka", &k)
    // 模拟推送数据
    for i := 0; i < 10; i++ {
        key := fmt.Sprintf("key_%d", i)
        for _, ds := range DataSourceManager {
            ds.Push(key)
        }
    }
    // 查询数据
    for i := 0; i < 10; i++ {
        key := fmt.Sprintf("key_%d", i)
        for _, ds := range DataSourceManager {
            log.Println(ds.Query(key))
        }
    }
}

空接口

  • 所有的类型都实现了空接口

类型断言 和类型判断

  • 一个interface 需要类型转换的时候,语法 i.(T)
  • v,ok:=i.(T) ,ok=true代表断言成功,ok=false v是这个类型的0值
  • 举例 ```go package main

import "fmt"

func main() {

var s interface{} = false
//s1, ok := s.(string)
//fmt.Println(s1, ok)
//
//s2, ok := s.(int)
//fmt.Println(s2, ok )
switch s.(type) {
case string:
    fmt.Println("是个string")
case int:
    fmt.Println("是个int")
default:
    fmt.Println("未知的type")
}

}

## hook机制
- logrus 的hook机制  github.com/sirupsen/logrus

# go语言错误
- error类型是一个接口类型,这是它的定义:
```go
type error interface { 
    Error() string 
}

if err!=nil

  • 函数调用时判断返回值

errors.New 创建error

package main

import (
    "errors"
    "log"
    "strings"
)

func validate(name string) (ok bool, err error) {
    if !strings.HasPrefix(name, "mysql") {
        return false, errors.New("name must start with mysql")
    }
    return true, nil
}

func main() {
    s1 := "mysql-abc"
    s2 := "redis-abc"
    _, err := validate(s1)
    if err != nil {
        log.Printf("[judge1][validate][err:%v]", err)
    }
    if ok, err := validate(s2); err != nil {
        log.Printf("[judge2][validate][err:%v][ok:%v]", err, ok)
    }
}

复杂的错误类型

  • 以os包举例 提供了 LinkError、PathError、SyscallError
  • 这些实现了 error 接口的错误类型,以 PathError 为例,顾名思义,它主要用于表示路径相关的错误信息,比如文件不存在,其底层类型结构信息如下:
    func (e *PathError) Error() string { 
      return e.Op + " " + e.Path + ": " + e.Err.Error() 
    }
    
  • 样例 ```go package main

import ( "log" "os" )

func main() {

f1, err := os.Stat("test.txt")
if err != nil {
    switch err.(type) {
    case *os.PathError:
        log.Printf("PathError")
    case *os.LinkError:
        log.Printf("LinkError")
    case *os.SyscallError:
        log.Printf("SyscallError")
    }
} else {
    log.Printf("f1:%v", f1)
}

}

## 自定义error
- 使用结构体额外的msg字段扩展
- 自定义自己的struct,添加用于存储我们需要额外信息的字段
- 缺点就是定义很多结构体
```go
package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    err error
    msg string
}

func (e *MyError) Error() string {
    return e.err.Error() + e.msg
}

func main() {

    err := errors.New("原始错误")
    myErr := MyError{
        err: err,
        msg: " 数据上传问题",
    }
    fmt.Println(myErr.Error())
}

golang 1.13中的Error Wrapping 错误嵌套

  • 用来给错误添加额外信息的信息
  • Golang并没有提供什么Wrap函数,而是扩展了fmt.Errorf函数,加了一个%w来生成一个可以Wrapping Error ```go package main

import ( "errors" "fmt" )

func main() {

e1 := errors.New("原始错误")
e2 := fmt.Errorf("数据删除错误 :%w", e1)
fmt.Println(e2)

}

# I/O操作也叫输入输出操作
- 其中I是指Input,O是指Output
- 用于读或者写数据的,有些语言中也叫流操作,是指数据通信的通道。



# Go 输入和输出操作是使用原语实现的
- 这些原语将数据模拟成可读的或可写的字节流。
- Go 的 io 包提供了 io.Reader 和 io.Writer 接口
- 分别用于数据的输入和输出
![image](pic/iorw.png)



# io库属于底层接口定义库
- 其作用是是定义一些基本接口和一些基本常量
- 一般用这个库只是为了调用它的一些常量,比如io.EOF


# io库比较常用的接口有三个,分别是Reader,Writer和Close。
- io库中实现的接口可以以流的方式高效处理数据,而不用考虑数据是什么,数据来自哪里,以及数据要发送到哪里的问题


## Reader
- io.Reader 表示一个读取器,它将数据从某个资源读取到传输缓冲区。在缓冲区中,数据可以被流式传输和使用。
![image](./pic/ior.png)
```go
type Reader interface {
    Read(p []byte) (n int, err error)
}
  • 对于要用作读取器的类型,它必须实现 io.Reader 接口的唯一一个方法 Read(p []byte)。
  • 换句话说,只要实现了 Read(p []byte) ,那它就是一个读取器。
  • Read() 方法有两个返回值,一个是读取到的字节数,一个是发生错误时的错误。
  • 通过 string.NewReader(string) 创建一个字符串读取器,然后流式地按字节读取:
package main

import (
    "io"
    "log"
    "os"
    "strings"
)

func main() {

    reader := strings.NewReader("xiaoyi 123dwd 123")
    // 每次读取4个字节
    p := make([]byte, 4)
    for {

        n, err := reader.Read(p)
        if err != nil {
            if err == io.EOF {
                log.Printf("读完了:eof错误 :%d", n)
                break
            }
            log.Printf("其他错误:%v", err)
            os.Exit(2)
        }
        log.Printf("[读取到的字节数为:%d][内容:%v]", n, string(p[:n]))
        log.Printf("[读取到的字节数为:%d][内容:%v]", n, string(p))
    }

}
  • 可以看到,最后一次返回的 n 值有可能小于缓冲区大小。
  • io.EOF 来表示输入流已经读取到头

查看strings.Reader.Read方法 文件在 C:\Program Files\Go\src\strings\reader.go

func (r *Reader) Read(b []byte) (n int, err error) {
    if r.i >= int64(len(r.s)) {
        return 0, io.EOF
    }
    r.prevRune = -1
    n = copy(b, r.s[r.i:])
    r.i += int64(n)
    return
}

自己实现一个 strings.Reader

  • 举例 a-z A-Z

组合多个 Reader,目的是重用和屏蔽下层实现的复杂度

  • 标准库已经实现了许多 Reader。
  • 使用一个 Reader 作为另一个 Reader 的实现是一种常见的用法。
  • 这样做可以让一个 Reader 重用另一个 Reader 的逻辑,下面展示通过更新 alphaReader 以接受 io.Reader 作为其来源。
package main

import (
    "io"
    "log"
    "strings"
)

type alphaReader struct {
    // 组合io.reader
    reader io.Reader
}

func (a *alphaReader) Read(p []byte) (int, error) {
    // 这里调用的是io.Reader
    n, err := a.reader.Read(p)
    if err != nil {
        return n, err
    }
    buf := make([]byte, n)
    for i := 0; i < n; i++ {
        if char := guolv(p[i]); char != 0 {
            buf[i] = char
        }
    }
    copy(p, buf)
    return n, nil
}

// 只保留字符串中的字母字符 a-z A-Z
func guolv(r byte) byte {
    if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') {
        return r
    }
    return 0
}

func main() {
    originReader := strings.NewReader("xiaoYX @dakdwd 1213kwdwd!!")
    reader := alphaReader{
        reader: originReader,
    }
    p1 := make([]byte, 4)
    for {
        n1, err := reader.Read(p1)
        if err == io.EOF {
            break
        }
        log.Printf("[][内容:%v]", string(p1[:n1]))
    }
}

os.File 结合

  • 以下代码展示了 alphaReader 如何与 os.File 结合以过滤掉文件中的非字母字符: ```go package main

import ( "fmt" "io" "log" "os" )

var a byte = 0

type alphaReader struct { // 组合io.reader reader io.Reader }

func (a *alphaReader) Read(p []byte) (int, error) { // 这里调用的是io.Reader n, err := a.reader.Read(p) if err != nil { return n, err } // 此时的p [xi@a] [xiao] buf := make([]byte, n) // buf = [0 0 0 0] fmt.Println(buf) for i := 0; i < n; i++ { if char := guolv(p[i]); char != 0 { buf[i] = char } } fmt.Println(buf) // buf = [79 68 0 65] copy(p, buf) return n, nil }

// 只保留字符串中的字母字符 a-z A-Z func guolv(r byte) byte { if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { return r } return 0 }

func main() { file,err:=os.Open("a.txt") if err!=nil{ return } //originReader := strings.NewReader("xiaoYX(@dakdwd[1213kwdwd!!") reader := alphaReader{ reader: file, } p1 := make([]byte, 4) for { n1, err := reader.Read(p1) if err == io.EOF { break } log.Printf("[][内容:%v]", string(p1[:n1])) } }

## Writer 
- io.Writer 表示一个编写器,它从缓冲区读取数据,并将数据写入目标资源。
![image](./pic/iow.png)

- 对于要用作编写器的类型,必须实现 io.Writer 接口的唯一一个方法 Write(p []byte)
- 同样,只要实现了 Write(p []byte) ,那它就是一个编写器。
```go
type Writer interface {
    Write(p []byte) (n int, err error)
}
  • Write() 方法有两个返回值,一个是写入到目标资源的字节数,一个是发生错误时的错误。

bytes.Buffer库

  • bytes.Buffer 的针对的是内存到内存的缓存

closer

  • 关闭
    type Closer interface {
      Close() error
    }
    

ioutil库 工具包

  • ioutil库包含在io目录下,它的主要作用是作为一个工具包,里面有一些比较实用的函数
  • 比如 ReadAll(从某个源读取数据)、ReadFile(读取文件内容)、WriteFile(将数据写入文件)、ReadDir(获取目录) ### readFile实例
  • 举例 ```go package main

import ( "fmt" "io/ioutil" )

func main() { bytes, err := ioutil.ReadFile("go.mod") if err != nil { fmt.Println(err) return } fmt.Printf("%s", bytes) fmt.Printf("%v", string(bytes)) }

### writeFile 写入文件
- 举例

package main

import ( "fmt" "io/ioutil" )

func main() { fileName := "a.txt" err := ioutil.WriteFile(fileName, []byte("升职加薪\n迎娶白富美"), 0644) fmt.Println(err) }

### readDir 读取目录下的文件元信息
- 举例

```go
package main

import (
    "io/ioutil"
    "log"
)

func main() {
    fs, _ := ioutil.ReadDir("./lugo03")

    for _, f := range fs {
        log.Printf("[name:%v][size:%v][mode:%v][modTime:%v]",
            f.Name(),
            f.Size(),
            f.Mode(),
            f.ModTime(),


        )
    }
}

os库 操作系统打交道

os.create

  • file.WriteString 和 file.Write,举例 ```go package main

import ( "os" )

func main() { file,_:=os.Create("b.txt")

for i:=0;i<5;i++{
    file.WriteString("WriteString\n")
    file.Write([]byte("Write\n"))
}

}

## os其他常用函数
- 举例
```go
package main

import (
    "log"
    "os"
)

func main() {
    hn, _ := os.Hostname()
    log.Printf("主机名:%v", hn)
    log.Printf("进程pid:%v", os.Getpid())
    log.Printf("命令行参数:%v", os.Args)
    log.Printf("获取GOROOT 环境变量:%v", os.Getenv("GOROOT"))

    for _, v := range os.Environ() {
        log.Printf("环境变量 %v", v)
    }
    dir,_:=os.Getwd()
    log.Printf("当前目录:%v", dir)
    log.Println("创建单一config目录")
    os.Mkdir("config",0755)
    log.Println("创建层级config1/yaml/local目录")
    os.MkdirAll("config1/yaml/local",0755)

    //log.Printf("删除单一文件或目录",os.Remove("config"))
    //log.Printf("删除层级文件或目录",os.RemoveAll("config1"))
}

读取文件 ioutil.ReadFile vs bufio

  • 举例
package main

import (
    "bufio"
    "fmt"
    "io/ioutil"
    "os"
)

func main() {

    // 方式一
    bytes1, _ := ioutil.ReadFile("go.mod")

    file, _ := os.Open("go.mod")
    // 方式二
    bytes2, _ := ioutil.ReadAll(file)
    // 方法三
    file.Close()
    file, _ = os.Open("go.mod")
    bo := bufio.NewReader(file)
    buf := make([]byte, 200)
    bo.Read(buf)

    fmt.Println(string(bytes1))
    fmt.Println(string(bytes2))
    fmt.Println(string(buf))
}
  • 两者都提供了对文件的读写功能,唯一的不同就是bufio多了一层缓存的功能,这个优势主要体现读取大文件的时候(ioutil.ReadFile是一次性将内容加载到内存,如果内容过大,很容易爆内存)

标准输出,标准输入

  • os.Stdout.Write 替代fmt.print

os.stdin 作为脚本的输入内容

反射的定义

  • 反射是值一类应用,他们能够 字描述 自控制

go中反射的简介

  • go是一种静态语言。golang 提供一种机制 。在编译时不知道类型的情况下,可以做如下
  • 更新变量
  • 运行时查看值
  • 调用方法
  • 对他们的布局进行操作的机制

为什么使用反射

两个经典场景

  1. 你编写的函数,还不知道传给你的参数类型是什么,可能是没约定好,也可能是传入的类型很多。
  2. 希望通过用户的输入来决定调用哪个函数(根据字符串调用方法),动态执行函数
  • 举例使用 interface.type判断类型

使用反射获取变量内部的信息

  • reflect包提供 valueOf 和TypeOf
  • reflect.ValueOf :获取输入接口中的数据的值,如果为空的则返回 0
  • reflect.TypeOf :动态获取数据接口中的值的类型,如果为空则返回nil
  • 思考为何 TypeOf 可以传入所有的类型,因为所有的类型都实现了空接口

举例1 内置类型的测试

举例2 自定义struct的反射

  • 生产使用举例 未知原有类型【遍历探测其Filed】,写个函数统一处理
  • go语言里面struct里面成员变量小写,在使用反射时会直接panic reflect.Value.Interface: cannot return value obtained from unexported field or method
  • 但是结构体方法变量小写是不会panic的,也不会反射查看到
  • 指针方法是不能被反射查看到的

具体过程

对于成员变量

对于方法

举例3 反射修改值

  • 只能指针类型
  • pointer.Elem().Setxxx()

举例4 反射调用方法

  • 过程说明
    1. 首先通过reflect.ValueOf 获取到反射类型对象
    2. reflect.Value.MethodByName这.MethodByName,需要指定准确真实的方法名字,MethodByName代表注册
    3. []reflect.Value,这个是最终需要调用的方法的参数,无参数传空切片
  • 代码举例

结构体标签和反射

  • json的标签解析json
  • yaml的标签解析yaml
  • 自定义xiaoyi标签
  • 原理是t.Field.Tag.Lookup("标签名")
  • 举例

反射的副作用

1.代码可读性变差

2.隐藏的错误躲过编译检查

  • go作为静态语言,编译器能发现类型错误
  • 但对于反射代码是无能为力的,可能运行很久才会panic

3. go反射性能问题

type_ := reflect.ValueOf(obj)
fieldValue := type_.FieldByName("hello")
  • 每次取出的fieldValue类型是reflect.Value
  • 它是一个具体的值,不是一个可复用的反射对象了
  • 每次反射都要malloc这个reflect.Value结构体,还有GC
  • 比正常代码运行速度慢1-2g个数量级,如果是追求性能的关键模块应减少反射