ug 满天飞的功能, 我替他们可惜.
null 指针的历史, 满满的都是 bug. 无论是历史, 还是现实, 我都看不出来, 数据存在内存地址为 0x0 的地方有什么意义. 指向 0x0 的指针通常都有特定的含义. 比如, 返回类型是指针的函数出错, 会返回 0x0 . 递归数据结构把 0x0 当作基底(base case), 如: 树结构的页节点, 或链表的结尾. 这也是 null 指针在 Go 中的用法.
然而,这样使用null指针也是不安全的。事实上,null指针是类型系统的后门,它让你能够创造某个根本不是所属类型的实例。程序员有时候会忘记某个指针的值可能是null这个事实,这是一个很常见的情况。在最好的情况下,你的程序会挂掉,而在最坏的情况下,这会产生一个可以被人利用的漏洞。编译器无法轻易地阻止这种情况的发生,因为null指针破坏了语言的类型系统。
对于Go来说,使用多重返回值这个机制,利用它第二个返回值来返回一个代表“失败”的值是一个正确也被鼓励的做法。然而,这种机制很容易被忽略或者误用,并且在表示递归数据结构的时候没有什么用用处。
好的解决方案:代数数据类型和类型安全的错误模式
我们可以使用类型系统来包装错误状况,基底,而不是试图打破类型系统。
现在我们想要构建一个表示链表的类型。我们想表示两种情况:我们是否已经到达了链表的末尾,某个链表的节点上到底有没有被存放在那里的数据。一种类型安全的方式是分别使用不同的类型来表示这些情况,最后将它们组合成一个单独的类型(使用代数数据类型)。现在我们有一个叫做Cons的类型来表示一个存放有某些数据的链表,一个叫做End的类型来表示链表的末尾。我们可以这样写:
(Rust)
(Haskell)
每个类型都为递归操作这个数据结构的算法声明了一个基底(End)。。Rust和Haskell都不允许null指针的出现,所以我们永远都不会碰到null指针解引用所造成的bug(除非我们做一些很大胆的底层操作)。
这些代数数据结构通过像模式匹配(后面讲它)这样的技术,允许我们写出非常明了的代码。
那么,我们如何得到一个可能返回或者不返回给定类型的数据的函数,或是一个可能内部包含或者没有包含一个给定类型的数据的数据结构呢?也就是说,我们如何将错误状况(failure condition)封装到我们的类型系统中来呢?Rust使用Option,Haskell使用一个叫Maybe的类型来解决这个问题。
我们想象这样一个函数,它所作的事情是搜索一个非空字符串的数组,寻找一个以这‘H’开头的字符串,返回第一个找到的这样的字符串,如果没有找到,就返回某种错误状况。在Go语言中,我们可以通过返回nil来表示“没找到”这个错误。但是在Haskell和Rust中,不使用危险的指针,我们就可以安全地完成这个任务。
(Rust)
(Haskell)
我们可以返回一个包含或者没有包含一个字符串的对象来代替返回一个字符串或者null指针的做法。使用search()函数的程序员也会很清楚地知道这个函数可能会失败(因为它返回的对象的类型已经这么说了),而且程序员必须处理这两种状况,否则报错。这样我们就跟null指针解引用所造成的bug说再见了。
给程序中的每个值都指定类型, 有时看起来点过老土。 某些场合, 值的类型显而易见,如
int x = 5 y = x*2
这里的 y 明显就是整形。更复杂点的,我们甚至可以根据函数的参数类型推断出它的返回类型(反之亦然)。
Rust 和 Haskell 都基于 Hindley-Milner 类型系统, 他们都很擅长类型推导, 你可以实现像下面这样好玩的功能:
(Haskell)
函数 (*2) 有一个 Num 类型参数, 返回也是一个Num 类型, Haskell 由此推断 a 和 b 也是 Num 类型. 最后推断出, 该函数有若干个 Num 类型参数, 返回若个 Num 类型的值. 这种方式比 Go 和 C++ 的简单类型推导强大多了. 有了它, 哪怕是结构复杂的程序, 就算我们不声明这么多显性类??, 编译器也能正确处理.
Go 支持 := 赋值操作符, 用法如下:
(Go)
foo := bar()
它的原理是: 查找 bar() 的返回类型, 然后赋给 foo. 下列代码的道理也一样:
(C++)
auto foo = bar();
没什么稀奇的, 无非省去了人工查找函数 bar() 的返回类型, 在键盘上多敲几个字声明 foo 的类型那点时间而已.
不变性是指,在程序生成的时候,设好的值,以后不会再变。 它的优势很明显, 能减少因程序某个地方的数据结构改变,导致另一个地方出现问题的概率。
此外对程序优化也有利。
程序员应当尽可能使用不可变数据结构。 不变性使得判断负面影响和安全性变得更简单。同时也能减少各种 Bug 。
Haskell 默认情况下, 所有的值都是不可变的。改变数据结构就意味着, 在保证正确性的前提下, 重新创建一个新的数据结构。由于 Haskell 采用的是惰性求值(lazy eva luation)和永久性数据结构(persistent data structures), 所以运行的速度还是粉快的。Rust 属于系统级编程语言。不可能使用惰性求值,也就不能像 Haskell 那样始终使用不变性。 因此,虽然 Rust 默认情况下,变量的值是不可变的。 但是,在需要的时候, 还是可以将变量设置成可变的。这样挺好,因为它迫使程序员问自己, 底需不需要将这个变量设成可变的。 这是很好的变成习惯, 对编译器优化代码也有好处。
Go 不支持这项功能。
控制流结构是高级编程语言有别于汇编的原因之一. 它允许我们在抽象层面, 有条理地控制程序流程. 毫无疑问, 所有高级语言都支持控制流结构, 否则, 我还说个毛啊. 可惜, 有那么几种相当不错的控制流结构 Go 不支持.
模式匹配配合数据结构或值使用的时候, 效果相当好. 简直就是 case/switch 的加强版. 我们可以像这样对值进行匹配:
(Rust)
或者像这样解构数据结构(deconstruct data structures):
(Rust)
上面的例子, 有时也称作复合表达式. C 和 Go 中的 if 和 case/switch 语句只用来控制程序流程, 不会返回值; 而 Rust 和 Haskell 的 if 和 模式匹配语句则可以. 既然有值返回, 当然也能用来赋给其他东东. 这里给出一个 if 语句的例子:
(Haskell)
不是我故意找 Go 的茬; 它确实有几个不错的的控制流元素, 如, 用于并行计算的 select. 可惜没有我钟爱的复合表达式和模式匹配. Go 唯一支持赋值的语句, 是像这样的原子表达式 x := 5 或 x := foo().
给嵌入式系统编写程序与在一个有完整操作系统的计算机上编写程序有很大不同。某些语言相比而言更适合嵌入式编程的需要。
对于不少人赞成Go语言可以给机器人编程这件事我很疑惑。基于一些原因,Go语言并不适合用来为嵌入式系统编写程序。这一节并不是对Go语言的指责,Go语言并不是被设计用来编写嵌入式程序的语言。这一章节针对那些吹捧Go语言可以胜任嵌入式编程的人。
子问题 #1:堆和动态内存分配
堆是一块在运行期创建的可以存储任意数量对象的内存区域。我们将对堆的使用称作”动态内存分配“。
通常,在嵌入式系统中使用堆存储空间是不明智的。较大的内存开销和需要管理复杂的数据结构是主要的原因,尤其是当你在一块主频只有8MHz,RAM只有2KB的MCU上写程序的时候。
在实时系统(因为某一操作耗时过长就可能会跪的系统)中使用堆也是不明智的,因为对堆上空间的申请和释放所消耗的时间有很大的不