7

浅谈go面向对象与接口

 2 years ago
source link: https://xychen.github.io/post/2017-05-25-go-oop/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

一、面向对象

go面向对象

  • go中没有类的概念,面向对象是通过结构体来实现的
  • 结构体可以定义属性,也可以定义成员方法
// 定义一个结构体
type Point struct{ X, Y float64 }

// 普通函数
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// Point 类型的方法
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}
p := Point{1, 2}
q := Point{4, 6}
//普通方法调用
fmt.Println(Distance(p, q)) // "5", function call

// 结构体方法调用
fmt.Println(p.Distance(q))  // "5", method 

两个函数的名字都叫Distance,但是没有发生冲突,第一个属于包级别的,第二个属于Point类型的
上边类方法的代码里的参数p,叫做方法的receiver (其它语言中的this or self)
不同的receiver 也可以有相同的方法名 (和类其实一样的概念)

//定义了一个线段,多个Point组成,即Point的Slice
type Path []Point

func (path Path) Distance() float64 {
    sum := 0.0
    for i := range path {
        if i > 0 {
            //调用Point的Distance
            sum += path[i-1].Distance(path[i])
        }
    }
    return sum
}

基于指针对象的方法

函数前receiver和函数的参数一样,是一个值拷贝,我们也可以用传递指针的方式

// 放大
func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

类型名本身是一个指针的话,不能做为receiver

type P *int
func (P) f() {/*....*/}         //编译错误

指针receiver方法的调用

r := &Point{1, 2}
r.ScaleBy(2)
fmt.Println(*r)     // "{2, 4}"

p := Point{1, 2}
(&p).ScaleBy(2)

当然我们也可以简单方式调用: 如果我们调用的时候,给出的实际类型是一个Point类型,但是方法需要一个Point指针类型的receiver,编译器会隐式的帮助我们用&p去调用。这种简写方法只能用于可以取到地址的变量

//还可以直接写
p := Point{1, 2}
p.ScaleBy(2)
Point{1, 2}.ScaleBy(2)      //编译错误

实际类型是一个Point指针类型,但是方法需要一个Point的receiver,编译器也会隐式转换,自动*p解引用,通过地址找到这个变量

实际类型 接收器类型 操作
T T ✔️
*T *T ✔️
T *T 隐式转换,&取地址
*T T 隐式转换,*操作

方法值和方法表达式

可以把方法复制给一个变量,让其使用默认的对象

p := Point{1, 2}
q := Point{4, 6}

distanceFromP := p.Distance
distanceFromP(q)    //等价于 p.Distance(q)

scaleP := p.ScaleBy
scaleP(2)    //等价于 p.ScaleBy(2)

将对象方法转成普通的函数

p := Point{1, 2}
q := Point{4, 6}
//变成普通方法
distance := Point.Distance
fmt.Println(distance(p, q))
fmt.Printf("%T", distance)   //func (Point, Point) float64

scale := (*Point).ScaleBy
fmt.Println(scale(&p, 2))
fmt.Printf("%T", scale)   //func (*Point, float64)

如果goroutine和channel是支撑起Go语言的并发模型的基石,那么接口是Go语言整个类型系统的基石。

接口的约定

其它语言中,要实现某个接口,需要显式的进行指出
非入侵式接口:只要一个类实现了接口要求的所有函数,我们就说这个类实现了该接口

package main

import "fmt"
import "math"

type geometry interface {
    area() float64      //面积
    perim() float64     //周长
}

// rect类型实现了geometry接口中所有的方法,即实现了这个接口
type rect struct {
    width, height float64
}
func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

// circle类型实现了geometry接口中所有的方法,即实现了这个接口
type circle struct {
    radius float64
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

// geometry接口类型
func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}
    // rect、circle都实现了geometry接口,因此可以作为measur(测量)函数的参数
    measure(r)
    measure(c)
}

系统包中的接口: Printf和Sprintf都是通过调用Fprintf实现的,Fprintf第一个参数是一个io.Writer接口

package io

type Writer interface {
    Write(p []byte) (n int, err error)
}

package fmt

//实现了io.Writer接口
func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)

func Printf(format string, args ...interface{}) (int, error) {
    return Fprintf(os.Stdout, format, args...)      //输出到标准输出当中
}

func Sprintf() {
    var buf bytes.Buffer
    Fprintf(&buf, format, args...)      //输出到buffer中
    return buf.String()
}

自己实现一个io.Writer接口

type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
    *c += ByteCounter(len(p))
    return len(p), nil
}

var c ByteCounter
//传递的指针,因此c的值变成了字符串的长度
c.Write([]byte("hello"))
fmt.Println(c)      // 5, =len("hello")

c = 0   //reset
var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name)
fmt.Println(c)      //12, = len("hello, Dolly")

实现接口的条件

接口指定的规则非常简单:表达一个类型属于某个接口只要这个类型实现了这个接口

var w io.Writer
w = os.Stdout           //ok
w = new(bytes.Buffer)   //ok
w = time.Second         //编译失败

空接口类型,因为空接口类型对实现它的类型没有要求,所以可以将任意一个值赋值给空接口类型

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

一个接口的值由两部分组成,一个具体的类型和对应这个类型的值,他们被称为接口的动态类型和动态值

根据实例看一下接口值的变化

var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
var w io.Writer

初始化,赋予接口零值,它的类型和值的部分都是nil

type value
nil nil

调用空接口值上的任意方法都会产生panic:

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

第二个语句,将一个*os.File类型复制给变量w

type value
*os.File fd int=1(stdout), 即标准输出类型描述符
w = new(bytes.Buffer)

第三个语句,将一个*bytes.Buffer类型复制给变量w

type value
*bytes.Buffer data []byte, 即内存缓冲区

第四个语句,将nil赋值给接口值,重置成初始化的状态

使用%T可以查看接口的真实类型

var w io.Writer
fmt.Printf("%T\n", w)  // nil

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

w = new(bytes.Buffer)
fmt.Printf("%T\n", w)  // *bytes.Buffer

一个小坑:包含nil指针的接口不是nil接口

func f(out io.Writer) {
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

var buf *bytes.Buffer   //未初始化
f(buf)      //panic

当buf作为实参传递给out时,out的状态是这样的:

type value
*bytes.Buffer nil

也就是说它的值是nil,但是类型不是nil, 所以 out != nil 的结果依然是true,引发错误

var buf io.Writer
f(buf)      //ok
// 语法x.(T)

var w io.Writer
w = os.Stdout

f := w.(*os.File)   //success
c := w.(*bytes.Buffer)  //pannic


//可以使用这种语法
if c, ok := w.(*os.File); ok {
    /..../
}

if c, ok := w.(*bytes.Buffer); ok {
    /..../
}

举个简单的例子

net/http

package http
type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error
type dollars float32
func (d dollars) String() string {
    return fmt.Sprintf("%.2", d)
}

type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"shoes": 50, "sock": 5}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK