![aa66db4b09dfdecc32cbcde30c2ce151.png](https://i-blog.csdnimg.cn/blog_migrate/8ec72cb1df859c16c8d79576a6209d41.jpeg)
先声明一下,我的文章每一个字都是自己写的!如果我想翻译什么文章过来,我会事先声明。。。
这篇文章主要是讲我个人所理解的Go语言所提倡的设计规范,但很遗憾,我发现很多人学习了Go,但从来不往这方面去想,Go语言为什么要这么设计?为什么要舍弃面向对象?为什么不加入泛型呢?我想用一些例子来加以作答。
Go提供了有史以来最强大的非面向对象的结构体类。
下面的例子以实现部分线性代数功能为目的,我们先构造两种结构体,一个是实数向量,一个是实数矩阵,另外我们还实现了赋值成员函数或者method。
请不要在意一些检查,鉴于篇幅,我们不可能把所有的都写上,所以请当做伪代码看待。
type
他们的成员是64位浮点数slice用以包含数据,而RMatrix是由一个二维slice来包含数据的。相应的,我们还可以定义所谓的复数向量和复数矩阵
type
这么设计是合理的,因为向量根据成员的类型不同,确实有好几种,而矩阵也一样。甚至于,你还可以根据不同精度的数据来定义32位向量和矩阵。
所以,在编程中,你会遇到形形色色的问题,往往你不得不写出好几个不同的类或者结构体来,他们都属于一个大类,比如,好几种向量,好几种矩阵。
对于向量和矩阵的赋值操作已经写完了,接下来我们可能会考虑打印问题,不过这并不重要,因为打印函数也只是每一个结构体自己的成员函数。真正的第一个难点是,向量和矩阵的操作,比如,两个向量的加法!
这一段我们只考虑向量。
我们可以考虑写一个加法函数,首字母大写,作为包的一个输出函数
func
很简单,就是做两个实数向量的加法用的。但是这么设计不太方便,因为两个向量可能一个是复数向量,一个是实数向量,也有可能两个都是复数向量,那么是不是要写三个这样的函数呢?
这时候你是不是特别怀念泛型?但是,我们不需要泛型,因为Go语言设计接口就是为了这个目的!我们考虑以下接口
type
对于RVector和CVector都实现这个接口,接下来就是改写VecAdd
func
泛型就是这么实现的,连类型识别都不用的!所以Go语言其实是主张
任何函数的输入参数和返回值,除了基础类型(比如int等)以外,请尽量使用接口类型!并把函数内要使用的对象的成员函数在接口中定义!
但是,大家是不是觉得有点问题呢?为什么Vector里的ValAt函数一定要返回一个复数呢?害得VecAdd也默认新建一个复数向量。。。并没有必要这么做。
解决方法主要还是靠类型识别,可以在VecAdd里判断输入向量的类型到底是实数还是复数,然后决定新建那种向量对象,所以这不是什么大问题。
问题在于成员函数ValAt一直输出的是complex128,这么做的好处是排除了强类型运算要不停type assertion带来的麻烦,但不是很好的解决方案。我们满意于float32到float64的提升,但并不满意于float64到complex128的转变。。。
所以,我认为比较好的一个方案,就是把复数向量从Vector接口中赶出去,所有的实现Vector接口的结构都是各种不同精度的float的向量,唯独没有复数向量,所以他们最后都成为float64。而针对复数向量,写一个新的复数向量接口,接纳所有的向量,在里面全部变成复数向量并运算。
接下来,让我们约定一些很重要的东西,比如,你是怎么看待向量的?我的默认是
所有的向量都是列向量,所以也是一种特殊的只有一列的矩阵!
所以,向量也成了一种特殊的矩阵不是吗?按照这个逻辑,向量和矩阵其实也是可以想加减的。
- 矩阵也可以有Length,也就是列数乘以行数
- 向量也可以有两个坐标,行和列,只不过列向量比较特殊,只有一列。
- 如果矩阵和向量的Length一样,那么就直接按元素相加
- 如果矩阵和向量的Length不一样,向量的Length小,那么就还是把向量加到矩阵上去,向量加完了再从头开始加。
于是,我们可以写一个矩阵接口
type
我们就不考虑复数矩阵了。其中,Length已经介绍过了,Dims返回两个整数,分别对应行数和列数,ValAt可以得到相应位置的元素,而Transpose则返回一个自身的转置矩阵。
于是我们可以有以下针对所有实现了Matrix接口结构体的“泛型“函数
func
矩阵的花样比向量多,我们还可以定义一些特殊矩阵,比如对称矩阵、稀疏矩阵等,他们都应该是结构体struct,都应该实现上述的接口。
看到这里,大家也许觉得,标量其实也可以是矩阵和向量呀!没错,我们可以这么实现
type
然后让这个新结构体实现Vector和Matrix接口定义的所有函数,这都不用细说了。但是这个简单的结构体说明了一个问题,任何Go的对象都可以实现泛型不是吗?设计一个接口和若干实现该接口的结构体不就行了?
请尽量不要使用 interface{} !
这是一个很不好的习惯!你如果习惯了这种做法,你将会看到type assertion满天飞。。。这不单单影响程序运行效率,而且还降低了可读性和可维护性。
所以,还是要记住上面的话
设计一个接口和若干实现该接口的结构体,而所有的函数,除了基础类型(比如int等)以外,请尽量只接纳接口并返回接口。
剩下的问题是什么?代码量!
按照Go语言这么写,你会发现代码量蹭蹭蹭往上升,这时候你会特别怀念Python。。。
但实际上这么想不太公平的。增加的代码量其实都是无脑代码!
你可以尝试这么做,写代码之前,找张纸,把你的设计思路写下来,结构体和接口之间的关系图画一画,然后按照这个来写,基本上是无脑的,这其实是Go强大所在!
另外,科班出身的人不要跟我吐槽这个很难很烦。。。你们当年学的就是这些,还不是手拿把攥手到擒来的事情?
Go的设计者们认为,其实绝大多数任务都不需要面向对象,我们只是需要面向对象思想里的一些特性,比如,多态,然后泛型也不是个问题了。。。
最后,Go语言其实是一种逼格很高的语言,它自带歧视,对科班出身的程序员很友好,但是对非科班出身的程序员很不友好。。。