接下来我们将具体介绍go源码中提供的一些使用了接口技术的一些例子。
sort.Interface接口
像很多其他语言会为每一种类型写一个排序函数,并通过方法的方式调用,而go可以
仅通过一个 sort.Sort 方法给任意类型的序列进行排序的,可以是一个字符串(字符串切片),map,切片或者结构体等。
下面我们看看,sort.Sort方法是如何实现对任意类型排序的:
// sort.Sort源码
func Sort(data Interface) {
n := data.Len()
quickSort(data, 0, n, maxDepth(n))
}
首先Sort方法要求传入一个 sort.Interface 的接口类型,这个接口如下,它要求实现3个方法:
// 源码
type Interface interface {
Len() int // 获取序列大小
Less(i, j int) bool // 两个元素比较的结果
Swap(i, j int) // 一种交换两个元素的方式
}
Sort()内部会调用序列的这3个方法来进行排序,不同的序列类型通过不同的实现这3个方法的方式可以定义属于自己定制化的排序策略。
凡是实现了这个接口的类型的序列都可以用sort.Sort()来排序,这也是Sort()可以给任意类型序列(包括自定义类型,只要你实现了这三个方法)排序的原因。
注意:像这种只定义了函数名,参数列表和返回值列表但是没有定义函数体的函数,我们称之为函数签名。
那么下面我们可以自己写一个实现了这3个方法的类型(这个类型是专门用来给字符串切片排序的),并用这个类型结合sort.Sort来给一个字符串切片排序:
type StringSlice []string // StringSlice 类型实现了 sort.Interface 接口类型
func (s StringSlice) Len() int { return len(s) }
func (s StringSlice) Less(i, j int) bool { return s[i] < s[j] } // 升序排序
func (s StringSlice) Swap(i, j int) { s[i], s[j] = s[j],s[i] } // 交换元素
func main() {
strslice := []string{"oen","ozy", "vyag", "iem", "abc"}
ss := StringSlice(strslice) // 强制转换为 StringSlice 类型,目的是让传入sort.Sort的参数是sort.Interface类型
sort.Sort(ss) // sort.Sort 无返回值,会影响原切片
fmt.Println(ss)
}
这里有个小坑,不要写成 strslice = StringSlice(strslice) ,而是另起一个变量 ss来接收,原因是strslice的类型已经确定,始终是[]string,如果用strslice接收,就又把StringSlice类型转回了[]string,这样的话传入sort.Sort会报错,因为[]string不是Interface类型,StringSlice才是。
sort包本身提供了StringSlice类型,也提供了Strings函数能让上面这些调用简化成sort.Strings(names)。
接下来我们尝试写一个给表格数据根据某列对行来排序。
type Track struct { // 唱片
Title string
Author string
Album string
Year int
Length time.Duration // 唱片时长
}
type TrackTable []*Track // 陈列唱片的表格
type ByTitle struct {TrackTable} // 嵌入TrackTable类型,因此ByTitle也拥有TrackTable的所有方法
// 为唱片表格定义排序相关的方法,实现sort.Interface接口
// 下面这3个是根据title排序所实现的方法
func (tracktable ByTitle) Len() int { return len(tracktable.TrackTable)}
func (tracktable ByTitle) Less(i, j int) bool { return tracktable.TrackTable[i].Title < tracktable.TrackTable[j].Title}
func (tracktable ByTitle) Swap(i, j int) { tracktable.TrackTable[i], tracktable.TrackTable[j] = tracktable.TrackTable[j], tracktable.TrackTable[i] }
// 将字符串时间转为 time.Duration 类型
func length(t string) time.Duration {
d, err := time.ParseDuration(t)
if err != nil {
panic(t)
}
return d
}
// 格式化输出表格
func PrintTable(table TrackTable) {
for _, line := range table {
fmt.Printf("%#v\n", line)
}
}
func main() {
var tracks = TrackTable {
{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Moby", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
// 强制转换为ByTitle类型得到一个 ByTitle类型 的 trackTable
trackTableByTitle := ByTitle{tracks}
sort.Sort(trackTableByTitle) // 会改变ByTitle类型内的TrackTable成员的元素顺序
PrintTable(trackTableByTitle.TrackTable) // 查看改变后的TrackTable 顺序
}
这个程序是根据表格中的Title这一列升序排序。
如果我希望能够根据Title倒序排序可以这样做:
sort.Sort(sort.Reverse(trackTableByTitle))
只需要调用Reverse这个方法即可,这个方法做了什么呢?其实你可以仔细想想,让一个排序从升序改为降序,Len和Swap方法是不变的,变的只有一个Less方法。所以我们只要在Reverse方法中定义一个临时的重新实现了Interface接口的新类型(下例中的reverse类型),并且改写其中的Less方法,最后再把trackTableByTitle这个变量的类型从原先的Interface类型的tracktable类型转变为这个新类型再返回即可。
下面我们看看具体的代码。
package sort
// 源码中的 sort.Interface 接口
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i,j int)
}
// 源码中的 reverse 类型,它嵌入(或者说继承)了 sort.Interface (接口)类型。此时Len/Less/Swap还未实现
// reverse由于嵌入了sort.Interface接口作为成员,因此它自己也是sort.Interface类型
type reverse struct{Interface}
//为 reverse 类型实现 Less 方法
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i) // 降序的比较
}
// sort包的Reverse方法。这个方法传入的是一个 sort.Interface 类型,返回的也是一个 sort.Interface类型。但是返回出来的sort.Interface类型的 Less行为已经被 reverse 方法给替换成降序的Less
func Reverse(s Interface) Interface {
return &reverse{s} // 把s作为成员传入reverse
}
需要注意的是,reverse 类型只实现了Less 方法,但是没有具体实现Len和Swap方法。当Sort调用reverse的Len和Swap方法的时候,reverse自己没有这两个方法,于是会找reverse的匿名成员Interface的Len和Swap方法,而这个Interface成员的实际值就是trackTableByTitle,所以会调用trackTableByTitle原有的Len和Swap方法。
reverse类型的Less方法是自己的,而Len和Swap方法不是自己的,而是其成员Interface(也就是trackTableByTitle)的方法。
这里需要注意的一点:
以前我们往一个结构体A嵌入匿名成员B的时候都是嵌入底层类型为结构体类型的新类型。
但是这里reverse新类型嵌入了一个接口。
当新类型A嵌入接口B的时候,A可以不实现B的方法,但是B的成员必须实现B的方法。比如下面这个例子,A可以不实现test方法,但是b必须实现test方法。
type B interface {
test()
}
type BInstance struct{}
func (b BInstance) test(){
// do something
}
type A struct{
B
otherAttrs []string
}
func main() {
var b B
b = BInstance{}
a := A{b, []string{}}
fmt.Println(a)
}
往一个结构体新类型嵌入一个接口类型的成员这样做会有什么好处?
好处是我们可以通过传入不同的成员B来控制A的行为的多样化(相同的方法名有不同的方法内容)。
举个例子:
现在有3个等级的工人:初高中级,他们都会造房子,但是他们造房子的方式不同。
// 工人接口 需要实现BuildHouse造房子这个方法
type Worker interface {
BuildHouse (materialNum int) int // 传入材料,返回能造出的房子的个数
}
// 人类
type Person struct{
Name string
Age int
}
// 随便的一个工人
type RandomWorker struct {
Worker // 嵌入工人接口
Person // 嵌入人类
// 这样 RandomWorker 就有两个匿名成员了
}
// 工人获取材料 初高中级工人获取材料的能力都是一样的 所有定义在RandomWroker的方法
func (rw RandomWorker) GetMaterial(times int) int{ // times是获取的次数
return times * 5 // 返回材料的数量
}
// 不同级别的工人
type PrimaryWorker struct {}
type MediumWorker struct {}
type SeniorWorker struct {}
func (pw PrimaryWorker) BuildHouse(materialNum int) int{
return materialNum
}
func (mw MediumWorker) BuildHouse(materialNum int) int{
return materialNum * 4
}
func (sw SeniorWorker) BuildHouse(materialNum int) int{
return materialNum * 9
}
func main(){
pw := PrimaryWorker{}
worker := RandomWorker{
Person:Person{Name:"zbp", Age:24},
Worker:pw,
}
material := worker.GetMaterial(3)
houses := worker.BuildHouse(material)
fmt.Printf("Build %d Houses", houses)
}
这个例子我把pw作为Worker成员传入了RandomWorker中,如果我想换一个强一点的工人,可以把sw或者mw作为Worker成员传入了RandomWorker中,使得RandomWorker的BuildHouse有不同的行为(不同造房子的效率)。
这个RandomWorker就很像是一个适配器。
那假如Worker不是一个interface接口类型,而是一个struct结构体类型,然后PrimaryWorker/MediumWorker/SeniorWorker这3个类型不是实现Worker而是继承Worker,这样会有什么不同?
此时你会发现 pw/sw/mw 根本无法传入 RandomWorker的Worker成员中,因为PrimaryWorker/MediumWorker/SeniorWorker 与 Worker 根本不是同一类型,这么一来不同等级的Worker类型就无法与RandomWorker适配!下面我们看看具体的代码实现:
// 工人类型
type Worker struct{
Name string
Age int
}
// 随便的一个工人
type RandomWorker struct {
Worker // 嵌入工人类型
}
// 工人获取材料 初高中级工人获取材料的能力都是一样的 所有定义为RandomWroker的方法
func (rw RandomWorker) GetMaterial(times int) int{ // times是获取的次数
return times * 5 // 返回材料的数量
}
// 不同级别的工人(嵌入Worker类型,这样他们就都有Name和Age成员了)
type PrimaryWorker struct {Worker}
type MediumWorker struct {Worker}
type SeniorWorker struct {Worker}
func (pw PrimaryWorker) BuildHouse(materialNum int) int{
return materialNum
}
func (mw MediumWorker) BuildHouse(materialNum int) int{
return materialNum * 4
}
func (sw SeniorWorker) BuildHouse(materialNum int) int{
return materialNum * 9
}
func main(){
pw := PrimaryWorker{}
worker := RandomWorker{
Worker:pw, // 报错,因为pw不是Worker类型而是PrimaryWorker类型
}
material := worker.GetMaterial(3)
houses := worker.BuildHouse(material)
fmt.Printf("Build %d Houses", houses)
}
所以这就是嵌入一个接口和嵌入一个结构体的本质区别:以接口类型作为成员就可以传入多种不同的类型(继承不同的类型),以结构体为类型就只能传入特定的那一种类型。可以说嵌入接口比嵌入结构体更加接近其他类型语言的继承这个行为。
回到排序的问 题上,现在我还有一个需求,我希望先根据title排序,再根据author排序,最后根据year排序。因此我希望能够定制化的编写一个Less方法来实现这个排序规则。我希望的是可以在调用sort.Sort的时候才临时去传入这个排序规则。
为了实现这一点,我 们可以思考如何实现。首先我需要创建一个新类型实现一个sort.Interface接口,而且这个新类型要有TrackTable这个唱片表格。然后我要为Less方法留空,我得在传入Sort方法时才传进这个Less方法,因此可以为这个Less预留一个成员的位置。
type Track struct { // 唱片
Title string
Author string
Album string
Year int
Length time.Duration // 唱片时长
}
type TrackTable []*Track // 陈列唱片的表格
type customSort struct {
TrackTable
less func (X, Y *Track) bool // 这里稍微有点奇特,它以一个函数作为成员
}
// 实现Len和Swap方法
func (cs customSort) Len() int { return len(cs.TrackTable)}
func (cs customSort) Less(i, j int) bool { return cs.less(cs.TrackTable[i], cs.TrackTable[j])}
func (cs customSort) Swap(i, j int) { cs.TrackTable[i], cs.TrackTable[j] = cs.TrackTable[j], cs.TrackTable[i]}
func main() {
var tracks = TrackTable {
{"Go", "Moby", "From the Roots Up", 2012, length("3m38s")},
{"Go", "Delilah", "Moby", 1992, length("3m37s")},
{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
}
// customSort 是一个 sort.Sort 类型
sort.Sort(customSort{TrackTable:tracks, less: func(x, y *Track) bool {
// x 和 y分别是 TrackTable 中的当前元素和下一个元素
if (x.Title != y.Title){ // 先根据Title升序排序
return x.Title < y.Title
}
if (x.Author != y.Author){ // 再根据Author升序排序
return x.Author < y.Author
}
if (x.Year != y.Year){ // 再根据Year
return x.Year < y.Year
}
return false
}})
PrintTable(tracks)
}
这个例子的特定是:Less方法的行为由cs的less成员(是个函数签名,没有实现的函数)决定,而less的行为又由开发者自行定义并且作为成员传入到customSort中。
最后作者介绍了一个检查切片是否已经排好序的方法,这里以整型切片为例:
values := []int{3, 1, 4, 1}
fmt.Println(sort.IntsAreSorted(values)) // "false"
sort.Ints(values)
fmt.Println(values) // "[1 1 3 4]"
fmt.Println(sort.IntsAreSorted(values)) // "true"
sort.Sort(sort.Reverse(sort.IntSlice(values)))
fmt.Println(values) // "[4 3 1 1]"
fmt.Println(sort.IntsAreSorted(values)) // "false"
IntsAreSorted方法内部其实还是要将values这个数字切片转为sort.Interface类型(具体是一个IntSlice类型)后再依托Len、Less和Swap方法来判断该切片是否排好序。
===================================================
http.Handler接口
Go提供了一种可以快速创建网络服务的函数 http.ListenAndServe()
func ListenAndServe(address string, h Handler) error
平时我们创建一个服务器要先创建套接字,绑定套接字,监听套接字,最后调用accept方法接受连接,调用 recv 接收请求,然后开始写处理请求的逻辑。
但是ListenAndServer帮我们省了前面的步骤,我们只需要创建一个http.Handler类型的请求处理器h(它其实相当于php框架中的控制器)。对 h 这个编写相应的处理请求的逻辑然后传入ListenAndServer中,所有请求都会按照h控制器中的逻辑进行处理并返回响应。
我们看看 http.Handler 接口是如何定义的:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
它需要实现一个ServeHTTP的方法,这个方法就是具体的处理请求的逻辑。
例子:
type User struct {
Name string
Age int
}
func (u User) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 向w响应流写入内容
fmt.Fprintf(w, "User Info\nName:%s\nAge:%d", u.Name, u.Age)
}
func main(){
zbp := User{Name:"zbp", Age:24}
http.ListenAndServe("127.0.0.1:8080", zbp)
}
这里我定义了一个User类型,存储了某个用户的信息。
定义了一个User的ServeHTTP 方法,所以User类型实现了http.Handler接口。
w参数是http.ResponseWriter类型,他是一个响应流,往一个响应流写数据就相当于返回响应给客户端。r 是客户端发送过来的请求信息。
w 和 r 是由ListenAndServer自动传入到ServeHTTP中的。
当调用 http.ListenAndServe("127.0.0.1:8080", zbp) 时,服务端就会创建套接字并绑定和监听这个ip和端口,开启一个服务,并等待客户端的连接和请求。
当我在浏览器访问 127.0.0.1:8080 的时候,客户端就连接到这个进程的服务并且客户端的请求会被分派给zbp这个请求处理器,调用zbp的ServeHTTP方法处理这个请求。
请求处理完毕会,需要向 w 这个响应流写入响应信息,这个消息就会经由网络发送给客户端。
这个服务会一直运行,除非因为一些异常导致服务停止,此时ListenAndServer会返回一个error错误类型
我们在看看作者提供的例子
type Goods map[string]dollars
func (g Goods) ServeHTTP(w http.ResponseWriter, r *http.Request){
for item, price := range g {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
type dollars float32
// 如果为一个类型定义了String方法,这个String方法会在格式化输出字符串(如调用fmt.Printf,fmt.Fprintf之类的)的时候自动调用
func (d dollars) String() string{ return fmt.Sprintf("$%.2f", d)}
func main(){
goods := Goods{
"shoes" : 50,
"socks" : 5,
}
log.Fatal(http.ListenAndServe("127.0.0.1:8080", goods))
}
现在我希望这个服务能够根据不同的uri返回不同的内容。这意味这要对不同的uri编写不同的控制器和处理逻辑。
一种比较low的方法就是在一个ServeHTTP方法中通过if或者switch将多个处理逻辑隔开,并判断url来对应相应的逻辑。这样会使得这个控制器的代码变得很长。
例如这样
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
}
}
为了解决这个问题,go的http包提供了 请求多路器 ServeMux 请求多路器ServeMux可以将多个请求逻辑从一个控制器handler给分离到多个控制器handler。如下所示:
我想通过 /list 这个url列出所有的商品信息,通过 /goods?name=shoes 这个url获取某一个商品的信息。
type Goods map[string]dollars
func (g Goods) list(w http.ResponseWriter, r *http.Request){
for item, price := range g {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (g Goods) goods(w http.ResponseWriter, r *http.Request){
// 获取请求参数 name
goodsName := r.URL.Query().Get("name")
// 判断是否存在这个商品
price, ok := g[goodsName]
if !ok {
w.WriteHeader(404) // 状态码设为404
fmt.Fprintf(w, "没有找到商品 %s\n", goodsName) // 还要返回响应信息
return
}
fmt.Fprintf(w, "%s:%s\n", goodsName, price)
}
type dollars float32
// 如果为一个类型定义了String方法,这个String方法会在格式化输出字符串(如调用fmt.Printf,fmt.Fprintf之类的)的时候自动调用
func (d dollars) String() string{ return fmt.Sprintf("$%.2f", d)}
func main(){
goods := Goods{
"shoes" : 50,
"socks" : 5,
}
// 创建一个请求多路器
mux := http.NewServeMux()
// 为这个请求多路器定义多个控制器处理多个url的逻辑
mux.Handle("/list", http.HandlerFunc(goods.list)) // 第二参要求传入一个http.Handler接口类型,但是Goods类型不是Handler,Goods.list和Goods.goods也不是,需要用http.HandlerFunc转成http.Handler类型(http.HandlerFunc是一个类型而不是函数,)
mux.Handle("/goods", http.HandlerFunc(goods.goods))
log.Fatal(http.ListenAndServe("127.0.0.1:8080", mux)) // mux看来是符合http.Handler接口类型的
}
这个例子的http.HandlerFunc用了一种比较巧妙的使用接口的技巧。它将一个方法(goods.list和goods.goods)转换成了http.HandlerFunc类型, 这个类型又是符合 http.Handler接口类型的类型。并且调用这个类型的ServeHTTP方法其实又会调用goods.list和goods.goods方法本身。它的实现如下:
package http
type HandlerFunc func(w ResponseWriter, r *Request) // HandlerFunc 的底层类型是一个匿名函数签名,这就是它可以对goods.list转换的原因,因为HandlerFunc的底层类型就是一个函数
// 实现ServeHTTP方法以实现 http.Handler 接口
func (hf HandlerFunc) ServeHTTP (w ResponseWriter, r *Request) {
hf(w, r) // 调用ServeHTTP其实就是调用hf接收器本身,也就是goods.list
}
其实go还提供了一种更加简便的写法,mux.Handle("/list", http.HandlerFunc(goods.list))可以替换为:
mux.HandleFunc("/list", goods.list)
它的内部实现如下:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
为了方便,net/http包提供了一种无需注册ServeMux的控制器handler,它是http.HandleFunc方法,我们只需要将goods.list和goods.goods方法注册到这个方法上即可,而且无需将goods传给ListenAndServe方法:
goods := Goods{
"shoes" : 50,
"socks" : 5,
}
http.HandleFunc("/list", goods.list)
http.HandleFunc("/goods", goods.goods)
log.Fatal(http.ListenAndServe("127.0.0.1:8080", nil))
=====================================================
error接口
我们在介绍函数的时候有说过error类型,其实error类型是一种接口类型:
type error interface {
Error() string
}
只要实现了Error这个方法的新类型都算是error类型。
Go提供了一个errors包,里面有一个New函数,这个New函数可以传入一个字符串并快速生成一个error类型的对象。其实New里面做的事情就是将这个字符串作为成员传到一个新类型(这个新类型实现了Error方法),并且将这个新类型变量返回给调用方。
errors.New函数和相关的结构体和方法大致是这个样子的
package errors
// New方法返回一个实现了Error方法的*err类型,这个类型符合error这个interface接口
func New(text string) (e *err){
return &err{errText:text}
}
// 定义一个新类型用来存放错误信息
type err struct {
errText string
}
// 为err类型的指针类型实现Error方法,因此*err是符合error接口的
func (e *err) Error() string {
return e.errText
}
这个例子教了我们一种运用接口的技巧:如何可以快速批量的生成一个符合某接口类型的对象,或者说如何方便的让一个变量实现某个接口。就可以用这种构造函数(在本例中是New函数)结合一个实现了接口的结构体(err类型),在这个构造函数中将一个类型(string)转为这个实现了接口的新类型(*err,err本身不是Error接口类型)并返回的方式快速的获取一个符合这个接口类型的对象。
Go源码中的errors 包真实内容如下:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
调用errors.New函数是非常稀少的,因为有一个方便的封装函数fmt.Errorf,它还会处理字符串格式化,而fmt.Errorf的内部其实还是用 errors.New来实现的。
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
*errorString可能是最简单的错误类型,但远非只有它一个。例如,syscall包提供了系统调用的函数。在多个平台上,它定义一个实现error interface的数字类型Errno,并且在Unix平台上,Errno的Error方法会从一个字符串表中查找错误消息
package syscall
type Errno uintptr // 系统调用的错误类型,这是一个error类型,因为实现了Error方法
// 预定义了一个错误信息的数组类型的映射表
var errors = [...]string{
1: "operation not permitted", // EPERM
2: "no such file or directory", // ENOENT
3: "no such process", // ESRCH
// ...
}
// 根据Errno类型的错误码获取错误信息
func (e Errno) Error() string {
if 0 <= int(e) && int(e) < len(errors) {
return errors[e]
}
return fmt.Sprintf("errno %d", e)
}
下面的语句创建了一个持有Errno值为2的接口值
var err error = syscall.Errno(2)
fmt.Println(err.Error()) // "no such file or directory"
fmt.Println(err) // "no such file or directory"