教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用

Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用

发布时间:2022-02-20   编辑:jiaochengji.com
教程集为您提供Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用等资源,欢迎您收藏本站,我们将为您提供最新的Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用资源
<h1>Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用</h1>

<span class="img-wrap"></span>

大家都知道,Go不是面向对象(Object Oriented,后面简称为OO)语言。本文以Java语言为例,介绍传统OO编程拥有的特性,以及在Go语言中如何模拟这些特性。文中出现的示例代码都取自Cosmos-SDK或Tendermint源代码。以下是本文将要介绍的OO编程的主要概念:

<ul><li>

类(Class)

<ul><li>

字段(Field)

<ul><li>实例字段</li> <li>类字段</li> </ul></li> <li>

方法(Method)

<ul><li>实例方法</li> <li>类方法</li> <li>构造函数(Constructor)</li> </ul></li> <li>信息隐藏</li> <li>

继承

<ul><li>利斯科夫替换原则(Liskov Substitution Principle,LSP)</li> <li>方法重写(Overriding)</li> <li>方法重载(Overloading)</li> <li>多态</li> </ul></li> </ul></li> <li>

接口(Interface)

<ul><li>扩展</li> <li>实现</li> </ul></li> </ul><h2>类</h2>

传统OO语言很重要的一个概念就是,类相当于一个模版,可以用来创建实例(或者对象)。在Java里,使用<code>class</code>关键子来自定义一个类:

<pre><code class="lang-java hljs">class StdTx { // 字段省略 }</code></code></pre>

Go并不是传统意义上的OO语言,甚至根本没有"类"的概念,所以也没有<code>class</code>关键字,直接用struct定义结构体即可:

<pre><code class="lang-go hljs">type StdTx struct { // 字段省略 }</code></code></pre> <h3>字段</h3>

类的状态可以分为两种:每个实例各自的状态(简称实例状态),以及类本身的状态(简称类状态)。类或实例的状态由字段构成,实例状态由实例字段构成,类状态则由类字段构成。

<h4>实例字段</h4>

在Java的类里定义实例字段,或者在Go的结构体里定义字段,写法差不多,当然语法略有不同。仍以Cosmos-SDK提供的标准交易为例,先给出Java的写法:

<pre><code class="lang-java hljs">class StdTx { Msg[] msgs; StdFee fee; StdSignature[] StdSignatures String memo; }</code></code></pre>

再给出Go的写法:

<pre><code class="lang-go hljs">type StdTx struct { Msgs []sdk.Msg `json:"msg"` Fee StdFee `json:"fee"` Signatures []StdSignature `json:"signatures"` Memo string `json:"memo"` }</code></code></pre> <h4>类字段</h4>

在Java里,可以用<code>static</code>关键字定义类字段(因此也叫做静态字段):

<pre><code class="lang-java hljs">class StdTx { static long maxGasWanted = (1 << 63) - 1; Msg[] msgs; StdFee fee; StdSignature[] StdSignatures String memo; }</code></code></pre>

Go语言没有对应的概念,只能用全局变量来模拟:

<pre><code class="lang-go hljs">var maxGasWanted = uint64((1 << 63) - 1)</code></code></pre> <h3>方法</h3>

为了写出更容易维护的代码,外界通常需要通过方法来读写实例或类状态,读写实例状态的方法叫做实例方法,读写类状态的方法则叫做类方法。大部分OO语言还有一种特殊的方法,叫做构造函数,专门用于创建类的实例。

<h4>实例方法</h4>

在Java中,有明确的返回值,且没有用<code>static</code>关键字修饰的方法即是实例方法。在实例方法中,可以隐式或显式(通过<code>this</code>关键字)访问当前实例。下面以Java中最简单的Getter/Setter方法为例演示实例方法的定义:

<pre><code class="lang-java hljs">class StdTx { private String memo; // 其他字段省略 public voie setMemo(String memo) {this.memo = memo; } // 使用this关键字 public String getMemo() { return memo; } // 不用this关键字 }</code></code></pre>

实例方法当然只能在类的实例(也即对象)上调用:

<pre><code class="lang-java hljs">StdTx stdTx = new StdTx(); // 创建类实例 stdTx.setMemo("hello"); // 调用实例方法 String memo = stdTx.getMemo(); // 调用实例方法</code></code></pre>

Go语言则通过显式指定receiver来给结构体定义方法(Go只有这么一种方法,所以也就不用区分是什么方法了):

<pre><code class="lang-go hljs">// 在func关键字后面的圆括号里指定receiver func (tx StdTx) GetMemo() string { return tx.Memo }</code></code></pre>

方法调用看起来则和Java一样:

<pre><code class="lang-go hljs">stdTx := StdTx{ ... } // 创建结构体实例 memo := stdTx.GetMemo() // 调用方法</code></code></pre> <h4>类方法</h4>

在Java里,可以用<code>static</code>关键字定义类方法(因此也叫做静态方法):

<pre><code class="lang-java hljs">class StdTx { private static long maxGasWanted = (1 << 63) - 1; public static long getMaxGasWanted() { return maxGasWanted; } }</code></code></pre>

类方法直接在类上调用:<code>StdTx.getMaxGasWanted()</code>。Go语言没有对应的概念,只能用普通函数(不指定receiver)来模拟(下面这个函数在Cosmos-SDK中并不存在,仅仅是为了演示而已):

<pre><code class="lang-go hljs">func MaxGasWanted() long { return maxGasWanted }</code></code></pre> <h4>构造函数</h4>

在Java里,和类同名且不指定返回值的实例方法即是构造函数

<pre><code class="lang-java hljs">class StdTx { StdTx(String memo) { this.memo = memo; } }</code></code></pre>

使用关键字<code>new</code>调用构造函数就可以创建类实例(参加前面出现的例子)。Go语言没有提供专门的构造函数概念,但是很容易使用普通的函数来模拟:

<pre><code class="lang-go hljs">func NewStdTx(msgs []sdk.Msg, fee StdFee, sigs []StdSignature, memo string) StdTx { return StdTx{ Msgs: msgs, Fee: fee, Signatures: sigs, Memo: memo, } }</code></code></pre> <h3>信息隐藏</h3>

如果不想让代码变得不可维护,那么一定要把类或者实例状态隐藏起来,不必要对外暴露的方法也要隐藏起来。Java语言提供了4种可见性:

<table><thead><tr><th>Java类/字段/方法可见性</th> <th>类内可见</th> <th>包内可见</th> <th>子类可见</th> <th>完全公开</th> </tr></thead><tbody><tr><td>用public关键字修饰</td> <td>✔</td> <td>✔</td> <td>✔</td> <td>✔</td> </tr><tr><td>用protected关键字修饰</td> <td>✔</td> <td>✔</td> <td>✔</td> <td>✘</td> </tr><tr><td>不用任何可见性修饰符修饰</td> <td>✔</td> <td>✔</td> <td>✘</td> <td>✘</td> </tr><tr><td>用private关键字修饰</td> <td>✔</td> <td>✘</td> <td>✘</td> <td>✘</td> </tr></tbody></table>

相比之下,Go语言只有两种可见性:完全公开,或者包内可见。如果全局变量、函数、方法、结构体、结构体字段等等以大写字母开头,则完全公开,否则仅在同一个包内可见。

<h3>继承</h3>

在Java里,类通过<code>extends</code>关键字继承其他类。继承其他类的类叫做子类(Subclass),被继承的类叫做超类(Superclass),子类会继承超类的所有非私有字段和方法。以Cosmos-SDK提供的账户体系为例:

<pre><code class="lang-java hljs">class BaseAccount { /* 字段和方法省略 */ } class BaseVestingAccount extends BaseAccount { /* 字段和方法省略 */ } class ContinuousVestingAccount extends BaseVestingAccount { /* 字段和方法省略 */ } class DelayedVestingAccount extends BaseVestingAccount { /* 字段和方法省略 */ }</code></code></pre>

Go没有"继承"这个概念,只能通过"组合"来模拟。在Go里,如果结构体的某个字段(暂时假设这个字段也是结构体类型,并且可以是指针类型)没有名字,那么外围结构体就可以从内嵌结构体那里"继承"方法。下面是Account类继承体系在Go里面的表现:

<pre><code class="lang-go hljs">type BaseAccount struct { /* 字段省略 */ } type BaseVestingAccount struct { *BaseAccount // 其他字段省略 } type ContinuousVestingAccount struct { *BaseVestingAccount // 其他字段省略 } type DelayedVestingAccount struct { *BaseVestingAccount }</code></code></pre>

比如<code>BaseAccount</code>结构体定义了<code>GetCoins()</code>方法:

<pre><code class="lang-go hljs">func (acc *BaseAccount) GetCoins() sdk.Coins { return acc.Coins }</code></code></pre>

那么<code>BaseVestingAccount</code>、<code>DelayedVestingAccount</code>等结构体都"继承"了这个方法:

<pre><code class="lang-go hljs">dvacc := auth.DelayedVestingAccount{ ... } coins := dvacc.GetCoins() // 调用BaseAccount#GetCoins()</code></code></pre> <h4>利斯科夫替换原则</h4>

OO编程的一个重要原则是利斯科夫替换原则(Liskov Substitution Principle,后面简称LSP)。简单来说,任何超类能够出现的地方(例如局部变量、方法参数等),都应该可以替换成子类。以Java为例:

<pre><code class="lang-java hljs">BaseAccount bacc = new BaseAccount(); bacc = new DelayedVestingAccount(); // LSP</code></code></pre>

很遗憾,Go的结构体嵌套不满足LSP:

<pre><code class="lang-go hljs">bacc := auth.BaseAccount{} bacc = auth.DelayedVestingAccount{} // compile error: cannot use auth.DelayedVestingAccount literal (type auth.DelayedVestingAccount) as type auth.BaseAccount in assignment</code></code></pre>

在Go里,只有使用接口时才满足SLP。接口在后面会介绍。

<h4>方法重写</h4>

在Java里,子类可以重写(Override)超类的方法。这个特性非常重要,因为这样就可以把很多一般的方法放到超类里,子类按需重写少量方法即可,尽可能避免重复代码。仍以账户体系为例,账户的<code>SpendableCoins()</code>方法计算某一时间点账户的所有可花费余额。那么<code>BaseAccount</code>提供默认实现,子类重写即可:

<pre><code class="lang-java hljs">class BaseAccount { // 其他字段和方法省略 Coins SpendableCoins(Time time) { return GetCoins(); // 默认实现 } } class ContinuousVestingAccount { // 其他字段和方法省略 Coins SpendableCoins(Time time) { // 提供自己的实现 } } class DelayedVestingAccount { // 其他字段和方法省略 Coins SpendableCoins(Time time) { // 提供自己的实现 } }</code></code></pre>

在Go语言里可以通过在结构体上重新定义方法达到类似的效果:

<pre><code class="lang-go hljs">func (acc *BaseAccount) SpendableCoins(_ time.Time) sdk.Coins { return acc.GetCoins() } func (cva ContinuousVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins { return cva.spendableCoins(cva.GetVestingCoins(blockTime)) } func (dva DelayedVestingAccount) SpendableCoins(blockTime time.Time) sdk.Coins { return dva.spendableCoins(dva.GetVestingCoins(blockTime)) }</code></code></pre>

在结构体实例上直接调用重写的方法即可:

<pre><code class="lang-go hljs">dvacc := auth.DelayedVestingAccount{ ... } coins := dvacc.SpendableCoins(someTime) // DelayedVestingAccount#SpendableCoins()</code></code></pre> <h4>方法重载</h4>

为了讨论的完整性,这里简单介绍一下方法重载。在Java里,同一个类(或者超类和子类)可以允许有同名方法,只要这些方法的签名(由参数个数、顺序、类型共同确定)各不相同即可。以Cosmos-SDK提供的Dec类型为例:

<pre><code class="lang-java hljs">public class Dec { // 字段省略 public Dec mul(int i) { /* 代码省略 */ } public Dec mul(long i) { /* 代码省略 */ } // 其他方法省略 }</code></code></pre>

无论是方法还是普通函数,在Go语言里都无法进行重载(不支持),因此只能起不同的名字:

<pre><code class="lang-go hljs">type Dec struct { /* 字段省略 */ } func (d Dec) MulInt(i Int) Dec { /* 代码省略 */ } func (d Dec) MulInt64(i int64) Dec { /* 代码省略 */ } // 其他方法省略</code></code></pre> <h4>多态</h4>

方法的重写要配合多态)(具体来说,这里只关心动态分派)才能发挥全部威力。以Tendermint提供的Service为例,Service可以启动、停止、重启等等。下面是Service接口的定义(Go语言):

<pre><code class="lang-go hljs">type Service interface { Start() error OnStart() error Stop() error OnStop() error Reset() error OnReset() error // 其他方法省略 }</code></code></pre>

翻译成Java代码是下面这样:

<pre><code class="lang-java hljs">interface Servive { void start() throws Exception; void onStart() throws Exception; void stop() throws Exception; void onStop() throws Exception; void reset() throws Exception; void onRest() throws Exception; // 其他方法省略 }</code></code></pre>

不管是何种服务,启动、停止、重启都涉及到判断状态,因此<code>Start()</code>、<code>Stop()</code>、<code>Reset()</code>方法非常适合在超类里实现。具体的启动、停止、重启逻辑则因服务而异,因此可以由子类在<code>OnStart()</code>、<code>OnStop()</code>、<code>OnReset()</code>方法中提供。以<code>Start()</code>和<code>OnStart()</code>方法为例,下面先给出用Java实现的<code>BaseService</code>基类(只是为了说明多态,因此忽略了线程安全、异常处理等细节):

<pre><code class="lang-java hljs">public class BaseService implements Service { private boolean started; private boolean stopped; public void onStart() throws Exception { // 默认实现;如果不想提供默认实现,这个方法可以是abstract } public void start() throws Exception { if (started) { throw new AlreadyStartedException(); } if (stopped) { throw new AlreadyStoppedException(); } onStart(); // 这里会进行dynamic dispatch started = true; } // 其他字段和方法省略 }</code></code></pre>

很遗憾,在Go语言里,结构体嵌套 方法重写并不支持多态。因此在Go语言里,不得不把代码写的更tricky一些。下面是Tendermint里<code>BaseService</code>结构体的定义:

<pre><code class="lang-go hljs">type BaseService struct { Logger log.Logger name string started uint32 // atomic stopped uint32 // atomic quit chan struct{} // The "subclass" of BaseService impl Service }</code></code></pre>

再来看<code>OnStart()</code>和<code>Start()</code>方法:

<pre><code class="lang-go hljs">func (bs *BaseService) OnStart() error { return nil } func (bs *BaseService) Start() error { if atomic.CompareAndSwapUint32(&bs.started, 0, 1) { if atomic.LoadUint32(&bs.stopped) == 1 { bs.Logger.Error(fmt.Sprintf("Not starting %v -- already stopped", bs.name), "impl", bs.impl) // revert flag atomic.StoreUint32(&bs.started, 0) return ErrAlreadyStopped } bs.Logger.Info(fmt.Sprintf("Starting %v", bs.name), "impl", bs.impl) err := bs.impl.OnStart() // 重点看这里 if err != nil { // revert flag atomic.StoreUint32(&bs.started, 0) return err } return nil } bs.Logger.Debug(fmt.Sprintf("Not starting %v -- already started", bs.name), "impl", bs.impl) return ErrAlreadyStarted }</code></code></pre>

可以看出,为了模拟多态效果,<code>BaseService</code>结构体里多出一个难看的<code>impl</code>字段,并且在<code>Start()</code>方法里要通过这个字段去调用<code>OnStart()</code>方法。毕竟Go不是真正意义上的OO语言,这也是不得已而为之。

<h4>例子:Node</h4>

为了进一步加深理解,我们来看一下Tendermint提供的<code>Node</code>结构体是如何继承<code>BaseService</code>的。<code>Node</code>结构体表示Tendermint全节点,下面是它的定义:

<pre><code class="lang-go hljs">type Node struct { cmn.BaseService // 其他字段省略 }</code></code></pre>

可以看到,<code>Node</code>嵌入("继承")了<code>BaseService</code>。<code>NewNode()</code>函数创建<code>Node</code>实例,函数中会初始化<code>BaseService</code>:

<pre><code class="lang-go hljs">func NewNode(/* 参数省略 */) (*Node, error) { // 省略无关代码 node := &Node{ ... } node.BaseService = *cmn.NewBaseService(logger, "Node", node) return node, nil }</code></code></pre>

可以看到,在调用<code>NewBaseService()</code>函数创建<code>BaseService</code>实例时,传入了<code>node</code>指针,这个指针会被赋值给<code>BaseService</code>的<code>impl</code>字段:

<pre><code class="lang-go hljs">func NewBaseService(logger log.Logger, name string, impl Service) *BaseService { return &BaseService{ Logger: logger, name: name, quit: make(chan struct{}), impl: impl, } }</code></code></pre>

经过这么一番折腾之后,<code>Node</code>只需重写<code>OnStart()</code>方法即可,这个方法会在"继承"下来的<code>Start()</code>方法中被正确调用。下面的UML"类图"展示了<code>BaseService</code>和<code>Node</code>之间的关系:

<pre><code class="lang-go hljs"> ------------- | BaseService |<>--- ------------- | △ | | | ------------- | | Node |<---- ------------- </code></code></pre> <h2>接口</h2>

Java和Go都支持接口,并且用起来也非常类似。前面介绍过的Cosmos-SDK里的<code>Account</code>以及Temdermint里的<code>Service</code>,其实都有相应的接口。<code>Service</code>接口的代码前面已经给出过,下面给出<code>Account</code>接口的完整代码以供参考:

<pre><code class="lang-go hljs">type Account interface { GetAddress() sdk.AccAddress SetAddress(sdk.AccAddress) error // errors if already set. GetPubKey() crypto.PubKey // can return nil. SetPubKey(crypto.PubKey) error GetAccountNumber() uint64 SetAccountNumber(uint64) error GetSequence() uint64 SetSequence(uint64) error GetCoins() sdk.Coins SetCoins(sdk.Coins) error // Calculates the amount of coins that can be sent to other accounts given // the current time. SpendableCoins(blockTime time.Time) sdk.Coins // Ensure that account implements stringer String() string }</code></code></pre>

在Go语言里,使用接口 各种不同实现可以达到LSP的效果,具体用法也比较简单,这里略去代码演示。

<h4>扩展</h4>

在Java里,接口可以使用<code>extends</code>关键字扩展其他接口,仍以Account系统为例:

<pre><code class="lang-java hljs">interface VestingAccount extends Account { Coins getVestedCoins(Time blockTime); Coint getVestingCoins(Time blockTime); // 其他方法省略 }</code></code></pre>

在Go里,在接口里直接嵌入其他接口即可:

<pre><code class="lang-go hljs">type VestingAccount interface { Account // Delegation and undelegation accounting that returns the resulting base // coins amount. TrackDelegation(blockTime time.Time, amount sdk.Coins) TrackUndelegation(amount sdk.Coins) GetVestedCoins(blockTime time.Time) sdk.Coins GetVestingCoins(blockTime time.Time) sdk.Coins GetStartTime() int64 GetEndTime() int64 GetOriginalVesting() sdk.Coins GetDelegatedFree() sdk.Coins GetDelegatedVesting() sdk.Coins }</code></code></pre> <h4>实现</h4>

对于接口的实现,Java和Go表现出了不同的态度。在Java中,如果一个类想实现某接口,那么必须用<code>implements</code>关键字显式声明,并且必须一个不落的实现接口里的所有方法(除非这个类被声明为抽象类,那么检查推迟进行),否则编译器就会报错:

<pre><code class="lang-java hljs">class BaseAccount implements Account { // 必须实现所有方法 }</code></code></pre>

Go语言则不然,只要一个结构体定义了某个接口的全部方法,那么这个结构体就隐式实现了这个接口:

<pre><code class="lang-go hljs">type BaseAccount struct { /* 字段省略 */ } // 不需要,也没办法声明要实现那个接口 func (acc BaseAccount) GetAddress() sdk.AccAddress { /* 代码省略 */ } // 其他方法省略</code></code></pre>

Go的这种做法很像某些动态语言里的鸭子类型。可是有时候想像Java那样,让编译器来保证某个结构体实现了特定的接口,及早发现问题,这种情况怎么办?其实做法也很简单,Cosmos-SDK/Tendermint里也不乏这样的例子,大家一看便知:

<pre><code class="lang-go hljs">var _ Account = (*BaseAccount)(nil) var _ VestingAccount = (*ContinuousVestingAccount)(nil) var _ VestingAccount = (*DelayedVestingAccount)(nil)</code></code></pre>

通过定义一个不使用的、具有某种接口类型的全局变量,然后把nil强制转换为结构体(指针)并赋值给这个变量,这样就可以触发编译器类型检查,起到及早发现问题的效果。

<h2>总结</h2>

本文以Java为例,讨论了OO编程中最主要的一些概念,并结合Tendermint/Comsos-SDK源代码介绍了如何在Golang中模拟这些概念。下表对本文中讨论的OO概念进行了总结:

<table><thead><tr><th>OO概念</th> <th>Java</th> <th>在Golang中对应/模拟</th> </tr></thead><tbody><tr><td>类</td> <td>class</td> <td>struct</td> </tr><tr><td>实例字段</td> <td>instance field</td> <td>filed</td> </tr><tr><td>类字段</td> <td>static field</td> <td>global var</td> </tr><tr><td>实例方法</td> <td>instance method</td> <td>method</td> </tr><tr><td>类方法</td> <td>static method</td> <td>func</td> </tr><tr><td>构造函数</td> <td>constructor</td> <td>func</td> </tr><tr><td>信息隐藏</td> <td>modifier</td> <td>由名字首字母大小写决定</td> </tr><tr><td>子类继承</td> <td>extends</td> <td>embedding</td> </tr><tr><td>LSP</td> <td>完全满足</td> <td>只对接口有效</td> </tr><tr><td>方法重写</td> <td>overriding</td> <td>可以重写method,但不支持多态</td> </tr><tr><td>方法重载</td> <td>overloading</td> <td>不支持</td> </tr><tr><td>多态(方法动态分派)</td> <td>完全支持</td> <td>不支持,但可以通过一些tricky方式来模拟</td> </tr><tr><td>接口</td> <td>interface</td> <td>interface</td> </tr><tr><td>接口扩展</td> <td>extends</td> <td>embedding</td> </tr><tr><td>接口实现</td> <td>显式实现(编译器检查)</td> <td>隐式实现(鸭子类型)</td> </tr></tbody></table>

本文由CoinEx Chain团队Chase写作,转载无需授权。


到此这篇关于“Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用”的文章就介绍到这了,更多文章或继续浏览下面的相关文章,希望大家以后多多支持JQ教程网!

您可能感兴趣的文章:
Go面向对象编程以及在Tendermint/Cosmos-SDK中的应用
Go函数式编程以及在Tendermint/Cosmos-SDK中的应用
1.2Go语言开发环境搭建
php的sdk开发包怎么使用
golang 以及 go SDK 交叉编译
图解 Go 反射实现原理
Go 语言学习第一章节
带你领略Go源码的魅力----Go内存原理详解
ASP.NET 入门的五个步骤
[GO语言基础] 一.为什么我要学习Golang以及GO语言入门普及

[关闭]
~ ~