GO——接口(下)
创始人
2024-05-23 23:56:46
0

接口

  • 接口值
    • 警告:一个包含空指针值的接口不是nil接口
  • `sort.Interface`接口
  • `http.Handler`接口
  • 类型断言
  • 类型分支

接口值

接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像Go语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
  • 第一个语句定义了变量w:

    var w io.Writer
    

    变量总是被一个定义明确的值初始化,即使接口类型也不例外。对于一个接口的零值就是它的类型和值的部分都是nil。

  • 第二个语句将一个*os.File类型的值赋给变量w:

    w = os.Stdout
    

    这个赋值过程调用了一个具体类型到接口类型的隐式转换,这和显式的使用io.Writer(os.Stdout)是等价的。这类转换不管是显式的还是隐式的,都会刻画出操作到的类型和值。这个接口值的动态类型被设为*os.File指针的类型描述符,它的动态值持有os.Stdout的拷贝;这是一个代表处理标准输出的os.File类型变量的指针。

    调用一个包含*os.File类型指针的接口值的Write方法,使得(*os.File).Write方法被调用。

    效果和下面这个直接调用一样:

    os.Stdout.Write([]byte("hello")) // "hello"
    

接口值可以使用和!=来进行比较。两个接口值相等仅当它们都是nil值,或者它们的动态类型相同并且动态值也根据这个动态类型的操作相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

然而,如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(比如切片),将它们进行比较就会失败并且panic

当我们处理错误或者调试的过程中,得知接口值的动态类型是非常有帮助的。所以我们使用fmt包的%T动作:

var w io.Writer
fmt.Printf("%T\n", w) // ""
w = os.Stdout
fmt.Printf("%T\n", w) // "*os.File"

警告:一个包含空指针值的接口不是nil接口

关键:一个接口只有动态类型和动态值都是nil,对其进行==nil判断时才为true,否则为false

针对赋值的时候,如果两者类型不同,那么即使传入的值是nil,但是被赋值(接口)的动态类型会被更改为传入的类型

比如:

const debug = truefunc main() {var buf *bytes.Bufferif debug {buf = new(bytes.Buffer) // enable collection of output}f(buf) // NOTE: subtly incorrect!if debug {// ...use buf...}
}// If out is non-nil, output will be written to it.
func f(out io.Writer) {// ...do something...if out != nil {out.Write([]byte("done!\n"))}
}

此时如果debug=false,那么调用函数f()时,传入的类型是*bytes.Buffer,与参数类型不一致,那么此时out的动态类型就是*bytes.Buffer。根据前文所说,接口的动态类型不为空,那么此时out!=nil就为true,而不是预期的false了。

修改方式如下:

var buf io.Writer

此时调用f()方法时,传入的类型与参数类型一致,所以此时的out动态类型和动态值都是nil,也就不会出现问题了。

sort.Interface接口

一个内置的排序算法需要知道三个东西:序列的长度,表示两个元素比较的结果,一种交换两个元素的方式;这就是sort.Interface的三个方法:

package sorttype Interface interface {Len() intLess(i, j int) bool // i, j are indices of sequence elementsSwap(i, j int)
}

按照一种类型的某个值排序:

type byArtist []*Track
func (x byArtist) Len() int           { return len(x) }
func (x byArtist) Less(i, j int) bool { return x[i].Artist < x[j].Artist }
func (x byArtist) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }sort.Sort(byArtist(tracks))

对tracks进行逆向排序。然而我们不需要定义一个有颠倒Less方法的新类型byReverseArtist,因为sort包中提供了Reverse函数将排序顺序转换成逆序。Z

sort.Sort(sort.Reverse(byArtist(tracks)))

sort包定义了一个不公开的struct类型reverse,它嵌入了一个sort.Interface。reverse的Less方法调用了内嵌的sort.Interface值的Less方法,但是通过交换索引的方式使排序结果变成逆序。

package sorttype reverse struct{ Interface } // that is, sort.Interfacefunc (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) }func Reverse(data Interface) Interface { return reverse{data} }

对于我们需要的每个切片元素类型和每个排序函数,我们需要定义一个新的sort.Interface实现。

Len和Swap方法对于所有的切片类型都有相同的定义。下个例子,具体的类型customSort会将一个切片和函数结合,使我们只需要写比较函数就可以定义一个新的排序。

type customSort struct {t    []*Trackless func(x, y *Track) bool
}func (x customSort) Len() int           { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int)    { x.t[i], x.t[j] = x.t[j], x.t[i] }sort.Sort(customSort{tracks, func(x, y *Track) bool {if x.Title != y.Title {return x.Title < y.Title}if x.Year != y.Year {return x.Year < y.Year}if x.Length != y.Length {return x.Length < y.Length}return false
}})

sort包中的IsSorted函数帮我们做这样的检查。

values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // "false"
//对ints排序
sort.Ints(values)
fmt.Println(values)                     // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
//对int切片排序
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values)                     // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"

http.Handler接口

package httptype Handler interface {ServeHTTP(w ResponseWriter, r *Request)
}func ListenAndServe(address string, h Handler) error

ListenAndServe函数需要一个例如“localhost:8000”的服务器地址,和一个所有请求都可以分派的Handler接口实例。它会一直运行,直到这个服务因为一个错误而失败(或者启动失败),它的返回值一定是一个非空的错误。

func main() {db := database{"shoes": 50, "socks": 5}log.Fatal(http.ListenAndServe("localhost:8000", db))
}type dollars float32func (d dollars) String() string {return fmt.Sprintf("$%.2f", d)
}type database map[string]dollarsfunc (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {for item, price := range db {fmt.Fprintf(w, "%s:%s", item, price)}
}

满足同一接口的不同类型是可替换的。

显然我们可以继续向ServeHTTP方法中添加case,但在一个实际的应用中,将每个case中的逻辑定义到一个分开的方法或函数中会很实用。net/http包提供了一个请求多路器ServeMux来简化URL和handlers的联系。一个ServeMux将一批http.Handler聚集到一个单一的http.Handler中。

func main3() {db := database{"shoes": 50, "socks": 5}mux := http.NewServeMux()mux.Handle("/list", http.HandlerFunc(db.list))mux.Handle("price", http.HandlerFunc(db.price))log.Fatal(http.ListenAndServe("localhost:8000", mux))
}func (db database) list(w http.ResponseWriter, req *http.Request) {for item, price := range db {fmt.Fprintf(w, "%s:%s\n", item, price)}
}func (db database) price(w http.ResponseWriter, req *http.Request) {item := req.URL.Query().Get("item")price, ok := db[item]if !ok {w.WriteHeader(http.StatusNotFound)fmt.Fprintf(w, "no such item:%q\n", item)return}fmt.Fprintf(w, "%s\n", price)
}

语句http.HandlerFunc(db.list)是一个转换而非一个函数调用,因为http.HandlerFunc是一个类型。它有如下的定义:

package httptype HandlerFunc func(w ResponseWriter, r *Request)func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {f(w, r)
}

ServeHTTP方法的行为是调用了它的函数本身。因此HandlerFunc是一个让函数值满足一个接口的适配器,这里函数和这个接口仅有的方法有相同的函数签名。

net/http包提供了一个全局的ServeMux实例DefaultServerMux和包级别的http.Handlehttp.HandleFunc函数。现在,为了使用DefaultServeMux作为服务器的主handler,我们不需要将它传给ListenAndServe函数;nil值就可以工作。相当于就是如果我们不给ListenAndServe函数传入特定的ServerMux,或者直接调用http.HandlerFunc函数的话,就会去使用特定的全局ServerMux实例。

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。类型断言检查x的动态值类型是否和T相同,返回结果的真实值是x的动态值,类型改为T

这里有两种可能:

第一种,如果断言的类型T是一个具体类型,然后类型断言检查x的动态类型是否和T相同。换句话说,具体类型的类型断言从它的操作对象中获得具体的值。如果检查失败,接下来这个操作会抛出panic。例如:

var w io.Writer
w = os.Stdout
f := w.(*os.File)      // success: f == os.Stdout
c := w.(*bytes.Buffer) // panic: interface holds *os.File, not *bytes.Buffer

第二种,如果断言的类型T是一个接口类型,然后类型断言检查是否x的动态类型满足T。

对一个接口类型的类型断言改变了类型的表述方式,改变了可以获取的方法集合(通常更大),但是它保留了接口值内部的动态类型和值的部分。在下面的第一个类型断言后,wrw都持有os.Stdout,因此它们都有一个动态类型*os.File,但是变量w是一个io.Writer类型,只对外公开了文件的Write方法,而rw变量还公开了它的Read方法。

var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method

经常地,对一个接口值的动态类型我们是不确定的,并且我们更愿意去检验它是否是一些特定的类型。如果类型断言出现在一个预期有两个结果的赋值操作中,例如如下的定义,这个操作不会在失败的时候发生panic,但是替代地返回一个额外的第二个结果,这个结果是一个标识成功与否的布尔值:

var w io.Writer = os.Stdout
f, ok := w.(*os.File)      // success:  ok, f == os.Stdout
b, ok := w.(*bytes.Buffer) // failure: !ok, b == nil

第二个结果通常赋值给一个命名为ok的变量。如果这个操作失败了,那么ok就是false值,第一个结果等于被断言类型的零值,在这个例子中就是一个nil的*bytes.Buffer类型。

这个ok结果经常立即用于决定程序下面做什么。if语句的扩展格式让这个变的很简洁:

if f, ok := w.(*os.File); ok {// ...use f...
}

例子:

func writeString(w io.Writer, s string) (n int, err error) {type stringWriter interface {WriteString(string) (n int, err error)}if sw, ok := w.(stringWriter); ok {return sw.WriteString(s) // avoid a copy}return w.Write([]byte(s)) // allocate temporary copy
}

满足以下接口即可调用:

interface {io.WriterWriteString(s string) (n int, err error)
}

类型分支

接口被以两种不同的方式使用。

在第一个方式中,以io.Readerio.Writerfmt.Stringersort.Interfacehttp.Handlererror为典型,一个接口的方法表达了实现这个接口的具体类型间的相似性,但是隐藏了代码的细节和这些具体类型本身的操作。重点在于方法上,而不是具体的类型上。

第二个方式是利用一个接口值可以持有各种具体类型值的能力,将这个接口认为是这些类型的联合。类型断言用来动态地区别这些类型,使得对每一种情况都不一样。在这个方式中,重点在于具体的类型满足这个接口,而不在于接口的方法(如果它确实有一些的话),并且没有任何的信息隐藏。我们将以这种方式使用的接口描述为discriminated unions(可辨识联合)。

一个类型分支像普通的switch语句一样,它的运算对象是x.(type)——它使用了关键词字面量type——并且每个case有一到多个类型。一个类型分支基于这个接口值的动态类型使一个多路分支有效。

switch x.(type) {
case nil:       // ...
case int, uint: // ...
case bool:      // ...
case string:    // ...
default:        // ...
}

常见用法:

将提取的值绑定到一个在每个case范围内都有效的新变量。

switch x := x.(type) { /* ... */ }

也就是说当匹配到具体某个单一类型case的时候,变量x和这个case的类型相同

例如:

func sqlQuote(x interface{}) string {switch x := x.(type) {case nil:return "NULL"case int, uint:return fmt.Sprintf("%d", x) // x has type interface{} here.case bool:if x {return "TRUE"}return "FALSE"case string:return sqlQuoteString(x) // (not shown)default:panic(fmt.Sprintf("unexpected type %T: %v", x, x))}
}

变量x在bool的case中是bool类型,在string的case中是string类型。

相关内容

热门资讯

张岱的人物生平 张岱的人物生平张岱(414年-484年),字景山,吴郡吴县(今江苏苏州)人。南朝齐大臣。祖父张敞,东...
西游西后传演员女人物 西游西后传演员女人物西游西后传演员女人物 孙悟空 六小龄童 唐僧 徐少华 ...
名人故事中贾岛作诗内容简介 名人故事中贾岛作诗内容简介有一次,贾岛骑驴闯了官道.他正琢磨着一句诗,名叫《题李凝幽居》全诗如下:闲...
和男朋友一起优秀的文案? 和男朋友一起优秀的文案?1.希望是惟一所有的人都共同享有的好处;一无所有的人,仍拥有希望。2.生活,...
戴玉手镯的好处 戴玉手镯好还是... 戴玉手镯的好处 戴玉手镯好还是碧玺好 女人戴玉?戴玉好还是碧玺好点佩戴手镯,以和田玉手镯为佳!相嫌滑...
依然什么意思? 依然什么意思?依然(汉语词语)依然,汉语词汇。拼音:yī    rán基本解释:副词,指照往常、依旧...
高尔基的散文诗 高尔基的散文诗《海燕》、《大学》、《母亲》、《童年》这些都是比较出名的一些代表作。
心在飞扬作者简介 心在飞扬作者简介心在飞扬作者简介如下。根据相关公开资料查询,心在飞扬是一位优秀的小说作者,他的小说作...
卡什坦卡的故事赏析? 卡什坦卡的故事赏析?讲了一只小狗的故事, 我也是近来才读到这篇小说. 作家对动物的拟人描写真是惟妙...
林绍涛为简艾拿绿豆糕是哪一集 林绍涛为简艾拿绿豆糕是哪一集第三十二集。 贾宽认为是阎帅间接导致刘映霞住了院,第二天上班,他按捺不...
小爱同学是女生吗小安同学什么意... 小爱同学是女生吗小安同学什么意思 小爱同学,小安同学说你是女生。小安是男的。
内分泌失调导致脸上长斑,怎么调... 内分泌失调导致脸上长斑,怎么调理内分泌失调导致脸上长斑,怎么调理先调理内分泌,去看中医吧,另外用好的...
《魔幻仙境》刺客,骑士人物属性... 《魔幻仙境》刺客,骑士人物属性加点魔幻仙境骑士2功1体质
很喜欢她,该怎么办? 很喜欢她,该怎么办?太冷静了!! 太理智了!爱情是需要冲劲的~不要考虑着考虑那~否则缘...
言情小说作家 言情小说作家我比较喜欢匪我思存的,很虐,很悲,还有梅子黄时雨,笙离,叶萱,还有安宁的《温暖的玄》 小...
两个以名人的名字命名的风景名胜... 两个以名人的名字命名的风景名胜?快太白楼,李白。尚志公园,赵尚志。
幼儿教育的代表人物及其著作 幼儿教育的代表人物及其著作卡尔威特的《卡尔威特的教育》,小卡尔威特,他儿子成了天才后写的《小卡尔威特...
海贼王中为什么说路飞打凯多靠霸... 海贼王中为什么说路飞打凯多靠霸气升级?凯多是靠霸气升级吗?因为之前刚到时确实打不过人家因为路飞的实力...
运气不好拜财神有用吗运气不好拜... 运气不好拜财神有用吗运气不好拜财神有没有用1、运气不好拜财神有用。2、拜财神上香前先点蜡烛,照亮人神...
“犹如……,好像……。”如何造... “犹如……,好像……。”如何造句?用“犹如……,好像……。”造句例子如下:岁月犹如长河,人生如同奔流...