暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【Golang】图解类型断言

幼麟实验室 2020-07-16
783

还在死记硬背类型断言的“规则”?不如一起画一画~




我们已经知道接口可以分为空接口与非空接口两类。相对于接口这种“抽象类型”,像int,slice,string,map,struct等类型被称为“具体类型”。
类型断言是Go语言中应用在接口值上的一个神奇特性而类型断言的目标类型可以是某种具体类型,也可以是某种非空接口类型。这样我们就组合出四种类型断言,接下来就逐一看看它们究竟是如何断言的!



01


空接口.(具体类型)



    var e interface{}
    //......
    r,ok := e.(*os.File)
    e.(*os.File)就是要判断e存储的_type是否指向*os.File的类型元数据。反正我们介绍过Go语言里每种类型的类型元数据都是全局唯一的。


    图:类型断言e.(*os.File)

      f,_ := os.Open("eggo.txt")
      e = f
      如果e像上面这样赋值,e的动态类型就是*os.File,所以e.(*os.File)类型断言成功,ok为true,r为e的动态值。


      图:e.(*os.File)类型断言成功

        f := "eggo"
        e = f
        如果e的动态类型不是*os.File,例如e被赋值为string,那么类型断言就会失败,ok就是false,r就是*os.File的类型零值nil。


        e.(*os.File)类型断言失败



        02


        非空接口.(具体类型)



          var rw io.ReadWriter
          //......
          r,ok := rw.(*os.File)

          rw.(*os.File)是要判断rw的动态类型是否为*os.File。前面我们介绍过,程序中用到的itab结构体都会缓存起来,可以通过<接口类型, 动态类型>组合起来的key,查找到对应的itab指针。
          所以这里的类型断言只需要一次比较就能完成,就是看iface.tab是否等于<io.ReadWriter, *os.File>这个组合对应的itab指针就好。



          图:类型断言rw.(*os.File)

          如果把一个*os.File类型的变量f赋给rw,它的动态值就是f,动态类型就是*os.File。
            f,_ := os.Open("eggo.txt)
            rw = f
            rw这里存储的itab指针就指向<io.ReadWriter, *os.File>组合对应的itab,所以类型断言成功,ok为true,r被赋值为rw的动态值。


            图:rw.(*os.File)类型断言成功

            下面我们定义一个eggo类型,并且由*eggo类型实现io.ReadWriter接口。
              type eggo struct {
                  name string
              }


              func (e *eggo) Read(b []byte) (n int, err error) {
                  return len(e.name), nil
              }


              func (e *eggo) Write(b []byte) (n int, err error) {
                  return len(e.name), nil
              }
              如果把一个*eggo类型的变量赋值给rw,rw的动态类型就是*eggo,rw持有的itab指针就不指向<io.ReadWriter, *os.File>组合对应的itab结构体,类型断言就会失败,ok为false,而r就会被置为*os.File的类型零值nil。
                := eggo{name: "eggo"}
                rw = &f

                图:rw.(*os.File)类型断言失败




                03


                空接口.(非空接口)


                 
                  var e interface{}
                  //......
                  rw,ok := e.(io.ReadWriter)
                  e.(io.ReadWriter)就是要判断e的动态类型是否实现了io.ReadWriter接口。


                  图:类型断言e.(io.ReadWriter)

                  如果e像这样赋值:
                    f,_ := os.Open("eggo.txt")
                    e = f

                    e的动态类型就是*os.File,我们知道*os.File类型元数据的后面可以找到该类型实现的方法列表描述信息。



                    图:e赋值为*os.File类型


                    其实并不需要每次都检查动态类型的方法列表,还记得itab缓存吗? 实际上,当类型断言的目标类型为非空接口时,会首先去itabTable里查找对应的itab指针,若没有找到,再去检查动态类型的方法列表。
                    此处注意,就算从itabTable中找到了itab指针,也要进一步确认itab.fun[0]是否等于0。这是因为一旦通过方法列表确定某个具体类型没有实现指定接口,就会把itab这里的fun[0]置为0,然后同样会把这个itab结构体缓存起来,和那些断言成功的itab缓存一样。这样做的目的是避免再遇到同种类型断言时重复检查方法列表。

                    回到例子中,这里会断言成功,
                    ok为true,rw就是一个io.ReadWriter类型的变量,其动态值与e相同。tab指向<io.ReadWriter, *os.File>对应的itab结构体。


                    图:e.(io.ReadWriter)类型断言成功


                      f := "eggo"
                      e = f

                      然而如果把一个字符串赋值给e,它的动态类型就是string,<io.ReadWriter, string>这个组合会对应下面这个itab,它也会被缓存起来。



                      图:断言失败的itab


                      断言失败,ok为false,rw为io.ReadWriter的类型零值,即tab和data均为nil。



                      图:e.(io.ReadWriter)类型断言失败





                      04


                      非空接口.(非空接口)


                        var w io.Writer
                        //......
                        rw,ok := w.(io.ReadWriter)
                        w.(io.ReadWriter)是要判断w的动态类型是否实现了io.ReadWriter接口。


                        图:类型断言w.(io.ReadWriter)

                        下面同样把一个*os.File类型的变量f赋值给w,它的动态值就是f,动态类型就是*os.File。
                          f,_ := os.Open("eggo.txt")
                          w = f



                          要确*os.File是否实现了io.ReadWriter接口,同样会先去itab缓存里查找<io.ReadWriter,*os.File>对应的itab,若存在,且itab.fun[0]不等于0,则断言成功;若不存在,再去检查*os.File的方法列表,创建并缓存itab信息。

                          这里断言成功,ok为true,rw为io.ReadWriter类型的变量,动态值与w相同,而itab是<io.ReadWriter, *os.File>对应的那一个。



                          图:w.(io.ReadWriter)断言成功


                          下面我们自定义一个eggo类型,且*eggo类型只实现io.Writer要求的Write方法,并没有实现io.ReadWriter额外要求的Read方法。如果把一个*eggo类型的变量赋给w:

                            type eggo struct {
                            name string
                            }


                            func (e *eggo) Write(b []byte) (n int, err error) {
                            return len(e.name), nil
                            }


                            f := eggo{name: "eggo"}
                            w = &f

                            此时,w的动态类型为*eggo,而*eggo的方法列表里缺少一个Read方法,所以类型断言失败,下面这个itab被缓存起来。



                            断言失败后,ok为false,rw的data和tab均为nil。



                            图:w.(io.ReadWriter)类型断言成功


                            综上,类型断言的关键是明确接口的动态类型,以及对应的类型实现了哪些方法。而明确这些的关键,还是类型元数据,以及空接口与非空接口的数据结构。接下来的Type Switch也不外如是。




                            05


                            Type Switch



                              var e interface{}
                              str := "eggo"
                              e = str


                              switch b := e.(type) {
                              case *os.File:
                              {
                              fmt.Println("*os.File")
                              }
                              case string:
                              {
                              fmt.Println(b) //选择这个分支
                              }
                              default:
                              fmt.Println("default")
                              }
                              这里的b会被赋值为e的动态值,下面每个case都是把e的动态类型和某个具体类型作比较,相等则选择这个分支,没有匹配的则走到default分支。


                              图:type switch选择string类型分支

                              有时会遇到多个类型放在一个分支的情况,这时b的类型是interface{}。

                                switch b := e.(type) {
                                case *os.File:
                                {
                                fmt.Println("这里b的类型为*os.File", b)
                                }
                                case string:
                                {
                                fmt.Println("这里b的类型为string", b)
                                }
                                case intint32int64:
                                {
                                fmt.Println("多类型分支里b的类型为interface{}", b)
                                }
                                default:
                                    fmt.Println("default")
                                }





                                文章转载自幼麟实验室,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                评论