golang学习随便记8
Posted sjg20010414
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习随便记8相关的知识,希望对你有一定的参考价值。
接口
接口是对其他类型行为的概括和抽象,一个类型实现了某种接口,就是对使用方的一种承诺(因为它遵守了接口约定)。和常见的 OOP 语言不同,golang的接口是隐式实现的,一个类型并不会显式声明它实现了哪种接口,而是直接提供接口所必需的方法。这种方式让我们可以不改变已有类型的实现,就可以为它添加新的接口,对于不能修改的包中的类型,这一点有它的作用——感觉这和golang没有继承有关,因为有继承的 OOP ,会继承创建子类,同时去实现某个接口。
接口是约定
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(format string, args ...interface{}) string {
var buf bytes.Buffer
Fprintf(&buf, format, args...)
return buf.String()
}
Fprintf、Printf、Sprintf 都包含格式化部分,所以,通过接口机制来避免重复代码。Printf 直接调用 Fprintf,只是“文件”是标准输出。Sprintf 也调用 Fprintf,“文件”是字节缓冲区。这里,标准输出和字节缓冲区都可以作为“文件”,原因是 Fprintf 的第一个参数其实并非真正的文件,而是 io.Writer 接口类型,从而,凡是实现了 io.Writer 接口的类型,都可以作为 Fprintf 的“文件”。(实际golang标准库fmt包里的实现比这个要复杂)
type Writer interface {
Write(p []byte) (n int, err error)
}
io.Writer 定义为接口类型,它包含 Write 方法,参数为一个字节slice p,返回表示写了多少个字节的整数n和错误err 。换句话说,我们的类型,只要实现了该 Write 方法,就认为遵守了 io.Writer 接口约定,从而,可以作为 fmt.Fprintf 的第一个实参。这实现了一种类型替换为另一种类型,即 可取代性(substitutability)。golang没有子类是父类的兼容类型这样的可取代性,但至少同样包含实现相同接口的两个类型在接口函数上的可取代性。
type ByteCounter int
func (c *ByteCounter) Write(p []byte) (int, error) {
*c += ByteCounter(len(p))
return len(p), nil
}
上面的代码中,定义了类型 ByteCounter (其实就是整数),然后给该类型创建方法 Write,使得Write 方法的签名和 io.Writer 中的Write方法一致(这表示遵守接口约定)。这样,我们就可以让 ByteCounter 类型的变量作为 fmt.Fprintf 的第一个实参了:
var c ByteCounter
c.Write([]byte("hello")) // 直接调用 Write 方法
fmt.Println(c)
c = 0
var name = "Dolly"
fmt.Fprintf(&c, "hello, %s", name) // 间接调用 Write 方法
fmt.Println(c)
上面的代码中,fmt.Fprintf(&c, "hello, %s", name) 包含了间接调用 c 的 Write 方法,和直接调用 Write 方法 c.Write([]byte("hello")) 相比,差别是前者包含了格式化的功能(虽然这里没啥用)。两者都实现了 c 的值被修改(因为 Write 内部修改 c 的值)。
几乎对任何类型,我们都可以用 Fprintf 和 Fprintln 输出它的字符串形式的表示,原因是存在 fmt.Stringer 接口,并且通常类型都会实现这个接口(其实就是实现String()方法):
type Stringer interface {
String() string
}
接口类型
前面的接口类型中,都只有一个方法,但接口中可以有多个方法。如果接口有多个方法,那么实现接口时,必须实现所有的方法。
接口可以像嵌套结构体那样嵌套接口,这样可以避免重复书写接口方法。可以混合书写,如下面的3种写法等价
type ReadWriter interface {
Reader
Writer
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Write(p []byte) (n int, err error)
}
type ReadWriter interface {
Read(p []byte) (n int, err error)
Writer
}
实现接口
一个类型实现了接口的所有方法才算实现了这个接口,并且此时,称该类型“是一个”XXX接口类型,如 *bytes.Buffer is an io.Writer
对于接口类型的变量,所有实现该接口的类型值(可以是一个包含前者的接口类型)都可以赋值给它,但没有实现该接口的类型不行。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = time.Second // 编译错误
var rwc io.ReadWriteCloser
rwc = os.Stdout
rwc = new(bytes.Buffer)
w = rwc
rwc = w // 编译错误
上面的代码,可以简单理解为“子类是父类的兼容类型,但父类不是子类的兼容类型”,无非golang里面的子类概念是实现接口的类型或者包含前一个接口的接口类型。
接口的方法中,接收者 T 和 *T 不能等价,尽管调用方法上编译器会帮我们隐式实现转换。例如
type IntSet struct { /* ... */ }
func (*IntSet) String() string
var s IntSet
var _ = s.String() // OK: s 被编译器隐式转换为 &s
var _ fmt.Stringer = &s // OK: &s 是指针接收者,是接口的兼容类型
var _ fmt.Stringer = s // 编译错误 是 &s 拥有 String()方法,而不是 s
将一个类型赋值给兼容的接口类型变量,接口类型变量只能调用它自身接口范围内的方法,并不能调用来源类型的方法,如下
os.Stdout.Write([]byte("hello"))
os.Stdout.Close()
var w io.Writer
w = os.Stdout // 尽管 w 指向 os.Stdout,但可调用方法集合是 io.Writer 范围
w.Write([]byte("hello")) // OK: io.Writer 有 Write 方法
w.Close() // 编译错误: io.Writer 没有 Close 方法
子类是父类的兼容类型,对于空接口类型 interface{} ,它是任何类型的兼容类型(因为不需要实现任何方法),从而任何类型的变量或值都可以赋值给空接口类型的变量,有点 void * 的意思。这也是王垠大神吐槽的一个点,因为它确实会破坏类型检查。
接口值
一个接口类型的值包括:一个具体类型和该类型的一个值,分别称为接口的动态类型和动态值。
golang是静态类型的语言,所以,类型只是编译时的概念,类型本身不是一个值,和C#(Java不熟悉,应该一样)那样的语言不一样,C#类型也是内存中的一个东西,类型的静态方法就是这个母体的方法,类型的静态变量则有单例模式的效果,实例都有指向这个母体的指针。
下面的代码演示了接口赋值过程中类型的动态性
var w io.Writer
fmt.Printf("%T\\t%v\\n", w, w) // <nil> <nil>
w = os.Stdout
fmt.Printf("%T\\t%v\\n", w, w) // *os.File &{0xc00007a280}
w = new(bytes.Buffer)
fmt.Printf("%T\\t%v\\n", w, w) // *bytes.Buffer
w = nil
fmt.Printf("%T\\t%v\\n", w, w) // <nil> <nil>
通过接口类型的变量调用其对应具体类型的方法具有间接性,编译时无法知道一个接口值的动态类型会是什么,所以需要“动态分发”(有那么点多态的意思)。例如,w = os.Stdout; w.Write([]byte("hello")) 中,编译器必须生成一段代码从类型描述符得到 Write 方法的地址,再用该地址间接调用该方法,调用的接收者就是接口值的动态值 os.Stdout。当 w = new(bytes.Buffer),动态类型是*bytes.Buffer,动态值则是一个指向新分配缓冲区的指针,再调用 w.Write([]byte("hello")),方法的接收者是缓冲区的地址。接口值为nil,则不能调用任何方法。
接口值可以用 == 和 != 进行比较,也可以和nil比较。两个接口值相等的含义是同为nil或者不仅动态类型完全一致,而且动态值也相等(差不多就是php === 的意思)。接口值可以比较,所以,接口值可以充当 map 的键,也可以作为 switch 语句的操作数。
两个接口值,如果动态类型完全一致,但动态值不可比较(如slice),则这两个接口值不可比较(会 panic)。接口类型“什么都往里塞”的特点,使得接口值的比较必须小心,即只对所含动态值可比较的场合进行。
接口值为空(nil)和接口的动态值为空(nil)不是一回事!可以认为前者是“真没东西”,而后者是“有东西,动态指向了某个类型,但东西是空的,该类型对应动态值为空”。下面的代码演示了接口的动态值为空和接口为空不同
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
if debug {
// ...
}
fmt.Println(buf)
}
func f(out io.Writer) {
if out != nil {
fmt.Printf("%V\\n", out) // &{[] %!V(int=0) %!V(bytes.readOp=0)}
out.Write([]byte("done!\\n")) // debug = false, panic!
}
}
当 debug 为 false 时,out 就动态指向了 *bytes.Buffer 类型,但该类型的动态值为空(空指针),对空指针取引用值引发 panic。对于某些类型,比如 *os.File,空接收值是合法的,但对于 *bytes.Buffer 不行,这里 panic,不是方法无法调用,是方法被调用了,但在调用中尝试访问缓冲区时崩溃了。
问题是出在给 out 参数传入了一个具体类型T的参数(类型 T,值 nil),此时,out 就可能出现 <T, nil> 的情况,解决办法是将 buf 声明为 io.Writer 这样的接口类型(默认动态类型nil,动态值nil),从而传入out时不存在转换,即从 <T, nil> 改成了 <nil, nil>
用 sort.Interface 排序
golang sort包的排序是针对接口sort.Interface的,从而它没有和具体的序列类型或元素类型绑定。
package sort
type Inteface interface {
Len() int
Less(i, j int) bool // i, j 是序列元素的下标
Swap(i, j int)
}
凡是实现了该接口(3个方法)的类型,都可以直接调用 sort.Sort() 就地排序。
这个是应用层面的,我们暂时略过。
http.Handler 接口
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
应用,略过
error接口
所有 error 类型,其实都是实现了 error 接口的类型,只要实现返回错误消息的 Error() 方法
type error interface {
Error() string
}
// -----------------------------------------------------
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 包中实现 error 接口时,用errorString这个结构体包裹字符串,是为了给后面可能的更改留下余地,而接收者是 *errorString 而不是 errorString,确保了每次 errors.New("EOF") 得到的值是不一样的(因为字符串分配地址不一样): errors.New("EOF") == errors.New("EOF") // false
通常,errors.New 不会直接被使用,而是更容易格式化的 fmt.Errorf 被使用
类型断言
判断接口类型变量是不是某个具体类型 T, x is T ?
var w io.Writer
w = os.Stdout
f := w.(*os.File)
c := w.(*bytes.Buffer) // panic,接口持有 *os.File 而非 *bytes.Buffer
上面的代码中,接口类型的变量 w 断言的类型是一个具体类型 *os.File,所以,就回检查 w 的动态类型是否是 *os.File,检查成功(动态类型的确是 *os.File),类型断言的结果为 w 的动态值。后面一句断言则引发 panic。
判断接口类型变量是否满足另一个接口 I, x s.t. I ?
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter) // 成功: *os.File 有 Read 和 Write 方法
w = new(ByteCount)
rw = w.(io.ReadWriter) // panic: *ByteCount 没有 Read 方法
上面的代码中,一开始 w 赋值为 os.Stdout,它是满足 io.ReadWriter 接口的(此时动态值不会提取出来,结果仍然是一个接口值,接口值的类型和值也没有变更),但后来赋值为 *ByteCount,它因为没有 Read 方法,不满足 io.ReadWriter 接口,会引发 panic
类型断言失败时崩溃并不能让它实用,实际实用时,类型断言通常出现在需要两个结果的赋值表达式中,此时,断言失败不会崩溃,只是用来指示是否成功的第二个布尔值为 false
var w io.Writer = os.Stdout
f, ok := w.(*os.File) // os.Stdout, true
b, ok := w.(*bytes.Buffer) // nil, false
有时候,断言完会覆盖掉原来的变量
if w, ok := w.(*os.File); ok {
// ...
}
类型分支
接口有两种风格:第一种风格,强调各种类型的共性,即都满足这个接口定义的方法(隐藏了各个具体类型的布局和各自特有功能)。第二种风格,把接口当作一堆类型的联合(union)使用,强调满足这个接口的那些具体类型,而不是接口约定的方法(往往没有接口方法),也不注重信息隐藏,称为可识别联合(discriminated union)。前者相当于 OOP 的 子类型多态 (subtype polymorphism),后者相当于 Ad hoc polymorphism (这玩意不好翻译,“即时多态”可能更好理解)
func sqlQuote(x interface{}) string {
if x == nil {
return "NULL"
} else if _, ok := x.(int); ok {
return fmt.Sprintf("%d", x)
} else if _, ok := x.(uint); ok {
return fmt.Sprintf("%d", x)
} else if b, ok := x.(bool); ok {
if b {
return "TRUE"
}
return "FALSE"
} else if s, ok := x.(string); ok {
return sqlQuoteString(s) // 该函数这里未给出
} else {
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
上面的函数用于对数据库查询SQL语句中的参数值添加引号。变量 x 的类型是 interface {} ,它的作用就是 Ad hoc 多态,把各种类型联合在一起。用 golang 的switch ... case 语句,可以改写如下
func sqlQuote(x interface{}) string {
switch x := x.(type) {
case nil:
return "NULL"
case int, uint:
return fmt.Sprintf("%d", x)
case bool:
if x {
return "TRUE"
}
return "FALSE"
case string:
return sqlQuoteString(s) // 该函数这里未给出
default:
panic(fmt.Sprintf("unexpected type %T: %v", x, x))
}
}
作者的一些建议
golang 的接口用法有和其他语言相同的地方,也有不同的地方。相同的使用经验有:
接口可以实现解耦,即接口定义文件和具体实现接口的类型在不同的包中。
接口定义时,应该尽量“小”,即仅仅定义你需要的——让实现接口的类型更容易实现,约定更容易达成。
不同的是,golang的接口通常都是被动创建的,即开发过程中,发现有两个或者多个类型满足某些特点(例如类似的方法),则可以创建接口,抽象出公共部分,去掉实现细节。如果一开始就创建一堆接口,容易出现每个接口只有一个实现类型的情况,太多这种情况就是滥用接口了。
以上是关于golang学习随便记8的主要内容,如果未能解决你的问题,请参考以下文章