接口类型是对其他类型行为的概括与抽象。通过使用接口,可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定的类型实现上
Go语言的接口是隐式实现,对于一个具体的类型,无须声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计可以无需改变已有类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用
- 接口即约定
具体类型指定了它所含数据的精确布局,同时暴露了基于这个精确布局的内部操作。比如对于数值有算术操作,对于slice类型有索引、append、range等操作。具体类型还会通过其方法来提供额外的能力-->只要知道了具体类型的数据,就精确地知道了它是什么以及它能干什么
接口类型是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,也没有数据的哪些基本操作,提供的仅仅是一些方法。拿到一个接口类型的值,仅仅能知道它能做什么/它提供了哪些方法
fmt.Printf-->是把结果发到标准输出(标准输出就是一个文件)
fmt.Sprintf-->是把结果以string类型返回
package fmt //Fprintf的前缀F指文件,表示格式化的输出会写入第一个实参所指代的文件 func Fprintf(w io.Writer, format string, ards ...interface{}) (int, error) //对于Printf,第一个实参就是os.Stdout,它属于*os.File类型 func Printf(format string, args ...interface{}) (int, error) { return Fprintf(os.Stdout, format, args...) } //对于Sprintf,第一个实参模拟了一个文件,&buf就是一个指向内存缓冲区的指针,与文件类似,这个缓冲区也可以写入多个字节 func Sprintf(format string, args ...interface{}) string { var buf bytes.Buffer Fprintf(&buf, format, args...) return buf.String() } //Fprintf的第一个形参也不是文件类型,而是io.Writer接口类型 package io //Writer接口封装了基础的写入方法 type Writer interface { //Write从p向底层数据流写入len(p)个字节的数据 //返回实际写入的字节数(0 <= n <= len(p)) //如果没写完,那么会返回遇到的错误 //在Write返回n<len(p)时,err必须为非nil //Write不允许修改p的数据,即使是临时修改 //实现时不允许残留p的引用 Write(p []byte) (n int, err error) }
io.Writer接口定义了Fprintf和调用者之间的约定。一方面,这个约定要求调用者提供的具体类型(比如*os.File或者*bytes.Buffer)包含一个与其签名和行为一致的Write方法。另一方面,这个约定保证了Fprintf能使用任何满足io.Writer接口的参数。Fprintf只需要能调用参数的Write函数,无须假设它写入的是一个文件还是一段内存
//*ByteCounter类型的方法仅仅统计传入数据的字节数 type ByteCounter int func (c *ByteCounter) Write(p []byte) (int, error) { *c += ByteCounter(len(p)) //转换int为ByteCounter类型 return len(p), nil } //因为*ByteCounter满足io.Writer接口的约定,所以可以在Fprintf中使用它,Fprintf察觉不到这种类型差异,ByteCounter也能正确地累积格式化后结果的长度 func main() { //!+main var c ByteCounter c.Write([]byte("hello")) fmt.Println(c) // "5" c = 0 // reset the counter var name = "Dolly" fmt.Fprintf(&c, "hello, %s", name) fmt.Println(c) // "12" //!-main }
除了io.Writer之外,fmt包还有另一个重要的接口。Fprintf和Fprintln提供了一个让类型控制如何输出自己的机制。定义一个String方法就可以让类型满足广泛使用的接口fmt.Stringer
package fmt //在字符串格式化时如果需要一个字符串,那么就调用这个方法来把当前值转化为字符串 //Print这种不带格式化参数的输出方式也是调用这个方法 type Stringer interface { String() string }
- 接口类型
一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型定义中的所有方法
package io type Reader interface { Read(p []byte) (n int, err error) } type Closer interface { Close() error } //另外可以通过组合已有接口得到新接口 type ReadWriter interface { Reader Writer } type ReadWriteCloser interface { Reader Writer Closer } //嵌入式接口,与嵌入式结构类似,让我们可以直接使用一个接口,而不用逐一写出这个接口所包含的方法
- 实现接口
如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。比如,*os.File类型实现了io.Reader、Writer、Closer和ReaderWriter接口
*bytes.Buffer实现了Reader、Writer和ReaderWriter,但没有实现Closer,因为它没有close方法
//接口的赋值规则:仅当一个表达式实现了一个接口时,这个表达式才可以赋给该接口 var w io.Writer w = os.Stdout //OK:*os.File有Write方法 w = new(bytes.Buffer) //OK:*bytes.Buffer有Write方法 w = time.Second //编译错误:time.Duration缺少Write方法
一个类型有某一个方法的具体含义:
对每一个具体类型T,部分方法的接收者就是T,而其他方法的接收者则是*T指针
空接口类型:接口类型interface{}。因为空接口类型对其实现类型没有任何要求,所以可以把任何值赋给空接口类型
var any interface{} any = true any = 12.34 any = "hello" any = map[string]int{"one": 1} any = new(bytes.Buffer)
非空的接口类型(比如io.Writer)通常由一个指针类型来实现,特别是当接口类型的一个或多个方法暗示会修改接收者的情形(比如Write方法)。一个指向结构的指针才是最常见的方法接收者
从具体类型出发、提取其共性而得出的每一种分组方式都可以表示为一种接口类型。与基于类的语言(它们显式地声明了一个类型实现的所有接口)不同的是,在Go语言里我们可以在需要时才定义新的抽象和分组,并且不用修改原有类型的定义
- 使用flag.Value来解析参数
如何使用标准接口flag.Value来帮助我们定义命令行标志
var period = flag.Duration("period", 1*time.Second, "sleep period") func main() { flag.Parse() fmt.Printf("Sleeping for %v...", *period) time.Sleep(*period) fmt.Println() }
//flag.Value类型的接口 package flag //Value接口代表了存储在标志内的值 type Value interface { //String方法用于格式化标志对应的值,可用于输出命令行帮助信息。由于有了该方法,因此每个flag.Value其实也是fmt.Stringer。Set方法解析了传入的字符串参数并更新标志值 String() string Set(string) error }
- 接口值
一个接口类型的值有两个部分:一个具体类型和该类型的一个值,二者称为接口的动态类型和动态值
接口值可以用==和!=操作符来比较。如果两个接口值都是nil或者二者的动态类型完全一致且二者动态值相等(使用动态类型的==操作符来做比较),那么两个接口值相等。因为接口值是可以比较的,所以它们可以作为map的键,也可以作为switch语句的操作数
需要注意,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如slice),那么这个比较会以崩溃的方式失败
一般来讲,在编译时我们无法知道一个接口值的动态类型会是什么,所以通过接口来做调用时必然需要使用动态分发。编译器必须生成一段代码来从类型描述符拿到名为write的方法地址,再间接调用该方法地址。调用的接收者就是接口的动态值,即os.Stdout
含有空指针的非空接口:空的接口值(其中不包含任何信息)与仅仅动态值为nil的接口值是不一样的
const debug = true func main() { var buf *bytes.Buffer if debug { buf = new(bytes.Buffer) } f(buf) if debug { //...使用buf... } } //如果out不是nil func f(out io.Writer) { //其他代码 if out != nil { out.Write([]byte("done! ")) } } //当设置debug为true时,主函数收集函数f的输出到一个缓冲区中 //当设置debug为false时,会导致程序在调用out.Write时崩溃 if out != nil { out.Write([]byte("done! ")) //宕机:对空指针取引用值 } //当main函数调用f时,它把一个类型为*bytes.Buffer的空指针赋给了out参数,所以out的动态值确实为空。但它的动态类型是*bytes.Buffer,这表示out是一个包含空指针的非空接口,所以防御性检查out!=nil仍然是true //动态分发机制决定了我们肯定会调用(*bytes.Buffer).Write,只不过这次接收者值为空
- 使用sort.Interface来排序
sort包提供了针对任意序列根据任意排序函数原地排序的功能。在很多语言中,排序算法跟序列数据类型绑定,排序算法跟序列元素类型绑定。Go语言的sort.Sort函数对序列和其中元素的布局无任何要求,它使用sort.Interface接口来指定通用排序算法和每个具体的序列类型之间的协议。这个接口的实现确定了序列的具体布局,以及元素期望的排序方式
//一个原地排序算法需要知道三个信息:序列长度、比较两个元素的含义以及如何交换两个元素,所以sort.Interface接口就有三个方法 package sort type Interface interface { Len() int Less(i, j int) bool //i, j是序列元素的下标 Swap(i, j int) } //要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把sort.Sort函数应用到上面这类方法的实例上 //字符串slice type StringSlice []string func (p StringSlice) Len() int {return len(p)} func (p StringSlice) Less(i, j int) bool {return p[i] < p[j]} func (p StringSlice) Swap(i, j int) {p[i], p[j] = p[j], p[I]} //现在就可以对一个字符串slice进行排序,只须简单地把一个slice转换为StringSlice类型即可 sort.Sort(StringSlice(names))
//类型转换生成了一个新的slice,与原始的names有同样的长度、容量和底层数组,不同的是额外增加了三个用于排序的方法
//sort包提供了StringSlice类型,以及一个直接排序的Strings函数,上面代码可以简写为sort.Strings(names)
//这种技术可以方便地复用到其他排序方式,比如忽略大小写或者特殊字符。对于更复杂的排序,也可以使用同样的思路,只用加上更复杂的数据结构和更复杂的sort.Interface方法实现
type Track struct { Title string Artist string Album string Year int Length time.Duration }
var tracks = []*Track{
{"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")},
}
func length(s string) time.Duration {
d, err := time.ParseDuration(s)
if err != nil {
panic(s)
}
return d
}
sort包提供了一个Reverse函数,可以把任意的排序反向。sort.Reverse函数使用了一个重要概念:组合(通过结构体内嵌组成类型)。sort包定义了一个未导出的类型reverse,这个类型是一个嵌入了sort.Interface的结构。reverse的Less方法直接调用了内嵌的sort.Interface值的Less方法,只交换传入的下标,就可以颠倒排序的结果
package sort type reverse struct { Interface } //that is, sort.Interface func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } func Reverse(data Interface) Interface { return reverse{data} }
//reverse的另外两个方法Len和Swap,由内嵌的sort.Interface隐式提供。导出的函数Reverse则返回一个包含原始sort.Interface值的reverse实例
type customSort struct { t []*Track less 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]} //多层比较函数,先按照标题Title排序,接着是年份Year,最后是时长Length //匿名函数 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 }})
对一个长度为n的序列进行排序需要O(nlogn)次比较操作,而判断一个序列是否已经排好序则只需最多(n-1)次比较。sort包提供的IsSorted函数就可以做这个判断。与sort.Sort类似,它使用sort.Interface来抽象序列及其排序函数,只是从不调用Swap方法而已
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"
- http.Handler接口
net/http
package http type Handler interface { ServeHTTP(w ResponseWriter, t *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 float32 func (d dollars) String() string { return fmt.Sprintf("$%.2f", 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 ", item, price) } }
//把现有功能的URL设为/list,再加上另外一个/price用来显示单个商品的价格,商品可以在 //请求参数中指定,比如/price?item=socks 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 ", 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 ", item) return } fmt.Fprintf(w, "%s ", price) default: w.WriteHeader(http.StatusNotFound) //404 fmt.Fprintf(w, "no such page: %s ", req.URL) } } }
//下面的程序创建了一个ServeMux,用于将/list、/price这样的URL和对应的处理程序 //关联起来,这些处理程序也已经拆分到不同的方法中。最后作为主处理程序 //在ListenAndServe调用中使用这个ServerMux func main() { 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)) } type database map[string]dollars func (db database) list(w http.ResponseWriter, req *http.Request) { for item, price := range db { fmt.Fprintf(w, "%s: %s ", item, price) } } func (db database) price(w http.ResponseWriter, req *http.Request) { item := req.URL.Query().Get("item") price, of := db[item] if !ok { w.WriteHeader(http.StatusNotFound) //404 fmt.Fprintf(w, "no such item: %q ", item) return } fmt.Fprintf(w, "%s ", price) }
- error接口
error是预定义类型,实际上它只是一个接口类型,包含一个返回错误消息的方法
type error interface { Error() string }
//构造error最简单的方法是调用errors.New,它会返回一个包含指定的错误消息的新的error实例
//完整的error包只有如下4行代码
package errors
func New(text string) error { return &errorString{text} }
type errorString struct { text string }
func (e *errorString) Error() string { return e.text }
//底层的errorString类型是一个结构,而没有直接用字符串,主要是为了避免将来的布局变更。满足error接口的是*errorString指针,而不是原始的errorString,主要是为了让每次New分配的error实例都互不相等????
直接调用errors.New比较罕见,因为有一个更易用的封装函数fmt.Errorf,它还额外提供了字符串格式化功能
package fmt
import "errors"
func Errorf(format string, args ...interface{}) error {
return errors.New(Sprintf(format, args...))
}
- 示例:表达式求值器
- 类型断言
类型断言是一个作用在接口值上的操作,写出来类似于x.(T),其中x是一个接口类型的表达式,而T是一个类型(称为断言类型)。类型断言会检查作为操作数的动态类型是否满足指定的断言类型
//如果断言类型T是一个具体类型,那么类型断言会检查x的动态类型是否就是T。如果检查成功,类型断言的结果就是x的动态值,类型当然就是T。类型断言就是用来从它的操作数中把具体类型的值提取出来的操作。如果检查失败,那么操作崩溃 var w io.Writer w = os.Stdout f := w.(*os.File) //成功:f == os.Stdout c := w.(*bytes.Buffer) //崩溃:接口持有的是*os.File而不是*bytes.Buffer //如果断言类型是一个接口类型,那么类型会检查x的动态类型是否满足T。如果检查成功,动态值并没有提取出来,结果仍然是一个接口值,接口值的类型和值部分也没有变更,只是结果的类型为接口类型T。类型断言是一个接口值表达式,从一个接口类型变为拥有另外一套方法的接口类型(通常方法数量是增多),但保留了接口值中的动态类型和动态值部分
如下类型断言代码中,w和rw都持有os.Stdout,于是所有对应的动态类型都是*os.File,但w作为io.Writer仅暴露了文件的Write方法,而rw还暴露了它的Read方法
var w io.Writer
w = os.Stdout
rw := w.(io.ReadWriter)//成功:*os.File有Read和Write方法
w = new(ByteCounter)
rw = w.(io.ReadWriter)//崩溃:*ByteCounter没有Read方法
//无论哪种类型作为断言类型,如果操作数是一个空接口值,类型断言都失败。很少需要从一个接口类型向一个要求更宽松的类型做类型断言,该宽松类型的接口方法比原类型的少,而且是其子集。因为除了在操作nil之外的情况下,在其他情况下这种操作与赋值一致?
w = rw //io.ReadWriter可以赋给io.Writer
w = rw.(io.Writer)//仅当rw=nil时失败
//我们经常无法确定一个接口值的动态类型,这时就需要检测它是否时某一个特定类型。如果类型断言出现在需要两个结果的赋值表达式中,那么断言不会再失败时崩溃,而是会多返回一个布尔型的返回值来指示断言是否成功
var w io.Writer = os.Stdout
f, ok = w.(*os.File) //成功:ok, f == os.Stdout
b, ok = w.(*bytes.Buffer) //失败:!ok, b == nil
//按照惯例,一般把第二个返回值赋给一个名为ok的变量。如果操作失败,ok为false,而第一个返回值为断言类型的零值
//ok返回值通常马上就用来决定下一步做什么
if f, ok := w.(*os.File);ok{
//...使用f...
}
//当类型断言的操作数是一个变量时,有时会看到返回值的名字与操作数变量名一致,原有的值就被新的返回值掩盖了
if w, ok := w.(*os.File); ok{
//...use W...
}
- 使用类型断言来识别错误
考虑os包中的文件操作返回的错误集合,I/O会因为很多原因失败,但有三类原因通常必须单独处理:文件已存储(创建操作),文件没找到(读取操作)以及权限不足
os提供了三个帮助函数用来对错误进行分类
package os func IsExist(err error) bool func IsNotExist(err error) bool func IsPermission(err error) bool //用专门的类型来表示结构化的错误值。os包定义了一个PathError类型来表示在与一个文件路径相关的操作上发生错误(比如Open或者Delete) package os //PathError记录了错误以及错误相关的操作和文件路径 type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } //PathError的Error方法指示拼接了所有的字段,而PathError的结构则保留了错误所有的底层信息。对于那些需要区分错误的客户端,可以使用类型断言来检查错误的特定类型,这些类型包含的细节远远多于一个简单的字符串 _, err := os.Open("/no/such/file") fmt.Println(err) // "open/no/such/file: No such file or directory" fmt.Printf("%#v ", err) // &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}
import ( "errors" "syscall" ) var ErrNotExists = errors.New("file does not exist") //IsNotExist返回一个布尔值,该值表明错误是否代表文件或目录不存在 //report that a file or directory does not exist. It is satisfied by ErrNotExist 和其他 //一些系统调用错误会返回true func IsNotExist(err error) bool {
if pe, ok := err.(*PathError); ok {
err = pe.Err
}
return err == syscall.ENOENT || err == ErrNotExist
}
//实际使用情况如下
_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err))//"true"
//如果错误消息已被fmt.Errorf这类的方法合并到一个大字符串中,那么PathError的结构信息就丢失了
- 通过接口类型断言来查询特性
net/http
func writeHeader(w io.Writer, contentType string) error { if _, err := w.Write([]byte("Content-Type: ")); err != nil { return err } if _, err := w.Write([]byte(contentType)); err != nil { return err } //...... } //因为Write方法需要一个字节slice,而我们想写入的是一个字符串,所以[]byte(...)转换就是必需的。这种转换需要进行内存分配和内存复制,但复制后的内存又会被马上抛弃 //如何避免内存复制? //writeString将s写入w //如果w有WriteString方法,那么将直接调用该方法 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) //避免了内存复制 } return w.Write([]byte(s)) //分配了临时内存 } func writeHeader(w io.Writer, contentType string) error { if _, err := writeString(w, "Content-Type: "); err != nil { return err } if _, err := writeString(w, contentType); err!=nil { return err } //...... }
为了避免代码重复,把检查挪到了工具函数writeString中。实际上,标准库提供了io.WriteString,而且这也是向io.Writer写入字符串的推荐方法
- 类型分支
接口有两种不同的风格。第一种风格下,典型的比如io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler和error,接口上的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能。这种风格强调了方法,而不是具体类型
第二种风格则充分利用了接口值能够容纳各种具体类型的能力,它把接口作为这些类型的联合来使用。类型断言用来在运行时区分这些类型并分别处理。在这种风格中,强调的是满足这个接口的具体类型,而不是这个接口的方法,也不注重信息隐藏。这种风格的接口使用方式称为可识别联合
Go语言的数据库SQL查询API也允许我们干净地分离查询中的不变部分和可变部分
import "database/sql" func listTracks(db sql.DB, artist string, minYear, maxYear int) { result, err := db.Exec( "SELECT * FROM tracks WHERE artist = ? AND ? <= year AND year <= ?", artist, minYear, maxYear) //...... } //Exec方法把查询字符串中的每一个"?"都替换为与相应参数值对应的SQL字面量,这些参数可能是布尔型、数字、字符串或者nil。通过这种方式构造请求可以帮助避免SQL注入攻击,攻击者可以通过在输入数据中加入不恰当的引号来控制查询。在Exec的实现代码中,有一个类似如下的函数,将每个参数值转为对应的SQL字面量 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) //(not shown) } else { panic(fmt.Sprintf("unexpected type %T: %v", x, x)) } } //一个switch语句可以把包含一长串值相等比较的if-else语句简化掉。一个相似的类型分支语句则可以用来简化一长串的类型断言if-else语句 类型分支的最简单形式与普通分支语句类似,两个的差别是操作数改为x.(type)(注意:这里直接写关键字type而不是一个特定类型),每个分支是一个或者多个类型。类型分支的分支判定基于接口值的动态类型,其中nil分支需要x==nil,而default分支则在其他分支都没有满足时才运行 switch x.(type){ case nil: //... case int, uint: //... case bool: //... case string: //... default: //... } //与普通的switch语句类似,分支是按顺序来判定的,当一个分支符合时,对应的代码会执行。分支的顺序在一个或多个分支是接口类型时会变得重要,因为有可能两个分支都能满足。default分支的位置是无关紧要的。类型分支不允许使用fallthrough //在原来的代码中,bool和string分支的逻辑需要访问由类型断言提取出来的原始值。所以类型分支也有一种扩展形式,它用来把每个分支中提取出来的原始值绑定到一个新的变量 switch x := x.(type) //这里把新的变量也命名为x,重用变量名 //与switch语句类似,类型分支也隐式创建了一个词法块,所以声明一个新变量叫x并不与外部块中的变量x冲突。每个分支也会隐式创建各自的词法块 func sqlQuote(x interface{}) string { switch x := x.(type) case nil: return "NULL" case int, uint: return fmt.Sprintf("%d", x) //这里x类型为interface{} case bool: if x { return "TRUE" } return "FALSE" case string: return sqlQuoteString(x) default: panic(fmt.Sprintf("unexpected type %T: %v", x, x)) }
- 示例:基于标记的XML解析
- 一些建议