Golang中的SOLID原则


SOLID原则是2002年提出的,关于这个原则的描述和应用大多都是用Java代码作为示例,对于在SOLID原则之后十多年才被创造出来的Golang跟Java这类古老的语言还是有明显的差异的,所以有必要看看如何基于SOLID原则设计Golang的程序。

单一职责原则

A class should have one, and only one, reason to change.

单一职责最大的好处是最小的代码量达成修改目标,提到最小修改代码量就是涉及到耦合和内聚问题。Go里面没有class的概念,在Go中所有的代码都在一个package中,通过import使两个package建立源码级别的耦合,因而从粗粒度上来看Go的单一职责发生在package,当然从函数和方法的角度也会涉及到单一职责问题,这个问题是各种语言都会碰到的。Go标准库中的一些优秀 package 示例:

  • net/http – 提供 http 客户端和服务端
  • os/exec – 执行外部命令
  • encoding/json – 实现 JSON 文档的编码和解码

开放/封闭原则

Software entities should be open for extension, but closed for modification.

如下面的代码所示,我们有一个go类型A ,有一个字段year和一个方法Greet。我们有第二种类型B,它嵌入了一个A,因为A嵌入,因此调用者看到 B 的方法覆盖了A的方法。因为A作为字段嵌入B ,B可以提供自己的Greet方法,掩盖了A的Greet方法。但嵌入不仅适用于方法,还可以访问嵌入类型的字段。如您所见,因为A和B都在同一个包中定义,所以B可以访问A的私有year字段,就像在B中声明一样。因此嵌入是一个强大的工具,允许go的类型对扩展开放。

package main

type A struct {
        year int
}

func (a A) Greet() { fmt.Println("Hello Golang", a.year) }

type B struct {
        A
}

func (b B) Greet() { fmt.Println("Welcome to Golang", b.year) }

func main() {
        var a A
        a.year = 2021
        var b B
        b.year = 2021
        a.Greet() // Hello Golang 2021
        b.Greet() // Welcome to Golang 2021
}

里氏替换原则

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

在基于类的语言中,里氏替换原则通常被解释为,具有各种具体子类型的抽象基类的规范,但是go没有类和继承,所以没有办法根据抽象类层次结构实现替换,go中需要实现替换的是接口(即interface),这一点深刻影响了go的变成范式:go的类型(struct)不需要指定它们实现特定接口(interface)而是任何类型都可以实现接口,只要它具有的函数签名与接口声明的方法匹配即可

如下代码所示,crypto.randos.File都实现了io.Reader接口,分别操作/dev/random设备和磁盘文件:

// io.Reader
type Reader interface {
        // Read reads up to len(buf) bytes into buf.
        Read(buf []byte) (n int, err error)
}

// crypto.rand.Reader
type devReader struct {
    name string
    f    io.Reader
    mu   sync.Mutex
    used int32 // atomic; whether this devReader has been used
}
func (r *devReader) Read(b []byte) (n int, err error) {
    if atomic.CompareAndSwapInt32(&r.used, 0, 1) {
        // First use of randomness. Start timer to warn about
        // being blocked on entropy not being available.
        t := time.AfterFunc(60*time.Second, warnBlocked)
        defer t.Stop()
    }
    if altGetRandom != nil && r.name == urandomDevice && altGetRandom(b) {
        return len(b), nil
    }
    r.mu.Lock()
    defer r.mu.Unlock()
    if r.f == nil {
        f, err := os.Open(r.name)
        if f == nil {
            return 0, err
        }
        if runtime.GOOS == "plan9" {
            r.f = f
        } else {
            r.f = bufio.NewReader(hideAgainReader{f})
        }
    }
    return r.f.Read(b)
}

// os.File
type File struct {
    *file // os specific
}
func (f *File) Read(b []byte) (n int, err error) {
    if err := f.checkValid("read"); err != nil {
        return 0, err
    }
    n, e := f.read(b)
    return n, f.wrapErr("read", e)
}

接口隔离原则

Clients should not be forced to depend on methods they do not use.

接口隔离原则简单来说就是建立单一的接口, 不要建立臃肿庞大的接口。也就是接口尽量细化,同时接口中的方法尽量少,保持接口纯洁性。go的标准库io里面定义了大量的interface,每个interface的函数都不多,由于interface都比较小,所以他们能够方便的互相组合,比如ReadWriter是有Reader和Writer组成的。

// Reader is the interface that wraps the basic Read method.
//
// Read reads up to len(p) bytes into p. It returns the number of bytes
// read (0 <= n <= len(p)) and any error encountered. Even if Read
// returns n < len(p), it may use all of p as scratch space during the call.
// If some data is available but not len(p) bytes, Read conventionally
// returns what is available instead of waiting for more.
//
// When Read encounters an error or end-of-file condition after
// successfully reading n > 0 bytes, it returns the number of
// bytes read. It may return the (non-nil) error from the same call
// or return the error (and n == 0) from a subsequent call.
// An instance of this general case is that a Reader returning
// a non-zero number of bytes at the end of the input stream may
// return either err == EOF or err == nil. The next Read should
// return 0, EOF.
//
// Callers should always process the n > 0 bytes returned before
// considering the error err. Doing so correctly handles I/O errors
// that happen after reading some bytes and also both of the
// allowed EOF behaviors.
//
// Implementations of Read are discouraged from returning a
// zero byte count with a nil error, except when len(p) == 0.
// Callers should treat a return of 0 and nil as indicating that
// nothing happened; in particular it does not indicate EOF.
//
// Implementations must not retain p.
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Writer is the interface that wraps the basic Write method.
//
// Write writes len(p) bytes from p to the underlying data stream.
// It returns the number of bytes written from p (0 <= n <= len(p))
// and any error encountered that caused the write to stop early.
// Write must return a non-nil error if it returns n < len(p).
// Write must not modify the slice data, even temporarily.
//
// Implementations must not retain p.
type Writer interface {
    Write(p []byte) (n int, err error)
}

// Closer is the interface that wraps the basic Close method.
//
// The behavior of Close after the first call is undefined.
// Specific implementations may document their own behavior.
type Closer interface {
    Close() error
}

// Seeker is the interface that wraps the basic Seek method.
//
// Seek sets the offset for the next Read or Write to offset,
// interpreted according to whence:
// SeekStart means relative to the start of the file,
// SeekCurrent means relative to the current offset, and
// SeekEnd means relative to the end.
// Seek returns the new offset relative to the start of the
// file and an error, if any.
//
// Seeking to an offset before the start of the file is an error.
// Seeking to any positive offset is legal, but the behavior of subsequent
// I/O operations on the underlying object is implementation-dependent.
type Seeker interface {
    Seek(offset int64, whence int) (int64, error)
}

// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
    Reader
    Writer
}

// ReadCloser is the interface that groups the basic Read and Close methods.
type ReadCloser interface {
    Reader
    Closer
}

// WriteCloser is the interface that groups the basic Write and Close methods.
type WriteCloser interface {
    Writer
    Closer
}

// ReadWriteCloser is the interface that groups the basic Read, Write and Close methods.
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// ReadSeeker is the interface that groups the basic Read and Seek methods.
type ReadSeeker interface {
    Reader
    Seeker
}

// WriteSeeker is the interface that groups the basic Write and Seek methods.
type WriteSeeker interface {
    Writer
    Seeker
}

// ReadWriteSeeker is the interface that groups the basic Read, Write and Seek methods.
type ReadWriteSeeker interface {
    Reader
    Writer
    Seeker
}

依赖倒置原则

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

依赖调换

依赖倒置其实就是变换依赖方和被依赖方的位置:

  • 高层模块不应该依赖底层模块,都应该依赖于抽象
  • 抽象不依赖具体实现
  • 具体实现依赖抽象

如下图所示,对象A依赖对象B,经过依赖倒置之后对象A依赖变成了抽象A(interfaceA),然后对象B实现了抽象A。

package A

type B_Abstract interface {
    GetB() int
}

type A struct {
    Va int
}

func (a *A) Add(b B_Abstract) int {
    return b.GetB() + a.a
}
package B

type B struct {
    Vb int
}

func(b *B) GetB() int {
    return b.b
}
package main

import (
    "A"
    "B"
)

func main() {
    a := A.A{Va:1}
    b := B.B{Vb:1}
    a.Add(b)
}

依赖管理

如果go的项目已经遵守了前面的4个原则,那么代码应该已经被分解到不同的package中了,每个package都有一个明确定义的责任或目的。项目中代码应该根据接口描述其依赖关系,并且应该考虑这些接口以仅描述这些函数所需的行为。除此之外,还需要注意项目的依赖关系即项目的import graph结构,在go中import graph必须是非循环的。不遵守这种非循环要求将导致编译失败,但更为严重地是它代表设计中存在严重错误。

在所有条件相同的情况下,精心设计的go程序的import graph应该是宽的,相对平坦的,而不是高而窄的。 如果你有一个package,其函数无法在不借助另一个package的情况下运行,那么这或许表明代码没有很好地沿pakcage边界分解。

依赖倒置原则鼓励将特定的责任,沿着import graph尽可能的推向更高层级,推给main package或顶级处理程序,留下较低级别的代码来处理抽象接口。

总结来讲,如果按照SOLID原则来安排go的项目,cycle import等问题可以迎刃而解。

Clean Code Rules


Clean Code Rules

General rules

  • Follow standard conventions.
  • Keep it simple stupid. Simpler is always better. Reduce complexity as much as possible.
  • Boy scout rule. Leave the campground cleaner than you found it.
  • Always find the root cause. Always look for the root cause of a problem.

Design rules

  • Keep configurable data at high levels.
  • Prefer polymorphism to if/else or switch/case.
  • Separate multi-threading code.
  • Prevent over-configurability.
  • Use dependency injection.
  • Follow the Law of Demeter. A class should know only its direct dependencies.

Understandability tips

  • Be consistent. If you do something a certain way, do all similar things in the same way.
  • Use explanatory variables.
  • Encapsulate boundary conditions. Boundary conditions are hard to keep track of. Put the processing for them in one place.
  • Prefer dedicated value objects to a primitive types.
  • Avoid logical dependency. Don’t write methods that work correctly depending on something else in the same class.
  • Avoid negative conditionals.

Names rules

  • Choose descriptive and unambiguous names.
  • Make a meaningful distinction.
  • Use pronounceable names.
  • Use searchable names.
  • Replace magic numbers with named constants.
  • Avoid encodings. Don’t append prefixes or type information.
  • Functions rules
  • Small.
  • Do one thing.
  • Use descriptive names.
  • Prefer fewer arguments.
  • Have no side effects.
  • Don’t use flag arguments. Split method into several independent methods that can be called from the client without the flag.

Comments rules

  • Always try to explain yourself in code.
  • Don’t be redundant.
  • Don’t add obvious noise.
  • Don’t use closing brace comments.
  • Don’t comment out code. Just remove.
  • Use as explanation of intent.
  • Use as clarification of code.
  • Use as warning of consequences.

Source code structure

  • Separate concepts vertically.
  • Related code should appear vertically dense.
  • Declare variables close to their usage.
  • Dependent functions should be close.
  • Similar functions should be close.
  • Place functions in the downward direction.
  • Keep lines short.
  • Don’t use horizontal alignment.
  • Use white space to associate related things and disassociate weakly related.
  • Don’t break indentation.

Objects and data structures

  • Hide internal structure.
  • Prefer data structures.
  • Avoid hybrids structures (half object and half data).
  • Should be small.
  • Do one thing.
  • Small number of instance variables.
  • Base class should know nothing about their derivatives.
  • Better to have many functions than to pass some code into a function to select a behavior.
  • Prefer non-static methods to static methods.

Tests

  • One assert per test.
  • Readable.
  • Fast.
  • Independent.
  • Repeatable.

2020


2020总结——看见与成长

黄龙国际难得在晚上8点的时候就如此冷清,提交了2020的最后一个commit和文档工作之后,自己的节奏就这样慢了下来,挂上降噪耳机过去的一年如同电影画面一帧一帧在脑海中掠过,庚子年见证历史,也是见证我自己的一年,一句话总结自己的2020——看见与成长。

2020流水

回顾2020魔幻而精彩,疫情、美股暴跌、中美贸易战、澳洲大火、Kobe Bryant逝世、美国大选、Black Lives Matter、蚂蚁IPO,我自己也经历了经历了看见与成长。

成长

2020我自己也魔幻而精彩,回顾下来主要有:

  1. 学习理财,从当初不懂任何财经知识的小白变成了一个会分得清楚股票和债券、懂得选基金需要关注哪些信息、能够初步看懂公司的财务报表并切大概根据财报分析和判断公司的发展和前景。今天大概算了一下,经过一年的学习,自己的投资回报率已经达到16.3%。
  2. 锻炼酒量,年初红酒(酒的名字很漂亮:Luna,感谢大王的赠酒ω),春天威士忌,夏天啤酒,然后就一直啤酒。。。现在终于是能喝一点了,也慢慢懂得了酒的乐趣,它是苦恼时的朋友,是高兴时的观众
  3. 沟通技巧,以前的自己不自信,在工作中不敢于表达自己,不知道如果表达自己,这一年走出了自己的舒适圈,打破自己一直“收”的状态,走出去让大家知道我了解我
  4. 效率提升,这一年对自己的投资大部分在效率工具和服务的购买(订阅TickTick、购买了Alfred),学会GTD,学会了使用Notion,学会了番茄工作法
  5. 技术成长,经过Raven一年多的打磨,对golang越来越熟悉也越来清楚go与C的差异,对k8s的代码学习(借此学会go的Visitor模式、委托反转控制等等);大致看了一些kernel代码弄懂了ebpf的原理,借着文件保险箱学了一些kernel开发的经验;从前端开始搭建了一套插件系统,也从中学到了一些产品思维

看见

2020出了成长我也看到了很多:

  1. 搬新家了,有自己的房子可以住很开心
  2. 看着周则夷小朋友一点点长大
  3. 通勤由自己开车变成地铁,利用1个小时的时间进行阅读,剔除技术类书籍今年读了10本书,这是一个非常大的进步
  4. 结识了3个新朋友,老中青三代人,不知道被我说成“老”朋友的那位会不会生气(´▽`),也跟远在他乡的老朋友保持联系
  5. 开始经历真正意义上的婚后生活,总体来讲挺不满意的有难过有无奈,少有感动和共情,尝试重拳和轻抚的方式希望找寻到合适的方法,可是生活依旧平淡如水少有滋味,直到快年底的时候想通了,就像庄子说的“知其不可奈何而安之若命。”,人生之根本,是如何与自己相处

2020我想说

狄更斯说:“这是最好的时代,也是最坏的时代”,不管工作还是生活我还是愿意抱着积极的心态面对,心里面唯一有些不忿的恐怕就是那时不时在心里作祟的理想主义,朋友在给我的postcard里面感慨道:

世人慌慌张张,不过图的是碎银几两。可偏偏就是这碎银几两,能解世间万千惆怅;偏偏就是这几两碎银,可让父母安康,可护幼子成长。但也偏偏是这几两碎银,断了儿时念想,让少年染上沧桑,压弯了脊梁。

2020我想说的是:我路过山时山不说话,但是我不会难过因为我知道路是自己选的,若不是生活所迫,谁愿意把自己弄的一身才华

2021我想说

Only is better than best!

“任抛星汉归园圃,留取乾坤盛酒囊”,我相信也坚持看见自己。

“海棠断枝不见血,机关算尽悲无常‘,我愿意也保持自我成长。

写给周则夷小朋友的一封信


按语

当面对生活的困苦、身体的乏累、精神的折磨时,我时常在想当我成为一名父亲的时候我会为你做什么,能不能用我自己的经验和学识帮助你在经历困苦的时候稍微轻松一点、舒服一点。虽然你现在既不会说话,也听不懂我现在的呢喃,但我还是想送你一份这样的礼物。

《给周则夷小朋友的一封信》

周则夷小朋友,你好!

这是一份来自你父亲的一份迟到的礼物,本来打算在你出生的时候就送给你,一来是记录你的出生带来的欢喜和改变,二来是记录我对你的期待和祝福,可是生活就是这样——“若不是生活所迫,谁愿意把自己弄的一身才华“。你的父亲也是第一次踏上自己的人生路,他也在拼搏、也有迷茫、更有烦恼和力所不能及的事情,所以希望你可以原谅这份迟到的礼物,不过还好在魔幻的2020快结束的时候,这份礼物还是赶上了你的周岁生日。

亲爱的周则夷小朋友,虽然我们没有办法预知未来,但我希望长大的你是这样的:

  1. 我希望你是健康的,这个健康和高矮胖瘦、黑白美丑没有关系,仅仅是希望你健健康康
  2. 我希望你有健康的心理,这里的心理和喜怒哀乐无关,只是健康的心理
  3. 我希望你是独立的,这里的独立是生活自理的独立,更是心智情感上的独立,我会尊重你的选择,但也希望你能够学会对自己的选择负责任
  4. 我希望你是有趣的,人生大多平淡无常,希望你有个有趣的灵魂,人生就像普洱茶看着黑喝着淡,有趣的灵魂会让你在面对枯燥无味的生活时不会觉得乏味
  5. 我希望你是有爱的,你的出生天然附带着母爱、父爱等等,懂得与人为善、知恩图报,当然量力而为就行,至少作为父母不希望成为你的负担和压力
  6. 我希望你是乐观的,从妈妈的肚子里到这个世界,后面你还会经历从童年到少年、从少年到青年等等直至终老,每个阶段一定都是一个不断打破自己边界的过程,顺境中不骄不躁厚积薄发,逆境中不言放弃积极面对,这样你的路才能越走越宽
  7. 我希望你是宽容的,人非圣贤孰能无过,即便你有千般道理也要学会得饶人处且饶人,这样你的路才能越走越远
  8. 我希望你是丰富的,物质的丰富仅仅是基础,但是不要让自己成为被物欲操控的人,精神的丰富才是自我救赎的良方,俗世大多都未守过清规戒律,但却信了因果报应,当你垂垂老矣走到尽头时你会发现,只有丰富的精神才能解答你“是善果还是报应”
  9. 最后的最后,我希望你是一个普通人,这样你可以没有压力和枷锁走好人生路,如果你有爱好和兴趣那么我将是你最大的支持者。当然,如果你只是想做一个普通人,那样也好,这个世界本来就有很多普通人和平凡的幸福,这足够你平凡的一生来体会

结束语

最后让我们不要变成那种“都看得很明白,都活得很不明白”的人,当别人看你的时候你只要——心里有爱、眼里有光就够了!

人民币汇率


人民币汇率

人民币汇率定义

汇率是指两个国家的货币之间的比价、兑换率。人民币汇率即人民币与外币之间的比价、兑换率。
人民币汇率代表人民币的对外价值,由国家外汇管理局在独立自主、统一性原则基础上,参照国内外物价对比水平和国际金融市场汇率浮动情况统一制订、调整,逐日向国内外公布,作为一切外汇收支结算的交换比率,它是官方汇率,没有市场汇率,其标价方法采用国际上通用的直接标价法,即以固定单位 (如100、10000、100000等) 的外币数折合若干数额的人民币,用以表示人民币对外币的汇率。固定单位的外币数大小须视各该外币的价值大小而定,除人民币对比利时法郎和意大利里拉汇率采用一万 (10000)单位、对日元汇率采用十万 (100000)单位作为折算标准外,对其他各种外币汇率均以一百(100)单位作为折算标准。

人民币汇率之在岸 v.s. 离岸

从地理上来讲,在岸汇率和离岸汇率的区别在于中国大陆和中国大陆之外的区别:

  • 在岸汇率,是指在中国大陆换汇的汇率;
  • 离岸汇率,是指在中国大陆以外的换汇的汇率;

我们通常说的兑换外币,一般是在中国大陆的银行完成兑换,所以使用的汇率是按照在岸汇率作为兑换标准。

在岸人民币汇率

在岸汇率:

即央行授权中国外汇中心于每个工作日上午对外公布当日人民币兑换美元、欧元、日元、港币汇率的中间价作为当日银行间即期外汇市场以及银行柜台交易汇率的参考价格,这就叫在岸人民币。

离岸人民币汇率

离岸汇率:

即央行开放香港以及其他国家进行人民币交易的汇率就叫离岸人民币,而2010年中国香港实施的人民币离岸交易(CNH)已经是泛指海外离岸人民币交易。

在岸和离岸关系的讲解文章

2016年底,人民币对美元连连暴跌,不少人认为汇率即将破7,然而看到了在岸汇率的坚挺,结果很快离岸人民币汇率又开始上涨。

在岸汇率和离岸汇率因为不相同,所以会产生一个差,一旦这个差过大,投机者就可以通过低汇率的一岸买进,通过高汇率的一岸卖出,从而进行卖出套利。

同时,大量的套利,又会很快的让离岸和在岸的汇率的差,快速减少。

那么,两者的基本概念是什么?

在岸人民币就指的在国内市场人民币汇率基本水平的一个价格趋势或者价格的走势。主要是在本土,就在我们国内的市场层面。

离岸人民币基本上就是在海外,在中国界外的一些市场,最早的离岸市场可能是在香港,然后又从离岸市场扩展到新加坡。现在新加坡可能是人民币报价体系比较重要的一个离岸市场,当然现在可能还会延展到芝加哥,这样就是说在海外市场人民币的报价水平就称为离岸市场。

它实际上是一个onshore(在岸)和offshore(离岸)这样一个基本概念,一个是在本土国,一个是在海外,这是一个最基本的概念。

两个市场的参与者、价格形成机制以及交易量方面都有较大的差异:

  1. 离岸市场人民币汇率对超预期的经济数据反应更强烈,当经济数据超预期时,汇率会第一时间反映市场,对经济预期的调整。而离岸市场因为没有管制,对数据,尤其是超预期的部分的反应更大。
  2. 离岸和在岸两地流动性差异会影响人民币汇率差价。虽然同是人民币兑美元的汇率,但是两者之间存在着价差。
    • 当大家都急着抢购人民币抛售外币时,离岸人民币市场中人民币流动性恶化,离岸人民币升值压力上升,当CNH比CNY表现出更强的升值压力,价差上升。
    • 当大家争相抛售人民币买美元时,离岸人民币市场中美元流动性恶化,离岸人民币贬值压力上升,价差有可能会下降。一般来说,在正常时期,价差较小,但是在非正常时期,由于离岸人民币市场表现更为敏感,所以价差拉大。

3.国际金融市场的冲击,尤其是海外投资者风险偏好的变化,对离岸市场的人民币汇率影响更大。

因其与国际金融市场的联系更紧密,而在岸市场因为存在管制,对这些冲击就不那么敏感。因此,在国际金融市场较动荡的时候,在岸和离岸汇率通常也会出现较明显的差价。

其实,离岸人民币与在岸人民币之间也会相互影响:

  1. 通过跨境进出口企业贸易结算,当离岸人民币比在岸人民币弱时:也就是在国内换1美元要花掉6.7人民币,而出了国换1美元则要用7元甚至更高的价格。虽然苦了留学党,但是跨境出口企业会倾向于在离岸市场交易,因为同样的美元收入在离岸市场,可换得更多的人民币收入,出口企业在离岸市场上卖出美元、买入人民币又会使离岸人民币升值。

    跨境进口企业则倾向于在在岸上交易,因为同样数额的美元进口支出在在岸市场,可用较少的人民币购买即可。进口企业在在岸市场上出售人民币、买入美元的行为又会使在岸人民币贬值。

  2. 通过无本金交割远期外汇市场交易(NDF)市场,在岸金融机构不允许在离岸人民币市场上交易;离岸金融机构也不允许在岸人民币市场上进行任何活动。

    因此交易者们不能直接在离岸和在岸人民币市场上进行套汇交易,然而,都可以在无本金交割远期外汇交易(NDF)市场上进行操作。

    NDF是一种离岸金融衍生产品,在岸金融机构,可以通过在岸人民币远期市场和NDF市场进行套汇。

  3. 信息或信心渠道也会导致两地汇率趋同

    举个例子来说,在周边国家经济前景恶化的情况下,离岸投资者对内地经济增长的信心也可能下降(因为周边国家是中国的重要出口目的地)。

    这样离岸人民币汇率可能贬值,这又会影响在岸市场对人民币的信心,从而带动在岸人民币汇率同向变化。