接口

接口是一种协议,比如一个汽车的协议,就应该有 “行驶”,“按喇叭”,“开远光” 等功能(方法),这就是实现汽车的协议规范,完成了汽车的协议规范,就实现了汽车的接口,然后使用接口

接口的定义:本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程

Go 语言中的接口是双方约定的一种合作协议;接口实现者不需要关心接口会被如何使用,调用者也不需要关心接口的实现细节。接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构

Go 语言中接口的设计是非侵入式的,接口编写者无须知道接口被哪些类型实现,而接口实现者只需知道实现的是什么样子的接口,无须指明是哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现

非侵入式设计是 Go 语言设计师经过多年的大项目经验总结出来的设计之道。只有让接口和实现者真正解耦,编译速度才能真正提高,项目之间的耦合度也会降低

接口声明的格式

接口类型名:在命名时,一般会在单词的后面添加 er,例如写操作的接口叫 Writer,关闭功能的接口叫 Closer

方法名:当方法名首字母是大写,且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包之外的代码访问

参数列表、返回值列表:

type 接口类型名 interface{
方法名1(参数列表) 返回值列表
方法名2(参数列表) 返回值列表
...
}

实现接口

接口定义后,需要实现接口,调用方才能正确编译通过并使用接口

接口的实现需要遵循两条规则才能使接口可用

1)接口的方法与实现接口的类型方法格式一致

在类型中添加与接口签名一致的方法就可以实现该方法。签名包括方法中的名称、参数列表、返回参数列表。也就是说,只要实现接口类型中的方法的名称、参数列表、返回参数列表中的任意一项与接口要实现的方法不一致,那么接口的这个方法就不会被实现

模拟数据写入的 Demo:

// 实现一个写入器接口
type Writer interface{
WriteData(data interface{}) error
} // 文件类型结构体
type file struct {} // 实现 Writer 接口的 WriteData 方法
func (f *file) WriteData(data interface{}) error {
// 模拟写入数据
fmt.Println(data)
return nil
} func main(){
// 实例化 file
var f *file = new(file)
// 声明 Writer 接口
var W Writer
// 将文件类型结构体赋值给接口
W = f
// 使用接口调用数据写入
W.WriteData("hello, world~")
} 运行结果:
hello, world~

  

2)接口中所有方法均被实现

当一个接口中有多个方法时,只有这些方法都被实现了,接口才能正确编译并使用

类型与接口的关系

类型与接口之间有一对多和多对一的关系

1)一个类型实现多个接口

把 Socket 能够写入数据和需要关闭的特性使用接口来描述,Demo:

// 实现一个写入接口
type Writer interface{
Write(b []byte)(n int, err error)
} // 实现一个关闭接口
type Closer interface{
Close() (err error)
} // 套接字结构体
type Socket struct {} // 实现 Writer 接口的 Write 方法
func (s *Socket) Write(b []byte) (n int, err error) {
fmt.Println("write data")
return 0, nil
} // 实现 Closer 接口的 Close 方法
func (s *Socket) Close() (err error) {
fmt.Println("closer socket")
return nil
} func main(){
// 实例化 file
var s *Socket = new(Socket)
// 声明 Writer 和 Closer 接口
var W Writer
var C Closer
// 将文件类型结构体赋值给接口
W = s
C = s
// 使用接口调用数据写入
W.Write(make([]byte, 0))
C.Close()
} 运行结果:
write data
closer socket

  

2)多个类型对应一个接口(这里多个类型本质上指的还是一个类型)

Service 接口定义了两个方法:一个开启服务的方法 Start(),一个是输出日志的方法 Log(),使用 GameService 结构体来实现 Service 接口;GameService 自己的结构只能实现 Start(),而 Service 接口中的 Log() 已经被一个输出日志的 Logging 实现了, 无须再进行 GameService 再重新实现一遍,所以,选择将 Logging 嵌入到 GameService 能最大程度的避免冗余代码,详细实现过程如下:

// 实现一个服务接口
type Service interface{
Start(args string)(err error)
Log(args string)(err error)
} // 日志器结构体
type Logging struct{}
// 日志记录方法
func (l *Logging) Log(info string) (err error){
fmt.Println(info)
return nil
} // 游戏服务结构体,内嵌日志器
type GameService struct{
Logging
}
// 游戏服务开启方法
func (gs *GameService) Start(args string) (err error){
fmt.Println("game start", args)
return nil
} func main(){
// 实例化 游戏服务结构体,并将实例赋值给 Service
var s Service = new(GameService)
// 使用接口调用服务启动,日志记录
s.Start("come on")
s.Log("this is a log info")
} 运行结果:
game start come on
this is a log info

  

错误示例:

如果游戏服务结构体单独实现 Start() 方法,日志器单独实现 Log() 方法,这样并没有实现接口的所有的方法

// 实现一个服务接口
type Service interface{
Start(args string)(err error)
Log(args string)(err error)
} // 日志器结构体
type Logging struct{}
// 日志记录方法
func (l *Logging) Log(info string) (err error){
fmt.Println(info)
return nil
} // 游戏服务结构体,内嵌日志器
type GameService struct{}
// 游戏服务开启方法
func (gs *GameService) Start(args string) (err error){
fmt.Println("game start", args)
return nil
} func main(){
// 实例化 游戏服务结构体,并将实例赋值给 Service
var s Service = new(GameService)
// 使用接口调用服务启动,日志记录
s.Start("come on")
s.Log("this is a log info")
} 运行结果:
./main_04.go:31:9: cannot use new(GameService) (type *GameService) as type Service in assignment:
*GameService does not implement Service (missing Log method)

接口排序

使用 sort.Interface 接口实现排序

在排序时,使用 sort.Interface 提供数据的一些特性和操作方法,这个接口定义代码如下:

type Interface interface {
// 获取元素数量
Len() int // 小于比较
Less(i, j int) bool // 交换元素
Swap(i, j int)
}

这个接口需要实现者实现三个方法:Len(),Less(),Swap()

对一系列字符串进行排序时,把字符串放入切片,使用 type 关键字,定义为自定义的类型,为了让 sort 包能够识别自定义类型,就必须让自定义类型实现 sort.Interface 接口

package main
import (
"fmt"
"sort"
)
// 将[]string定义为MyStringList类型
type MyStringList []string func (m MyStringList) Len() int {
return len(m)
}
func (m MyStringList) Less(i, j int) bool {
return m[i] < m[j]
}
func (m MyStringList) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func main() {
// 准备一个内容被打乱顺序的字符串切片
var names MyStringList = MyStringList{
"3 Triple Kill",
"5 Penta Kill",
"2 Double Kill",
"4 Quadra Kill",
"1 First Blood",
}
// 使用sort包进行排序(Sort 接收一个Interface类型,MyStringList会被赋值给 Interface 类型)
sort.Sort(names)
// 遍历打印结果
for _, v := range names {
fmt.Printf("%s\n", v)
}
} 运行结果:
1 First Blood
2 Double Kill
3 Triple Kill
4 Quadra Kill
5 Penta Kill

  

常见类型的便捷排序

通过 sort.Interface 接口的排序过程具有很强的可定制性

1)字符串切片的便捷排序(与在上面用自定义类型实现的逻辑一样)

sort 包中有一个 StringSlice 类型,定义如下:

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] }
// Sort is a convenience method.
func (p StringSlice) Sort() { Sort(p) }

使用 sort 包下的 StringSlice 类型就可以对字符串切片进行排序,简化上面的步骤:

names := sort.StringSlice{
"3 Triple Kill",
"5 Penta Kill",
"2 Double Kill",
"4 Quadra Kill",
"1 First Blood",
}
sort.Sort(names)

字符串排序简化版:sort.Strings(names)

2)sort 包下其它的内建排序接口

类  型 实现 sort.lnterface 的类型 直接排序方法 说  明
字符串(String) StringSlice sort.Strings(a [] string) 字符 ASCII 值升序
整型(int) IntSlice sort.Ints(a []int) 数值升序
双精度浮点(float64) Float64Slice sort.Float64s(a []float64) 数值升序

编程中经常用到的 int32、int64、float32、bool 类型没有由 sort 实现,需要开发者自己编写

对结构体数据进行排序

除了基本类型,也可以对结构体的字段进行排序,结构体的多个字段在排序中可能存在多种排序规则,如先按分类排序,然后按名称排序

demo:定义英雄结构体,有 Name 和 Kind 字段,排序时要求先按照分类排序,相同分类则按名称排序,实现如下(排序的对象是英雄结构体):

// 定义int常量, 类似于枚举
const (
None int = iota // 0
Tank // 1
Assassin // 2
Mage // 3
) // Hero 结构体
type Hero struct{
Name string
Kind int
} // 自定义 Hero 的切片的类型
type Heros []Hero // 实现 sort.Interface 接口方法
func (hs Heros) Len() int {
return len(hs)
}
func (hs Heros) Less(i, j int) bool {
// 优先对分类进行排序
if hs[i].Kind != hs[j].Kind {
return hs[i].Kind < hs[j].Kind
} else {
// secondary: Name
return hs[i].Name < hs[j].Name
}
}
func (hs Heros) Swap (i,j int) {
hs[i], hs[j] = hs[j], hs[i]
} func main(){
var heros Heros = Heros{
Hero{"吕布", Tank},
Hero{"李白", Assassin},
Hero{"妲己", Mage},
Hero{"貂蝉", Assassin},
Hero{"关羽", Tank},
Hero{"诸葛亮", Mage},
}
fmt.Println("before:")
for _, v := range(heros){
fmt.Println(v)
}
sort.Sort(heros)
fmt.Println("\nafter:")
for _, v := range(heros){
fmt.Println(v)
}
} 运行结果:
before:
{吕布 1}
{李白 2}
{妲己 3}
{貂蝉 2}
{关羽 1}
{诸葛亮 3} after:
{关羽 1}
{吕布 1}
{李白 2}
{貂蝉 2}
{妲己 3}
{诸葛亮 3}

Hero

接口嵌套组合

Go 语言中,不仅结构体之间可以嵌套,接口之间也可以通过嵌套组合形成新的接口

1)io 包中的接口嵌套组合

io 包中定义了写入接口(Writer)、关闭接口(Closer)、写入关闭组合接口(WriterCloser),代码如下:

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

  

2)在代码中使用接口嵌套组合

我们实现一下上面 io 包中的三个接口(一个类型对应多个接口)

type Device struct {
Name string
} func (d Device) Write(p []byte) (n int, err error) {
fmt.Printf("%v call Write methodi\n", d)
return 0, nil
} func (d Device) Close() error {
fmt.Printf("%v call Close method\n", d)
return nil
} func main(){
var device1 io.Writer = Device{"device1"}
device1.Write(make([]byte, 0)) var device2 io.Closer = Device{"device2"}
device2.Close() var device3 io.WriteCloser = Device{"device3"}
device3.Write(make([]byte, 0))
device3.Close()
} 运行结果:
{device1} call Write methodi
{device2} call Close method
{device3} call Write methodi
{device3} call Close method

  

接口与类型之间的转换

使用接口断言 type assertions 将接口转换成另外一个接口,也可以将接口准换为另外的类型,接口的转换在开发中非常常见,使用也非常频繁

空接口

interface{},空接口没有任何方法,所有类型都实现了空接口(所以类型都可以赋值给空接口),下面会详细说

类型判断的格式

t := i.(T)

其中,i 是接口变量,T 是要转换的目标类型,t 是准换后的变量

如果 i 没有实现 T 接口的所有方法,即断言失败,会触发 panic,所以有一种友好的写法

断言失败时,将会把 ok 置为 false, t 置为 T 类型的 0 值,断言成功时,ok 置为 true,t 置为断言后的结果

t, ok := i.(T)

将接口转换成其它接口

一种类型实现了多个接口,就可以在多个接口之间进行切换

demo:鸟 和 猪具有不同的特性,鸟可以飞,可以行走,猪只能行走,现在有飞行动物接口 Flyer 和 行走动物接口 Walker,如果用结构体分别实现鸟和猪,鸟实现 Fly() 和 Walk(),猪实现 Walk(),那么鸟类型实现了飞行动物接口和行走动物接口,猪实现了行走动物接口

下面的demo中,猪类型实现了从空接口转换到行走接口,鸟类型实现了从空接口转换到行走接口,然后转换到飞行接口

//飞行接口
type Flyer interface {
Fly()
}
//行走接口
type Walker interface {
Walk()
}
//猪结构体,实现行走接口
type Pig struct{}
func (p Pig) Walk() {
fmt.Println("pig walk")
}
//鸟结构体,实现行走和飞行接口
type Bird struct{}
func (b Bird) Walk() {
fmt.Println("bird walk")
}
func (b Bird) Fly() {
fmt.Println("bird fly")
} func main() {
//空接口接收猪类型
var pig interface{} = new(Pig)
var bird interface{} = new(Bird)
//判断对象类型是否实现行走接口(转换到行走接口)
pigWalk, pigWalker := pig.(Walker)
birdWalk, birdWalker := bird.(Walker)
birdFly, isFlyer := bird.(Flyer)
//如果实现行走接口,则调用行走接口方法
if pigWalker {
pigWalk.Walk()
}
if birdWalker {
birdWalk.Walk()
}
if isFlyer {
birdFly.Fly()
}
} 运行结果:
pig walk
bird walk
bird fly

  

将接口转换为类型

在上面的代码中,可以将普通的指针类型 new(Pig),转换成接口 Walker,那么将 Walker 接口转换成 *Pig 类型也是可以的

var walk Walker = new(Pig)

//接口转换为类型
p, ok := walk.(*Pig)
if ok {fmt.Printf("%T", p)}

  

但是如果把普通指针类型 new(*Pig),转换成接口,然后将接口转换成 *Bird,这样会触发 panic: interface conversion: main.Walker is *main.Pig, not *main.Bird

var walk Walker = new(Pig)
p := walk.(*Bird)
fmt.Printf("%T", p) 运行结果:
panic: interface conversion: main.Walker is *main.Pig, not *main.Bird

报错意思是:接口转换类型时,main.Walker 接口的内部保存的是 *main.pig,而不是 *main.bird

因此,接口在转换为类型时,接口内保存的类型指针,必须是要转换的类型指针

空接口类型

空接口是接口类型的特殊形式,空接口没有任何方法,从实现的角度看,任何类型都实现了空接口,因此空接口类型可以保存任何值,也可以从空接口中取出原值

空接口的内部实现保存了对象的类型与指针,使用空接口保存一个数据的过程会比直接用变量保存稍慢,因此在开发中,应在需要的地方使用空接口,而不是所有地方都使用空接口

1)将值保存到空接口(从类型转换成接口)

func main(){
var any interface{}
any = 666
any = "hello, world~"
fmt.Println(any)
} 运行结果:
hello, world~

2)从空接口中获取值(从接口转换成类型)

func main(){
var any interface{}
any = "hello, world~" var value string = any.(string)
fmt.Println(value)
} 运行结果:
hello, world~

  

使用空接口实现可以保存任意值的字典(实现 python 中的字典)

空接口可以保存任何类型,这个特性可以方便的用于容器的设计,下面的例子中使用 map 和 insterface{} 实现了 python 中的字典,包含有 设置值、取值、清空的方法

package main
import "fmt" //定义key,value 可为任意值的字典结构体
type Dict struct {
data map[interface{}]interface{}
} //设置值
func (d Dict) Set(key, value interface{}) {
d.data[key] = value
} //根据键获取值
func (d Dict) Get(key interface{}) interface{} {
return d.data[key]
} //清空Dict
func (d *Dict) Clear(){
d.data = make(map[interface{}]interface{})
} func main(){
//字典结构包含有 map,需要在创建 Dictionary 实例时初始化 map
var dict Dict = Dict{}
dict.data = make(map[interface{}]interface{})
//var dict Dict = Dict{map[interface{}]interface{}{}} //可以写成这种 dict.Set("name", "johny")
dict.Set("age", 12)
dict.Set(666, 666)
// 根据键获取值(这里拿到的是 interface{},需要根据空接口中的值类型进行断言取值,不好用)
fmt.Println(dict.Get("name").(string))
fmt.Println(dict.Get("age").(int))
fmt.Println(dict.Get(666).(int))
// 清空字典
dict.Clear()
fmt.Println(dict)
} 运行结果:
johny
12
666
{map[]}

问题:在空接口转换成类型的时候,需要进行类型的断言,如果你不知道空接口中的类型,则需要做判断,有点麻烦

接口类型断言

在从接口转换成类型的时候,往往会不清楚要转换的目标类型是什么,所以需要判断空接口中的类型,if 的语句代码太繁杂,这里使用 switch 实现

1)类型断言 switch 格式(接口转换成类型)

package main
import "fmt" func assertions(element interface{}) {
switch element.(type){
case int:
fmt.Println(element.(int))
case string:
fmt.Println(element.(string))
case float64:
fmt.Println(element.(float64))
default:
fmt.Println("unsupported types")
}
} func main(){
assertions("666")
assertions("hello, world")
assertions(true)
} 运行结果:
666
hello, world
unsupported types

2)接口断言 switch 格式(接口转换成接口)

多个接口进行断言时,也可以使用 switch 分支简化判断过程

demo:现在移动支付逐渐成为人们普遍使用的支付方式,移动支付可以使用 faceID,而现金支付容易被偷(Stolen),使用 switch 接口断言可以方便判断是哪种支付接口,进行方法调用

现有两个支付接口 CantainCanUseFaceID 和 ContainStolen,分别实现了 UseFaceID() 和 Stolen() 方法,在支付函数 Payment() 中进行接口断言,然后调用相应的方法

package main
import "fmt"
// 移动支付接口
type CantainCanUseFaceID interface {
UseFaceID()
}
//现金支付接口
type ContainStolen interface {
Stolen()
} //alipay 结构体,实现移动支付接口
type Alipay struct {}
func (a *Alipay) UseFaceID() {
fmt.Println("alipay payment")
} //现金支付结构体,实现现金支付接口
type Cash struct {}
func (c *Cash) Stolen() {
fmt.Println("cash payment")
} func Payment(patternPayment interface{}) {
switch patternPayment.(type) {
// 可以使用移动支付
case CantainCanUseFaceID:
faceIDPayment := patternPayment.(CantainCanUseFaceID)
faceIDPayment.UseFaceID()
// 可以使用现金支付
case ContainStolen:
cashPayment := patternPayment.(ContainStolen)
cashPayment.Stolen()
}
} func main() {
//使用 alipay 支付
Payment(new(Alipay))
//使用现金支付
Payment(new(Cash))
} 运行结果:
alipay payment
cash payment

  

end ~

  

Go part 6 接口,接口排序,接口嵌套组合,接口与类型转换,接口断言的更多相关文章

  1. C#实现Comparable接口实现排序

    C#中,实现排序的方法有两种,即实现Comparable或Comparer接口,下面简单介绍实现Comparable接口实现排序功能. 实现Comparable接口需要实现CompareTo(obje ...

  2. 使用Comparable接口自定义排序

    Employee: package textq; /** * 调用接口Comparable排序 * @author Administrator * */ public class Employee i ...

  3. Java基础 TreeSet()来实现数组的【定制排序】 : Comparable接口(自然排序) 或者 Comparator接口 (定制排序)

    笔记: //排序真麻烦!没有C++里的好用又方便!ORZ!ORZ!数组排序还还自己写个TreeSet()和( Comparable接口(自然排序) 或者 Comparator接口 (定制排序))imp ...

  4. java:面向对象(接口(续),Compareble重写,Comparator接口:比较器的重写,内部类,垃圾回收机制)

    接口: *接口定义:使用interface关键字 * [修饰符] interface 接口名 [extends 父接口1,父接口2...]{ * //常量的声明 * //方法的声明 * } *接口成员 ...

  5. [Think In Java]基础拾遗1 - 对象初始化、垃圾回收器、继承、组合、代理、接口、抽象类

    目录 第一章 对象导论第二章 一切都是对象第三章 操作符第四章 控制执行流程第五章 初始化与清理第六章 访问权限控制第七章 复用类第九章 接口 第一章 对象导论 1. 对象的数据位于何处? 有两种方式 ...

  6. Java基础-接口.编写2个接口:InterfaceA和InterfaceB;在接口InterfaceA中有个方法void printCapitalLetter();在接口InterfaceB中有个方法void printLowercaseLetter();然 后写一个类Print实现接口InterfaceA和InterfaceB,要求 方法 实现输出大写英文字母表的功能,printLowerca

    #34.编写2个接口:InterfaceA和InterfaceB:在接口InterfaceA中有个方法void printCapitalLetter():在接口InterfaceB中有个方法void ...

  7. JAVA中的数据结构——集合类(线性表:Vector、Stack、LinkedList、set接口;键值对:Hashtable、Map接口<HashMap类、TreeMap类>)

    Java的集合可以分为两种,第一种是以数组为代表的线性表,基类是Collection:第二种是以Hashtable为代表的键值对. ... 线性表,基类是Collection: 数组类: person ...

  8. 34.编写2个接口:InterfaceA和InterfaceB;在接口InterfaceA中有个方法void printCapitalLetter();在接口InterfaceB中有个方法void printLowercaseLetter();然 后写一个类Print实现接口InterfaceA和InterfaceB,要求printCapitalLetter()方法 实现输出大写英文字母表的功能,

    //接口InterfaceA package jieKou; public interface IInterfaceA { void printCapitalLetter(); } //接口Inter ...

  9. c#类,接口,结构,抽象类介绍 以及抽象和接口的比较

    c#中的类是最常见的实际上就是对某种类型的对象定义变量和方法的原型. 结构是值类型,而类是引用类型. 1.与类不同,结构的实例化可以不使用 new 运算符.结构可以声明构造函数,但它们必须带参数. 2 ...

随机推荐

  1. Linux_CentOS软件安装调试 源代码包编译安装和 二进制包配置

    Linux 下源代码(C 语言)如何编译(安装) 1. 先安装源代码编译的软件 gcc,make,openssl 如下: yum install -y gcc make gcc-c++ openssl ...

  2. Dart 自增++自减--和循环语句

    void main(){ /* ++ -- 表示自增 自减 1 在赋值运算里面 如果++ -- 写在前面 这时候先运算 再赋值,如果++ --写在后面 先赋值后运行运算 var a=10; var b ...

  3. 中标麒麟+Qt+mysql解决驱动无法加载的问题

    问题描述:都安装了Qt,Mysql之后,发现Qt始终不能连接Mysql 1.安装Qt 2.写程序直接连接QMysql 打印QSqlDatabase: * driver not loaded ,进入/h ...

  4. Qt编写自定义控件64-垂直时间轴

    一.前言 垂直时间轴控件,主要用来描述企业发展历程大事件,或者软件版本迭代历史等,通过时间节点和事件描述来直观的展示发展的过程,一般在web网页或者app中经常看到此类控件,尤其是公司的官网关于公司部 ...

  5. 青葱的岁月 Mybatis JdbcType与Oracle、MySql数据类型对应列表

    Mybatis JdbcType Oracle MySql JdbcType ARRAY     JdbcType BIGINT   BIGINT JdbcType BINARY     JdbcTy ...

  6. anywhere随启随用的静态文件服务器

    手机移动端调试,也可以使用anywhere anywhere -p 8080  指定端口 anywhere -s 保持浏览器关闭 anywhere -h localhost -p 8080 通过主机名 ...

  7. FreeRTOS 时间片,外部中断,任务优先级的一个疑问

    时间片1ms 假设有两个任务,A和B,A任务等待中断里面发出的信号量,B任务在运行 此时,B任务运行了300us的时候中断发生,发出信号量,那么任务A接收到信号量,A任务优先级高,A任务运行 有个问题 ...

  8. 【GStreamer开发】GStreamer播放教程03——pipeline的快捷访问

    目的 <GStreamer08--pipeline的快捷访问>展示了一个应用如何用appsrc和appsink这两个特殊的element在pipeline中手动输入/提取数据.playbi ...

  9. mysql 库、表、数据的增删改

    数据库定义 语法形式 (1)创建数据库 create database [if not exists ] 数据库名 [charset 字符集] [collate 字符排序规则]; if not exi ...

  10. VMware中安装Ubntu

    "懦夫没有能力去表现爱:爱是勇者的特权"----甘地 原文请见: https://blog.csdn.net/wumumang/article/details/54099997 一 ...