go基础部分四 | 青训营笔记

93 阅读6分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

今天和大家分享go基础的最后一部分。主要包括异常错误,断言,反射,结构体标签的概念和常见操作。

错误与异常

错误

go中内建了一个错误接口,任何实现该接口的方法都可以使用错误:

type error interface {
  Error() string
}

例如,fmt.Println就会在内部调用Error()方法来返回错误字符串:

func FindFile() {
    file, err := os.Open("/a.txt") //找一个不存在的文件
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(file)
    }
}
//open /a.txt: no such file or directory

自定义错误:

//定义一个结构体
type errorString struct {
    s string
}
​
//自定义error
func MyError(text string) error {
    return &errorString{s: text}
}
​
//实现Error方法,来实现error接口
func (e *errorString) Error() string {
    return e.s
}
​
func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, MyError("被除数不能为零")
    }
    return a / b, nil
}
func main() {
    fmt.Println(divide(1, 0)) //0 被除数不能为零
}

更简单的,我么可以使用return errors.New(“错误信息”)或者fmt.Errorf("错误信息")快速返回自定义信息的错误

异常

异常指的是不应该出现问题的地方真的出现了问题。

我们可以使用panic来触发异常:

panic("asd")  //panic: asd
fmt.Println("123") //并不会执行

程序发生异常的时候,会在panic处停止,执行完所有延迟函数后,执行并打印panic中的值,就此返回,并返回堆栈信息。在没有panic的时候,发生异常则也会立即停止,执行延迟函数,打印错误信息,返回堆栈信息。可以看做在异常的下一句直接执行了panic

同时,我们可以通过recover 捕获异常:

func main() {
    read(2)
    fmt.Println("123") //会打印出来
}
​
func read(i int) {
    defer func() {
        err := recover() // 只用被延迟的方法可以使用recover()
        fmt.Println(err)
    }()
    arr := [1]int{1}
    fmt.Println(arr[i])
}
//runtime error: index out of range [2] with length 1
//123

会继续执行的原因是函数内发生了异常,本应立即返回,但是被defer中的recover函数捕获了,所以没有在外层产生异常。

异常会不断向上传递,所以我们可以在顶层函数上的延迟函数中捕获异常保证程序不崩溃

断言

基础

断言就是将接口类型的x转换成类型T。格式为:x.(T)

  • 类型断言的必要条件就x是接口类型,非接口类型的x不能做类型断言;
  • T可以是非接口类型(基础类型,结构体或者指针等),如果想断言合法,则T必须实现x的接口;
  • T也可以是接口,则x的动态类型也应该是接口T;
  • 类型断言如果非法,运行时会导致错误,为了避免这种错误,应该总是使用下面的方式来进行类型断言:
package main
​
import (
    "fmt"
)
func main() {
  var x interface{}
  x = 100
  value1,ok :=x.(int)
  if ok {
    fmt.Println(value1)
  }
  value2,ok :=x.(string)
  if ok {
    fmt.Println(value2)
  }
}

需要注意的如果不接收第二个参数也就是ok,这里失败的话则会直接panic。这里还存在一种情况就是x为nil同样会panic

若类型检查成功提取到的值也将拥有对应type的方法:

package main
​
import "fmt"
​
func main() {
  var a interface{}
  a =A{}
  value :=a.(A)
  value.Hi()
  fmt.Println("看是否输出",value.Name)
}
type A struct {
    Name string
}
func (a *A) Hi()  {
    a.Name="fushaohua"
    fmt.Println(a)
}

这里我们定义一个结构体,又定义了一个方法,其中方法的参数类型为一个泛型,那么此时我们想调用参数的属性或方法就会有问题:

type User struct {
    Name string
    Age  int
    Sex  bool
}
​
func (u User) SayName() (name string) {
    fmt.Println(u.Name)
    return
}
​
func main() {
    u := User{
        Name: "aei",
        Age:  10,
        Sex:  true,
    }
    check(u)
}
​
func check(v interface{}) {
    v.SayName() // 报错 type interface{} has no field or method SayName
}

这个时候我们使用断言就可以结局这个问题:

直接使用断言:

v.(结构体类型).属性或方法

func check(v interface{}) {
    v.(User).SayName() // 不再报错
}

同时我们可以通过v.(type)和switch结合使用获得动态的类型值:

func check(v interface{}) {
    switch v.(type) {
    case User:
        v.(User).SayName()
    }
}

断言和指针

type A struct {
    name string
}
​
type Boy interface {
    getName() string
}
​
func (receiver A) getName() string {
    return receiver.name
}
​
func main() {
    a := A{
        name: "xiaoming",
    }
    formatName(&a)
    fmt.Println(a.name) // Mr.xiaoming} 如果我们在下面使用 ns.(A) ,则值不会被修改
}
// 这里的形参类型为接口,我们传递结构体或者指针都可以
func formatName(boy Boy) {
    //n, _ := ns.(A) // 这里我们断言为结构体是不会报错的,go会帮我们自动解引用,但是这样我们后续的修改就不会影响传过来的参数了
    n, _ := boy.(*A)
    n.name = "Mr." + n.name
}

反射

go提供了一种机制,能够在运行时更新变量和检查它们的值,调用他们的方法和它们支持的内在操作,而不需要在编译时就知道这些变量的具体类型。

go中reflect包实现了运行时反射。reflect包会帮助识别interface{}变量的底层具体类型和具体值

func main() {
    reflectType(123) //int
    reflectType("abc") //string
}
​
func reflectType(x interface{}) {
    obj := reflect.TypeOf(x)
    fmt.Println(obj)
}

reflect.Typeof()类型不同,Kind()代表一个大的种类:

type Book struct {
    value string
}
​
func main() {
    book := Book{
        value: "《自行车》",
    }
    reflectType(book)
}
​
func reflectType(x interface{}) {
    typeb := reflect.TypeOf(x)
    kind := typeb.Kind() //注意这里是对reflect.TypeOf(x)返回的值进行.kind
    fmt.Println(typeb)
    fmt.Println(kind)
}
//main.Book 类型
//struct 种类

我们也可以通过NumField来返回字段的数量:

type Book struct {
    label string
    value int
}
​
func main() {
    book := Book{
        label: "《自行车》",
        value: 12,
    }
    reflectType(book)
}
​
func reflectType(x interface{}) {
    typeb := reflect.TypeOf(x)
    kind := typeb.Kind()
    if kind == reflect.Struct {
        fmt.Println(reflect.ValueOf(x).NumField())
    }
}
// 2 

同时我们可以和reflect.ValueOf(obj).Field(index)搭配起来,对结构体进行遍历:

type person struct {
    name string
    age  int
}
func main() {
    v := reflect.ValueOf(person{"steve", 30})
    count := v.NumField()
    for i := 0; i < count; i++ {
        f := v.Field(i)
        fmt.Println(f)
    }
}
// steve
// 30

也可以通过

func (v Value) FieldByIndex(index []int) Value
func (v Value) FieldByName(name string) Value

来拿到对应下标和对应属性名的值

结构体标签

在结构体上使用反引号加上字符串称为Tag,通常写作键值对的形式:

type person struct {
    Name string `json:"name"`          //json 包只识别以大写字母开头的属性
    Age  int    `json:"age,omitempty"` //加上omitempty当字段为空的时候,不填充该字段
}
​
func main() {
    p := reflect.TypeOf(person{})
    name, _ := p.FieldByName("Name")
    tag := name.Tag
    keyValue, _ := tag.Lookup("json")
    fmt.Printf("tag和key值:%s %s\n", tag, keyValue)
}
//tag和key值:json:"name" name

tag可用作json的encode:

type person struct {
    Name string `json:"name"`          //json 包只导出以大写字母开头的属性,我们通过标签改变其导出时的格式
    Age  int    `json:"age,omitempty"` //加上omitempty当字段为空的时候,不填充改字段
}
​
func main() {
    var v person
    v = person{
        Name: "steve",
    }
    data, err := json.Marshal(v)
    if err == nil {
        fmt.Printf("%s", data)
    }
}
//{"name":"steve"} 在JSON序列化的时候会根据结构体标签导出对应格式