普通视图

发现新文章,点击刷新页面。
昨天以前忘归

Go 源码阅读:内存分配前的溢出判断

2021年7月11日 19:01

今天在看切片内存分配的源码,makeslice 函数在内存分配前先使用 MaxUintptr 函数来判断内存分配是否越界:

func makeslice(et *_type, len, cap int) unsafe.Pointer {
mem, overflow := math.MulUintptr(et.size, uintptr(cap))
if overflow || mem > maxAlloc || len < 0 || len > cap {
// 先判断是否越界
mem, overflow := math.MulUintptr(et.size, uintptr(len))
if overflow || mem > maxAlloc || len < 0 {
panicmakeslicelen()
}
panicmakeslicecap()
}

// 内存分配
return mallocgc(mem, et, true)
}

出于好奇看了一下 MaxUintptr 的源码:

// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package math

import "runtime/internal/sys"

const MaxUintptr = ^uintptr(0)

// MulUintptr returns a * b and whether the multiplication overflowed.
// On supported platforms this is an intrinsic lowered by the compiler.
func MulUintptr(a, b uintptr) (uintptr, bool) {
if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}
overflow := b > MaxUintptr/a
return a * b, overflow
}

由源码可知,MulUintptr 接收两个参数,分别是要分配的类型大小 a 和要分配的数量 b,计算后返回要分配的内存空间以及是否溢出。

位运算表达式的含义

a|b < 1<<(4*sys.PtrSize) 这个位运算表达式看起来非常复杂,我们来剖析一下。

sys.PtrSize 表示系统指针大小,在 32 位机器中,sys.PtrSize = 4,64 位机器中,sys.PtrSize = 8<< 是左移运算符,我们知道在运算中左移 1 位就是一次乘 2 操作,因此 1<<(4*sys.PtrSize) 表示的其实就是 2^(4*sys.PtrSize)

综上,我们可以把表达式变形一下:

  • 在 32 位机器中,4*sys.PtrSize = 4 * 4 = 16 表达式可以写作 a|b < 2^16,可证明 ab 均小于 2^16,a * b 必然小于 2^32
  • 在 64 位机器中,4*sys.PtrSize = 4 * 8 = 32 表达式可以写作 a|b < 2^32,可证明 ab 均小于 2^32,a * b 必然小于 2^64

那么 2^32 与 2^64 又代表着什么呢?

何为溢出?

我们常说的 64 位系统或 32 位系统,其中的「位数」决定了计算机的寻址空间,即 CPU 对于内存的寻址能力。通俗地讲,就是 CPU 最多能够使用的内存。32 位系统的寻址空间为 2^32,64 位系统的寻址空间为 2^64。

因此,a|b < 1<<(4*sys.PtrSize) 的含义是:要分配的内存是否小于寻址空间。若小于寻址空间,即不存在溢出,此时函数返回 overflow = false

^uintptr(0) 是什么?

unintptr 是 Go 中的自定义整型:

#ifdef _64BIT
typedef uint64 uintptr;
#else
typedef uint32 uintptr;
#endif
  • 32 位系统中,unitptr 代表 uint32,占 4 字节,^uintptr(0) 等于 ^uint32(0),即 2^32 - 1
  • 64 位系统中,unitptr 代表 uint64,占 8 字节,^uintptr(0) 等于 ^uint64(0),即 2^64 - 1

因此,overflow := b > MaxUintptr/a 可以变形为:

  • 在 32 位机器中:overflow := b > (2^32 - 1)/a
  • 在 64 位机器中:overflow := b > (2^64 - 1)/a

这样就很好理解啦。

代码逻辑思考

如果由我来写这段代码,我无法想到这样的写法,大概率会使用 a * b < MaxUintptr 来暴力解决问题。然而计算机中乘法与除法并不意味着更快的计算过程,他们的本质还是使用累加器,而位运算才意味着高效。因此,先使用 a|b < 1<<(4*sys.PtrSize) 作为判断是非常巧妙的做法。

存在的疑问

if a|b < 1<<(4*sys.PtrSize) || a == 0 {
return a * b, false
}

我对以上这句逻辑判断存在疑问,根据短路求值,把 a == 0 写在前面是否更好呢?以及是否需要把 b == 0 也补上?准备提个 issue 问问开发者吧。

参考资料

MySQL 覆盖索引与延迟关联

2020年9月1日 20:35

在了解覆盖索引与延迟关联前,我们先简单建立一个订单表 Orders 用于举例说明。表中共包含 3 个字段:

  • id:订单 ID,int 类型,主键自增长
  • product_id:商品 ID,在此列上建立索引
  • name:订单名称
CREATE TABLE `orders` (
`id` int(10) NOT NULL COMMENT '订单 ID',
`product_id` int(10) DEFAULT NULL COMMENT '商品 ID',
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT NULL COMMENT '订单名称',
PRIMARY KEY (`id`),
KEY `product_idx` (`product_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

覆盖索引

什么是覆盖索引?

我们知道,如果 MySQL 根据索引查找到数据,但索引的叶子结点中并不包含我们所需要的数据字段,那么仍然需要进行回表查询。

如果一个索引包含(覆盖)我们所需要查询的所有字段值,我们就称之为「覆盖索引」。

MyISAM

当使用 MyISAM 存储引擎时,由于我们在 product_id 建立了索引,所以 SELECT product_id FROM orders 将使用覆盖索引:

mysql> EXPLAIN SELECT product_id FROM orders;
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+------+----------+-------------+
| 1 | SIMPLE | orders | NULL | index | NULL | product_idx | 5 | NULL | 2 | 100.00 | Using index |
+----+-------------+--------+------------+-------+---------------+-------------+---------+------+------+----------+-------------+
1 row in set (0.00 sec)

如果我们在查询字段中加入 id 列,即执行 SELECT id, product_id FROM orders WHERE product_id = 1,查询轨迹如下:

  1. product_id 索引树中找到 product_id = 1 子结点
  2. 通过该子结点指针读取磁盘上的数据行
  3. 取出数据行中的 id 字段

由于 MyISAM 的叶子结点存储着指向数据行的指针,该查询多了一步回表操作,无法使用覆盖索引。

mysql> EXPLAIN SELECT id, product_id FROM orders WHERE product_id = 1;
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | orders | NULL | ref | product_idx | product_idx | 5 | const | 1 | 100.00 | NULL |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------+
1 row in set (0.00 sec)

MyISAM 索引结构

InnoDB

InnoDB 与 MyISAM 的不同之处在于,InnoDB 的主键使用聚簇索引,而其二级索引的叶子结点保存着行的主键值。也就是说,二级索引不仅能覆盖其本身,也能覆盖到该行的主键值。

InnoDB 二级索引的叶子结点包含行主键值

由于 InnoDB 不同的数据存储方式,若使用 InnoDB 作为存储引擎,我们执行 SELECT id, product_id FROM orders WHERE product_id = 1 将得到如下结果:

mysql> EXPLAIN SELECT id, product_id FROM orders WHERE product_id = 1;
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | orders | NULL | ref | product_idx | product_idx | 5 | const | 1 | 100.00 | Using index |
+----+-------------+--------+------------+------+---------------+-------------+---------+-------+------+----------+-------------+
1 row in set (0.01 sec)

可以看到 Extra 显示 Using index,表示该查询使用了覆盖索引。该查询语句的查询轨迹如下:

  1. 在二级索引 product_id 的索引树中找到 product_id = 1 的叶子结点
  2. 取出该叶子结点的行主键值 id 一并返回

查询轨迹并未进行回表取值。

覆盖索引的好处

延迟关联

延迟关联(deferred join)指「延迟了对列的访问」,不直接获取所有需要的列。

在查询的第一阶段 MySQL 使用覆盖索引,再通过该覆盖索引查询到的结果到外层查询匹配锁需要的所有列值。

这样说有些抽象,我们来看看下面的例子。

用延迟关联优化分页(LIMIT)

当使用 LIMIT 碰上较大偏移量时,例如 LIMIT 10000, 20 这样的查询,MySQL 需要查询 10020 条记录然后再返回最后的 20 条。前面的 10000 最终都会被抛弃,这样的代价非常高。

mysql> EXPLAIN SELECT * FROM orders LIMIT 10000, 20;
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | orders | NULL | ALL | NULL | NULL | NULL | NULL | 2 | 100.00 | NULL |
+----+-------------+--------+------------+------+---------------+------+---------+------+------+----------+-------+

优化此类分页查询的一个最简单的办法就是尽可能使用索引覆盖扫描,而不是查询所有列。然后根据需要再做一次关联,返回所需要的列。

mysql> EXPLAIN SELECT * FROM orders AS o1 JOIN (SELECT id FROM orders LIMIT 10000, 20) AS o2 ON o1.id = o2.id;
+----+-------------+------------+------------+-------+---------------+-------------+---------+------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+---------------+-------------+---------+------------+------+----------+-------------+
| 1 | PRIMARY | o1 | NULL | ALL | PRIMARY | NULL | NULL | NULL | 2 | 100.00 | NULL |
| 1 | PRIMARY | <derived2> | NULL | ref | <auto_key0> | <auto_key0> | 4 | test.o1.id | 2 | 100.00 | Using index |
| 2 | DERIVED | orders | NULL | index | NULL | PRIMARY | 4 | NULL | 2 | 100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+-------------+---------+------------+------+----------+-------------+

这样一来,MySQL 在 SQL 语句的「内层」进行扫描时使用了覆盖索引,「外层」再通过索引树找到相关的数据行,直接减少了扫描的数据量。

参考资料

【译】用依赖注入解耦你的代码

2020年3月29日 20:37

用依赖注入解耦你的代码

无需第三方框架

[Icons8 团队](https://unsplash.com/@icons8?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) 摄于 [Unsplash](https://unsplash.com/s/photos/ingredients?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

没有多少组件是能够独立存在而不依赖于其它组件的。除了创建紧密耦合的组件,我们还可以利用依赖注入(DI)来改善 关注点的分离

这篇文章将会脱离第三方框架向你介绍依赖注入的核心概念。所有的示例代码都将使用 Java,但所介绍的一般原则也适用于其它任何语言。


示例:数据处理器

为了让如何使用依赖注入更加形象化,我们将从一个简单的类型开始:

public class DataProcessor {

private final DbManager manager = new SqliteDbManager("db.sqlite");
private final Calculator calculator = new HighPrecisionCalculator(5);

public void processData() {
this.manager.processData();
}

public BigDecimal calc(BigDecimal input) {
return this.calculator.expensiveCalculation(input);
}
}

DataProcessor 有两个依赖项:DbManagerCalculator。直接在我们的类型中创建它们有几个明显的缺点:

  • 调用构造函数时可能发生崩溃
  • 构造函数签名可能会改变
  • 紧密绑定到显式实现类型

是时候改进它了!


依赖注入

《敏捷开发的艺术》 的作者 James Shore 很好地指出

「依赖注入听起来复杂,实际上它的概念却十分简单。」

依赖注入的概念实际上非常简单:为组件提供完成其工作所需的一切。

通常,这意味着通过从外部提供组件的依赖关系来解耦组件,而非直接在组件内创建依赖,让组件间过度耦合。

我们可以通过多种方式为实例提供必要的依赖关系:

  • 构造函数注入
  • 属性注入
  • 方法注入

构造函数注入

构造函数注入,或称基于初始化器的依赖注入,意味着在实例初始化期间提供所有必需的依赖项,将其作为构造函数的参数:

public class DataProcessor {

private final DbManager manager;
private final Calculator calculator;

public DataProcessor(DbManager manager, Calculator calculator) {
this.manager = manager;
this.calculator = calculator;
}

// ...
}

由于这一简单的改变,我们可以弥补大多数最开始的缺点:

  • 易于替换:DbManagerCalculator 不再被具体的实现所束缚,现在可以模拟单元测试了。
  • 已经初始化并且「准备就绪」:我们不必担心依赖项所需要的任何子依赖项(例如,数据库文件名、有效数字(译者注)等),也不必担心它们可在初始化期间发生崩溃的可能性。
  • 强制要求:调用方确切地知道创建 DataProcessor 的所需内容。
  • 不变性:依赖关系始终如初。

尽管构造函数注入是许多依赖注入框架的首选方法,但它也有明显的缺点。其中最大的缺点是:必须在初始化时提供所有依赖项。

有时,我们无法自己初始化一个组件,或者在某个时刻我们无法提供组件的所有依赖关系。或者我们需要使用另外一个构造函数。一旦设置了依赖项,我们就无法再改变它们了。

但是我们可以使用其它注入类型来缓解这些问题。

属性注入

有时,我们无法访问类型实际的初始化方法,只能访问一个已经初始化的实例。或者在初始化时,所需要的依赖关系并不像之后那样明确。

在这些情况下,我们可以使用属性注入而不是依赖于构造函数:

public class DataProcessor {

public DbManager manager = null;
public Calculator calculator = null;

// ...

public void processData() {
// WARNING: Possible NPE
this.manager.processData();
}

public BigDecimal calc(BigDecimal input) {
// WARNING: Possible NPE
return this.calculator.expensiveCalculation(input);
}
}

我们不再需要构造函数了,在初始化后我们可以随时提供依赖项。但这种注入方式也有缺点:易变性

在初始化后,我们不再保证 DataProcessor 是「随时可用」的。能够随意更改依赖关系可能会给我们带来更大的灵活性,但同时也会带来运行时检查过多的缺点。

现在,我们必须在访问依赖项时处理出现 NullPointerException 的可能性。

方法注入

即使我们将依赖项与构造函数注入与/或属性注入分离,我们也仍然只有一个选择。如果在某些情况下我们需要另一个 Calculator 该怎么办呢?

我们不想为第二个 Calculator 类添加额外的属性或构造函数参数,因为将来可能会出现第三个这样的类。而且在每次调用 calc(...) 前更改属性也不可行,并且很可能因为使用错误的属性而导致 bug。

更好的方法是参数化调用方法本身及其依赖项:

public class DataProcessor {

// ...

public BigDecimal calc(Calculator calculator, BigDecimal input) {
return calculator.expensiveCalculation(input);
}
}

现在,calc(...) 的调用者负责提供一个合适的 Calculator 实例,并且 DataProcessor 类与之完全分离。

通过混合使用不同的注入类型来提供一个默认的 Calculator,这样可以获得更大的灵活性:

public class DataProcessor {

// ...

private final Calculator defaultCalculator;

public DataProcessor(Calculator calculator) {
this.defaultCalculator = calculator;
}

// ...

public BigDecimal calc(Calculator calculator, BigDecimal input) {
return Optional.ofNullable(calculator)
.orElse(this.calculator)
.expensiveCalculation(input);
}
}

调用者可以提供另一种类型的 Calculator,但这不是必须的。我们仍然有一个解耦的、随时可用的 DataProcessor,它能够适应特定的场景。

选择哪种注入方式?

每种依赖注入类型都有自己的优点,并没有一种「正确的方法」。具体的选择完全取决于你的实际需求和情况。

构造函数注入

构造函数注入是我的最爱,它也常受依赖注入框架的青睐。

它清楚地告诉我们创建特定组件所需的所有依赖关系,并且这些依赖不是可选的,这些依赖关系在整个组件中应该都是必需的。

属性注入

属性注入更适合可选参数,例如监听或委托。又或是我们无法在初始化时提供依赖关系。

其它编程语言,例如 Swift,大量使用了带属性的 委托模式。因此,使用属性注入将使其它语言的开发人员更熟悉我们的代码。

方法注入

如果在每次调用时依赖项可能不同,那么使用方法注入最好不过了。方法注入进一步解耦组件,它使方法本身持有依赖项,而非整个组件。

请记住,这不是非此即彼。我们可以根据需要自由组合各种注入类型。

控制反转容器

这些简单的依赖注入实现可以覆盖很多用例。依赖注入是很好的解耦工具,但事实上我们仍然需要在某些时候创建依赖项。

但随着应用程序和代码库的增长,我们可能还需要一个更完整的解决方案来简化依赖注入的创建和组装过程。

控制反转(IoC)是 控制流 的抽象原理。依赖注入是控制反转的具体实现之一。

控制反转容器是一种特殊类型的对象,它知道如何实例化和配置其它对象,它也知道如何帮助你执行依赖注入。

有些容器可以通过反射来检测关系,而另一些必须手动配置。有些容器基于运行时,而有些则在编译时生成所需要的所有代码。

比较所有容器的不同之处超出了本文的讨论范围,但是让我通过一个小示例来更好地理解这个概念。

示例: Dagger 2

Dagger 是一个轻量级、编译时进行依赖注入的框架。我们需要创建一个 Module,它就知道如何构建我们的依赖项,稍后我们只要添加 @Inject 注释就可以注入这个 Module

@Module
public class InjectionModule {

@Provides
@Singleton
static DbManager provideManager() {
return manager;
}

@Provides
@Singleton
static Calculator provideCalculator() {
return new HighPrecisionCalculator(5);
}
}

@Singleton 确保只能创建一个依赖项的实例。

要注入依赖项,我们只需要将 @Inject 添加到构造函数、字段或方法中。

public class DataProcessor {

@Inject
DbManager manager;

@Inject
Calculator calculator;

// ...
}

这些仅仅是一些基础知识,乍一看不可能会给人留下深刻的印象。但是控制反转容器和框架不仅解耦了组件,也让创建依赖关系的灵活性得以最大化。

由于提供了高级特性,创建过程的可配置性变得更强,并且支持了使用依赖项的新方法。

高级特性

这些特性在不同类型的控制反转容器和底层语言之间差异很大,比如:

  • 代理模式 和延迟加载。
  • 生命周期(例如:单例模式与每个线程一个实例)。
  • 自动绑定。
  • 单一类型的多种实现。
  • 循环依赖。

这些特性是控制反转容器真正的能力。你可能会认为诸如「循环依赖」这样的特性并非好的主意,确实如此。

但是,如果由于遗留代码或是过去不可更改的错误设计而需要这种奇怪的代码构造,那么我们现在有能力可以这样做。

总结

我们应该根据抽象(例如接口)而不是具体的实现来设计代码,这样可以帮助我们减少代码耦合。

接口必须提供我们代码所需要的唯一信息,我们不能对实际实现情况做任何假设。

「程序应当依赖抽象,而非具体的实现」
—— Robert C. Martin (2000), 《设计原则与设计模式》

依赖注入是通过解耦组件来实现这一点的好办法。它使我们能够编写更简洁明了、更易于维护和重构的代码。

选择三种依赖注入类型中的哪种很大程度上取决于环境和需求,但是我们也可以混合使用三种类型使收益最大化。

控制反转容器有时几乎以一种神奇的方式通过简化组件创建过程来提供另一种便利的布局。

我们应该处处使用它吗?当然不是。

就像其它模式和概念一样,我们应该在适当的时候应用它们,而不是能用则用。

永远不要把自己局限在一种做事的方式上。也许 工厂模式 甚至是广为厌恶的 单例模式 是能够满足你需求的更好的解决方案。


资料


控制反转容器

Java

Kotlin

Swift

C

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

《Head First 设计模式》学习笔记 | 策略模式

2020年3月9日 22:15

前言

我最近在看大名鼎鼎的《Head First 设计模式》。这本「OO 圣经」用 Java 实现各类设计模式,对于我 —— 一个非 Java 爱好者而言,读起来并不过瘾。

有人读完这本书可能会误解设计模式就是设计 Interface,而事实并非如此。在知乎的一个问题《Python 里没有接口,如何写设计模式?》中,vczh 轮子哥是这样回答的:

设计模式搞了那么多东西就是在告诉你如何在各种情况下解耦你的代码,让你的代码在运行时可以互相组合。这就跟兵法一样。难道有了飞机大炮,兵法就没有用了吗?

我觉得这个比喻很好,不同的语言就像不同的兵器,各有各的特点与使用方式,而设计模式就是那套「兵法」,无论你使用何种兵器,不过是「纵横不出方圆,万变不离其宗」。而只看书中一种「兵器」未免太少,不如我们多试几样?

本篇就来看一看第一章「兵法」 —— 策略模式(Strategy Pattern)。

定义

书中对策略模式的定义如下:

策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。

下面以书中的「模拟鸭子应用」为例。

继承的弊端

你要设计一个鸭子游戏,游戏里有各种各样的鸭子,它们会游泳(swim()),还会呱呱叫(quack()),每种鸭子拥有不同的外观(display())。

一开始,你可能会设计一个鸭子的超类 Duck,然后让所有不同种类的鸭子继承它:

设计一个鸭子超类(Superclass)

如果此时我们想让鸭子飞起来,就要在超类中增加一个 fly() 方法:

让鸭子飞

此时,鸭子家族来了一只擅于代码调试工作的小黄鸭。

此时,一切都乱套了,这位代码调试工作者会发出「吱吱」的叫声,但却不会飞,然而它却从鸭子超类继承了 quack()fly() 方法。为了让它尊重客观事实,我们需要在小黄鸭类中覆盖超类的 quack()fly() 方法,让它变得不会叫也不会飞。

在小黄鸭中覆盖原有的方法

虽然我们用「覆盖方法」的手段解决了小黄鸭的问题,但未来我们可能还会制造更多奇奇怪怪的鸭子。例如周黑鸭或北京烤鸭,它们显然既不会叫,也不会游泳,还不会飞,这时我们又要为它们重写所有的行为吗?利用继承的方式来为不同种类的鸭子提供行为显然不够灵活。

抽离可变行为

不同的鸭子具有不同的行为,鸭子的行为应当是灵活可变的

设计原则一:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。

因此,利用上述原则,我们把「鸭子的行为」从鸭子类(Duck)中抽离出来。

取出容易变化的行为

实现被抽离的行为

设计原则二:针对接口编程,而不是针对实现编程。

我们将这些被抽离出的行为归类:

  • 所有具体的飞行行为属于飞行策略
  • 所有具体的叫声行为属于叫声策略
  • 所有具体的游泳行为属于游泳策略
  • ……

我们可以利用接口或抽象类代表这些策略,然后让特定的具体行为来实现这些策略中的方法

例如,我们的飞行策略名为 FlyBehavior,我们将它设计为一个抽象类(当然也可以是接口)。然后,我们有两种具体的飞行方式 FlyWithWings(会飞)和 FlyNoWay(不会飞),它们需要实现飞行策略中的 fly() 方法:

整合

此时,我们已经将可变的行为从鸭子超类(Duck)中抽离,并把它们用具体的「行为类」进行表示。我们希望:如果鸭子要执行某个行为,它不需要自己处理,而是将这一行为委托给具体的「行为类」

因此,我们可以在鸭子超类(Duck)中加入「行为类」的实例变量,从而通过这些实例变量来调用具体的行为方法。

Class Duckfly() 方法中,我们可以使用实例 flyBehavior 调用具体的行为方法,从而达成「委托」的目的:

public function fly() 
{
$this->flyBehavior->fly();
}

具体实现

下面来看看不同语言的具体实现:

PHP

PHP 有抽象类也有接口,语法和 Java 比较接近。实现方法中规中矩,和书中的并无二致。只不过这里我把行为接口改成了抽象类。类图如下:

UML 类图关系

具体实现:

<?php
// 飞行行为类
abstract class FlyBehavior
{
abstract public function fly();
}

// 「飞」的具体行为
class FlyWithWings extends FlyBehavior
{
public function fly()
{
echo "会飞\n";
}
}

class FlyNoWay extends FlyBehavior
{
public function fly()
{
echo "不会飞\n";
}
}

// 叫声行为类
abstract class QuackBehavior
{
abstract public function quack();
}

// 「叫」的具体行为
class Quack extends QuackBehavior
{
public function quack()
{
echo "呱呱\n";
}
}

class Squeak extends QuackBehavior
{
public function quack()
{
echo "吱吱\n";
}
}

class MuteQuack extends QuackBehavior
{
public function quack()
{
echo "不会叫\n";
}
}

// 鸭子类
abstract class Duck
{
protected $flyStrategy;
protected $quackStrategy;

public function fly()
{
$this->flyStrategy->fly();
}

public function quack()
{
$this->quackStrategy->quack();
}
}

// 有只小黄鸭
class YellowDuck extends Duck
{
public function __construct($flyStrategy, $quackStrategy)
{
$this->flyStrategy = $flyStrategy;
$this->quackStrategy = $quackStrategy;
}
}

$yellowDuck = new YellowDuck(new FlyNoWay(), new Squeak());
$yellowDuck->fly();
$yellowDuck->quack();

/* Output:
不会飞
吱吱
*/
?>

Python

Python 就没有所谓的抽象类和接口了,当然你也可以通过 abc 模块来实现这些功能。

比较简单的做法是:将具体行为直接定义为函数,在初始化鸭子时通过构造函数传入行为函数,赋值给对应的变量。当执行具体行为时,将直接调用被赋值的变量,这时具体的行为动作就被委托给了传入的行为函数,达到了「委托」的效果。

class Duck:
def __init__(self, fly_strategy, quack_strategy):
self.fly_strategy = fly_strategy
self.quack_strategy = quack_strategy

def fly(self):
self.fly_strategy()

def quack(self):
self.quack_strategy()

def fly_with_wings():
print("会飞")

def fly_no_way():
print("不会飞")

def quack():
print("呱呱")

def squeak():
print("吱吱")

def mute_quack():
print("不会叫")

# 一只会飞也不会叫的小黄鸭
yellow_duck = Duck(fly_no_way, mute_quack)
yellow_duck.fly()
yellow_duck.quack()

# Output:
# 不会飞
# 不会叫

Golang

在 Go 语言中没有 extends 关键字,但可以通过在结构体中内嵌匿名类型的方式实现继承关系。此处,将 FlyBehavior 飞行行为和 QuackBehavior 行为声明为接口。

package main

import "fmt"

// FlyBehavior 飞行行为接口
type FlyBehavior interface {
fly()
}

// QuackBehavior 呱呱叫行为接口
type QuackBehavior interface {
quack()
}

// FlyWithWings 会飞的类
type FlyWithWings struct {
}

func (flyWithWings FlyWithWings) fly() {
fmt.Println("会飞")
}

// FlyWithWings 不会飞的类
type FlyNoWay struct{}

func (flyNoWay FlyNoWay) fly() {
fmt.Println("不会飞")
}

// Quack 呱呱叫
type Quack struct{}

func (quack Quack) quack() {
fmt.Println("呱呱")
}

// Squeak 吱吱叫
type Squeak struct{}

func (squeak Squeak) quack() {
fmt.Println("吱吱")
}

// MuteQuack 不会叫
type MuteQuack struct{}

func (muteQuack MuteQuack) quack() {
fmt.Println("不会叫")
}

// Duck 鸭子类
type Duck struct {
FlyBehavior FlyBehavior
QuackBehavior QuackBehavior
}

func (d *Duck) fly() {
d.FlyBehavior.fly() // 委托给飞行行为
}

func (d *Duck) quack() {
d.QuackBehavior.quack() // 委托给呱呱叫行为
}

func main() {
yellowDuck := Duck{FlyNoWay{}, Squeak{}}
yellowDuck.fly()
yellowDuck.quack()
}

/* Output:
不会飞
吱吱
*/

总结

三种设计原则:

  1. 封装变化
  2. 多用组合,少用继承
  3. 针对接口编程,不针对实现编程

注意此处的「针对接口编程」,书中也有强调:

「针对接口编程」真正的意思是「针对超类型(supertype)编程」。这里所谓的「接口」有多个含义,接口是一个「概念」,也是一种 Java 的 interface 构造。你可以在不涉及 Java interface 的情况下「针对接口编程」,关键就在多态。利用多态,程序可以针对超类型编程,执行时会根据实际状况执行到真正的行为。

因此,你不用拘泥于 interface,你所用的语言就算没有 interface 也能实现设计模式。

【译】SQL 将死于 No-SQL 之手?

2020年2月25日 20:37

SQL 将死于 No-SQL 之手?

SQL 永生不灭的两个原因

上周,我的一位朋友向我转发了一封来自一位成功创业者的电子邮件,邮件宣称「SQL 已死」。

这位创业者宣称,像 MongoDB、Redis 这样广受欢迎的 No-SQL 数据库会慢慢取代基于 SQL 的数据库,因此,作为数据科学家还需学习 SQL 是一个「历史遗留问题」。

我完全被他的电子邮件震惊了:他怎么得出如此离谱的结论?但是这也使我感到好奇……别人是否也有可能被类似地误导了?这位企业家已经发展了大批追随者,且直言不讳 —— 那么新晋数据科学家是否已经收到了避免学习 SQL 的建议?

因此我觉得我应当公开分享我对该创业者的回应,以防他人认为 SQL 即将走向灭绝。

在数据科学的职业生涯中,你绝对应当学习 SQL。No-SQL 的存在绝对不会影响学习 SQL 的价值。

基本上有两个原因可以保证 SQL 在未来几十年仍然适用。

原因 #1:No-SQL 数据库无法取代数据分析型数据库,例如 Presto、Redshift 或 BigQuery

无论你的应用是使用以 SQL 为后端的数据库,例如 MySQL,或是以 No-SQL 为后端的数据库,例如 MongoDB,这些后端中的数据最终都将被加载到一个专用的数据分析数据库中,例如 Redshift、Snowflake、BigQuery 或 Presto。

分析型数据库平台架构示例:SQL 与 NoSQL

为什么公司要将他们的数据转移到像 Redshift 这样特定的列式存储中?因为和 NoSQL 与 MySQL 这样的行式存储数据库相比,列式存储能快地运行分析查询。事实上,我敢打赌,列式存储和 NoSQL 一样会越来越受欢迎。

因此,无论是 NoSQL 还是其他的应用程序数据库都与数据科学家无关,因为数据科学家不会对应用程序数据库进行操作(尽管有一些例外,这些例外我将在之后讨论)。

原因 #2:No-SQL 数据库的好处不在于他们不支持 SQL 语言

事实证明,如果 No-SQL 数据存储支持基于 SQL 的查询引擎是有意义的,那么它们就可以实现该引擎。类似地,SQL 数据库也可以支持 NoSQL 查询语言,但是它们选择不支持。

为什么列式存储有意选择提供 SQL 接口呢?

他们之所以作出这样的选择是因为 SQL 是一种表达数据操作指令的强大语言。

让我们来考虑一个简单的查询示例,该查询用于计算来自 NoSQL 数据库 MongoDB 中某集合的文档数量。

注意:MongoDB 中的文档类似于行,集合类似于表。

db.sales.aggregate( [
{
$group: {
_id: null,
count: { $sum: 1 }
}
}
] )

将其与等价的 SQL 语句进行比较。

select count(1) from sales

显然,对于想要提取数据的人来说,SQL 语言是更好的选择。(NoSQL 数据库支持另一种语言,因为对于与数据库连接的应用程序库来说,正确构造 SQL 相对比较困难)。


在前面我提到过,应用程序数据库技术与科学家无关的规则是有例外的。例如,在我的第一家公司,我们实际上没有任何像 Redshift 这样的分析型数据库,所以我不得不直接查询该应用程序的数据库。(更准确地说,我是在查询应用程序数据库的只读副本)。

公司的应用也使用了 Redis 这样的 No-SQL 数据库,这样至少有一次,我需要从 Redis 中提取数据,所以我必须学习一些 Redis 的 NoSQL API 的某些组件。

因此,如果在主应用程序环境中完全使用 NoSQL 数据库,那么你学到的任何 SQL 知识都与之无关了。但是这样的环境非常少见,随着公司的发展,他们几乎都会把一个基于 SQL 的列式存储数据库投入使用。

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

macOS 下 MongoDB 连接报错排查过程及处理

2020年1月31日 20:37

安装

在 macOS 下使用 Homebrew 安装 MongoDB。但在 Homebrew 的核心库中,我们无法找到 MongoDB,于是需要先运行 brew tap,帮助我们扩大可安装软件的选择范围。

The tap command allows Homebrew to tap into another repository of formulae. Once you’ve done this you’ve expanded your options of installable software.

$ brew tap mongodb/brew

之后,运行如下命令安装 MongoDB:

$ brew install mongodb-community@4.2

启动

使用 brew 命令运行 MongoDB:

$ brew services start mongodb-community@4.2

建立连接

启动成功后,使用如下命令与 MongoDB 建立连接:

$ mongo

运行命令后发现连接失败,错误如下:

MongoDB shell version v4.2.2
connecting to: mongodb://127.0.0.1:27017/?compressors=disabled&gssapiServiceName=mongodb
2020-01-31T19:24:23.752+0800 E QUERY [js] Error: couldn't connect to server 127.0.0.1:27017, connection attempt failed: SocketException: Error connecting to 127.0.0.1:27017 :: caused by :: Connection refused :
connect@src/mongo/shell/mongo.js:341:17
@(connect):2:6
2020-01-31T19:24:23.768+0800 F - [main] exception: connect failed
2020-01-31T19:24:23.768+0800 E - [main] exiting with code 1

报错排查

看报错信息总之就是连接失败了,这么一大段除了 Error connecting 也没啥有用的信息了。为了获取具体的错误信息,我们可以查看 MongoDB 的日志文件,日志在 /usr/local/var/log/mongodb 目录中,日志信息如下:

$ cat /usr/local/var/log/mongodb/mongo.log
2020-01-31T20:20:11.840+0800 I CONTROL [main] Automatically disabling TLS 1.0, to force-enable TLS 1.0 specify --sslDisabledProtocols 'none'
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] MongoDB starting : pid=44665 port=27017 dbpath=/usr/local/var/mongodb 64-bit host=Jalan-JiangdeMacBook-Pro.local
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] db version v4.2.2
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] git version: a0bbbff6ada159e19298d37946ac8dc4b497eadf
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] allocator: system
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] modules: none
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] build environment:
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] distarch: x86_64
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] target_arch: x86_64
2020-01-31T20:20:11.875+0800 I CONTROL [initandlisten] options: { config: "/usr/local/etc/mongod.conf", net: { bindIp: "127.0.0.1" }, storage: { dbPath: "/usr/local/var/mongodb" }, systemLog: { destination: "file", logAppend: true, path: "/usr/local/var/log/mongodb/mongo.log" } }
2020-01-31T20:20:11.876+0800 I STORAGE [initandlisten]
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten] ** WARNING: Support for MMAPV1 storage engine has been deprecated and will be
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten] ** removed in version 4.2. Please plan to migrate to the wiredTiger
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten] ** storage engine.
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten] ** See http://dochub.mongodb.org/core/deprecated-mmapv1
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten]
2020-01-31T20:20:11.877+0800 I STORAGE [initandlisten] Detected data files in /usr/local/var/mongodb created by the 'mmapv1' storage engine, so setting the active storage engine to 'mmapv1'.
2020-01-31T20:20:11.878+0800 I STORAGE [initandlisten] exception in initAndListen: Location18656: Cannot start server with an unknown storage engine: mmapv1, terminating
2020-01-31T20:20:11.878+0800 I NETWORK [initandlisten] shutdown: going to close listening sockets...
2020-01-31T20:20:11.878+0800 I NETWORK [initandlisten] removing socket file: /tmp/mongodb-27017.sock
2020-01-31T20:20:11.878+0800 I - [initandlisten] Stopping further Flow Control ticket acquisitions.
2020-01-31T20:20:11.879+0800 I CONTROL [initandlisten] now exiting
2020-01-31T20:20:11.879+0800 I CONTROL [initandlisten] shutting down with code:100

列出关键信息:WARNING: Support for MMAPV1 storage engine has been deprecated and will be removed in version 4.2. Please plan to migrate to the wiredTiger

翻译一下就是:4.2 版本已不再支持 MMAPV1 引擎,请迁移至 wiredTiger 引擎

Compatibility Changes in MongoDB 4.0 中我们可以看到,MongoDB 从 4.0 版本开始就不再支持 MMAPV1 引擎了(Deprecate MMAPv1),因此,我们需要把引擎切换成 wiredTiger。

错误处理

首先,启动要切换引擎的 MongoDB:

$ brew services start mongodb-community@4.2

如果这个数据库中还有你所需要的数据,需要使用 mongodump 对数据库进行备份。

然后,创建一个新的目录,用于 wiredTiger 引擎的数据存储。我创建的目录为:/usr/local/varmongodb-w

最后,使用 wiredTiger 引擎启动 MongoDB:

$ mongod --storageEngine wiredTiger --dbpath /usr/local/varmongodb-w

其中,--dbpath 后填写你刚才创建的新目录。

如果你刚才备份过数据,还需要使用 mongorestore 将数据重新导入。

大功告成啦~

参考资料

2019,冒险者的孤勇

2020年1月2日 20:37

半个月前,我参加掘金年度征文,写了一篇《2019 年「我与技术那些事儿」| 掘金年度征文》。这篇文章基本是我 2019 年在技术上的总结了。

但技术永远都只是生活的一部分。我不想在公众平台上写太多私人的事,所以还是按照以往的惯例,在博客上发一篇真正的、全面的 2019 年度总结。

2019 年 12 月的前半月我忙着发烧,后半月忙着制作 GitHub 年度报告,所以这篇「真正的总结」也只好放到 2020 年来写。

技术方面我就不再赘述了,如果你对我的技术总结感兴趣,欢迎戳上面的掘金链接。

2019 年,在深圳

冒险

我给这篇年度总结起名《2019,冒险者的孤勇》,因为 2019 年对我而言就是冒险、承担、执拗。

年初冒险的决定改变了我的工作和生活,但我并没有因此而过得多好,反而时常为此流泪叹息。我一直很焦虑,时常自责。直到有一次,我的心理咨询师对我说:「你年初的决定很好地保护了你自己」。从此往后,我开始用这句话不段地自我鼓励:虽然我做得还不够好,但这个决定我从来没有做错过。就算没有人帮助我,我也可以保护好自己。

「往者不可谏,来者犹可追」,这段冒险一旦开始就没有办法回头了。我在这条路上「又勇又怂」:勇在开疆扩土,怂在本性难移。「怂」的部分总是很难改变,但愿 2020 勇字当头,能活得更自在、更热忱些。

2019 年 10 月,摄于福州南站

尝鲜

2019 年是「尝鲜」的一年,我经历了很多「人生第一次」:

斯巴达勇士竞速赛观光团

行路

2019 年我去了香港、潮汕、上海、广州、顺德和南浔。原本在下半年有出国的计划,但很遗憾,最终没有达成。

2019 年 11 月,摄于南浔古镇

2020 年依旧想去一些从未到达的城市,看看别人的生活。

反思

我在 2019 年悟出的一个道理是:凡事急不得

我经常遇见想做的事就开始挖坑,却往往三分钟热度,最终都没能把坑填上,导致事后自责懊恼,自信心受挫。我总是着急着想做成一些事,却忘记了很多事是着急不得的。专心做一件事时,其实就不能专心做另一件事,要用沉浸的心态把一件事做好,才能最终尝到「延迟满足」的滋味。

很感谢 2019 年最后一天我完成了 GitHub 年度报告,这不是什么大项目,但在完成的过程中我经历了热情、懈怠、疲倦、放弃、重拾、收获几个过程,可谓是跌宕起伏了。这个项目让我尝到了甜头,让我在回顾 2019 时对自己有了更多的自信,也让我学会了如何调整心态,让自己更积极地面对生活中的「困倦期」。

2019 年的最后一个月我病了很久,一次病毒性感冒让我在两周中反复发烧,三进医院,直到今天我还在咳嗽,这个病依旧没有好全。2020 年我会把身体管理放在首要位置,未来还很长,不能在年轻时就把革命的本钱提前消耗殆尽。

感恩

2019 年来到深圳后结识了很多新朋友,其中不乏许多大佬和有趣的人。

非常感谢遇到了现在的神仙舍友,在生活上对我照顾颇多。

舍友下厨为我庆生

年初离开厦门后,我在年底顺利地和佳爷少棉又见上面。除此之外,今年也顺利地和王总、应总实现了「一年一聚」之约。

感谢所有的朋友 2019 年听我吐了一年的苦水,感恩一直有你们鼓励我、督促我、陪着我。祝所有朋友 2020 年能一切顺利,未来都过上自己想要的生活。

劝君更尽一杯酒

最后的最后,也感谢自己从未放弃过自己。

拔剑吧,2020

跨年是一件颇具仪式感的事。

2019 年的最后一天,我和少棉在深圳一起度过了告别 20 世纪 10 年代的跨年夜。这是我们在深圳的第一次跨年,在深圳湾吹着海风,听着一群陌生人喊着倒计时的数字。深圳湾没有烟花表演,黑漆漆一片,在人才公园的入口亦看不见繁华的香港。但在零点到来的时刻,听着陌生人大喊着「新年快乐」,迎接 2020 的一霎那有种莫名的幸福。

少棉曾经对我说「我们终于活下来了」,我把这句话写到了跨年的朋友圈文案里,还写了一句「我们有一万个去冒险的梦想,2019 年终于实现了一个」。

2019 年真的太难了,但我们也曾在 2019 年感慨「2018 年太难」,似乎进入成年人的世界后就再没有舒坦的日子。

但那又如何?2019 死里逃生,还怕些什么呢?

拔剑吧 2020,我怕你个球

聊聊 Go 语言中的面向对象编程

2019年12月2日 20:40

我们知道,在 Go 语言中没有类(Class)的概念,但这并不意味着 Go 语言不支持面向对象编程,毕竟面向对象只是一种编程思想。

让我们回忆一下面向对象的三大基本特征:

  1. 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
  2. 继承:使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
  3. 多态:不同对象中同种行为的不同实现方式

我们一起来看看 Go 语言是如何在没有类(Class)的情况下实现这三大特征的。

封装

「类」

在 Go 语言中可以使用结构体Structs)对属性进行封装,结构体就像是类的一种简化形式。

例如,我们要定义一个矩形,每个矩形都有长和宽,我们可以这样进行封装:

type Rectangle struct {
Length int
Width int
}

方法

既然有了「类」,你可能会问了,那「类」的方法在哪呢?

Go 语言中也有方法Methods):Go 方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量。因此方法是一种特殊类型的函数。

定义方法的格式如下:

func (recv receiver_type) methodName(parameter_list) (return_value_list) { ... }

上文中我们已经定义了一个矩形 Rectangle,现在我们要定义一个方法 Area() 来计算它的面积:

package main

import (
"fmt"
)

// 矩形结构体
type Rectangle struct {
Length int
Width int
}

// 计算矩形面积
func (r *Rectangle) Area() int {
return r.Length * r.Width
}

func main() {
r := Rectangle{4, 2}
// 调用 Area() 方法,计算面积
fmt.Println(r.Area())
}

上面的代码片段输出结果为 8。

访问权限

我们常会说一个类的属性是公共的还是私有的,在其他编程语言中,我们常用 publicprivate 关键字来表达这样一种访问权限。

在 Go 语言中没有 publicprivateprotected 这样的访问控制修饰符,它是通过字母大小写来控制可见性的。

如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头,这表示它们能被其它包访问或调用(相当于 public);非大写开头就只能在包内使用(相当于 private)。

访问未导出字段

当遇到只能在包内使用的未导出字段时,我们又该如何访问呢?

和其他面向对象语言一样,Go 语言也有实现 gettersetter 的方式:

  • 对于 setter 方法使用 Set 前缀
  • 对于 getter 方法只使用成员名

例如我们现在有一个处于 person 包中的 Person 结构体:

package person

type Person struct {
firstName string
lastName string
}

我们可以看到,它的两个成员变量都是非大写字母开头,只能在包内使用,现在我们为其中的 firstName 来定义 settergetter

// 获取 firstName
func (p *Person) FirstName() string {
return p.firstName
}

// 设置 firstName
func (p *Person) SetFirstName(newName string) {
p.firstName = newName
}

这样一来,我们就可以在 main 包里设置和获取 firstName 的值了:

package main

import (
"fmt"

"./person"
)

func main() {
p := new(person.Person)
p.SetFirstName("firstName")
fmt.Println(p.FirstName())
}

/* Output:
firstName
*/

继承

在 Go 语言中没有 extends 关键字,它使用在结构体中内嵌匿名类型的方法来实现继承。

匿名类型:即这些类型没有显式的名字。

我们定义一个 Engine 接口类型,一个 Car 结构体,让 Car 结构体包含一个 Engine 类型的匿名字段:

type Engine interface {
Start()
Stop()
}

type Car struct {
Engine // 包含 Engine 类型的匿名字段
}

此时,匿名字段 Engine 上的方法「晋升」成为了外层类型 Car 的方法。我们可以构建出如下代码:

func (c *Car) GoToWorkIn() {
// get in car
c.Start()
// drive to work
c.Stop()
// get out of car
}

多态

在面向对象中,多态的特征为:不同对象中同种行为的不同实现方式。在 Go 语言中可以使用接口实现这一特征。

我们先定义一个正方形 Square 和一个长方形 Rectangle

// 正方形
type Square struct {
side float32
}

// 长方形
type Rectangle struct {
length, width float32
}

然后,我们希望可以计算出这两个几何图形的面积。但由于他们的面积计算方式不同,我们需要定义两个不同的 Area() 方法。

于是,我们可以定义一个包含 Area() 方法的接口 Shaper,让 SquareRectangle 都实现这个接口里的 Area()

// 接口 Shaper
type Shaper interface {
Area() float32
}

// 计算正方形的面积
func (sq *Square) Area() float32 {
return sq.side * sq.side
}

// 计算长方形的面积
func (r *Rectangle) Area() float32 {
return r.length * r.width
}

我们可以在 main() 函数中这样调用 Area()

func main() {
r := &Rectangle{10, 2}
q := &Square{10}

// 创建一个 Shaper 类型的数组
shapes := []Shaper{r, q}
// 迭代数组上的每一个元素并调用 Area() 方法
for n, _ := range shapes {
fmt.Println("图形数据: ", shapes[n])
fmt.Println("它的面积是: ", shapes[n].Area())
}
}

/*Output:
图形数据: &{10 2}
它的面积是: 20
图形数据: &{10}
它的面积是: 100
*/

由以上代码输出结果可知:不同对象调用 Area() 方法产生了不同的结果,展现了多态的特征。

总结

  • 面向对象的三大特征是:封装、继承和多态
  • Go 语言使用结构体对属性进行封装,结构体就像是类的一种简化形式
  • 在 Go 语言中,方法是作用在接收者(receiver)上的一个函数,接收者是某种类型的变量
  • 名称首字母的大小写决定了该变量/常量/类型/接口/结构/函数……能否被外部包导入
  • 无法被导入的字段可以使用 gettersetter 的方式来访问
  • Go 语言使用在结构体中内嵌匿名类型的方法来实现继承
  • 使用接口可以实现多态

参考资料


聊聊 Go 语言中的字符表示与字符串遍历

2019年11月16日 23:57

和其他语言不同,在 Go 语言中没有字符类型,字符只是整数的特殊用例

为什么说字符只是整数的特殊用例呢?因为在 Go 中,用于表示字符的 byterune 类型都是整型的别名。在 Go 的源码中我们可以看到:

// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
  • byteuint8 的别名,长度为 1 个字节,用于表示 ASCII 字符
  • runeint32 的别名,长度为 4 个字节,用于表示以 UTF-8 编码的 Unicode 码点

Tips:Unicode 从 0 开始,为每个符号指定一个编号,这叫做「码点」(code point)。

字符的表示

那么,如何在 Go 语言中表示字符呢?

在 Go 语言中使用单引号包围来表示字符,例如 'j'

byte

如果要表示 byte 类型的字符,可以使用 byte 关键字来指明字符变量的类型:

var byteC byte = 'j'

又因为 byte 实质上是整型 uint8,所以可以直接转成整型值。在格式化说明符中我们使用 %c 表示字符,%d 表示整型:

// 声明 byte 类型字符
var byteC byte = 'j'
fmt.Printf("字符 %c 对应的整型为 %d\n", byteC, byteC)
// Output: 字符 j 对应的整型为 106

rune

byte 相同,想要声明 rune 类型的字符可以使用 rune 关键字指明:

var runeC rune = 'J'

但如果在声明一个字符变量时没有指明类型,Go 会默认它是 rune 类型

runeC := 'J'
fmt.Printf("字符 %c 的类型为 %T\n", runeC, runeC)
// Output: 字符 J 的类型为 int32

为什么需要两种类型?

看到这里你可能会问了,既然都用于表示字符,为什么还需要两种类型呢?

我们知道,byte 占用一个字节,因此它可以用于表示 ASCII 字符。而 UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等byte 显然不擅长这样的表示,就算你想要使用多个 byte 进行表示,你也无从知晓你要处理的 UTF-8 字符究竟占了几个字节。

因此,如果你在中文字符串上狂妄地进行截取,一定会输出乱码:

testString := "你好,世界"
fmt.Println(testString[:2]) // 输出乱码,因为截取了前两个字节
fmt.Println(testString[:3]) // 输出「你」,一个中文字符由三个字节表示

此时就需要 rune 的帮助了。利用 []rune() 将字符串转为 Unicode 码点再进行截取,这样就无需考虑字符串中含有 UTF-8 字符的情况了:

testString := "你好,世界"
fmt.Println(string([]rune(testString)[:2])) // 输出:「你好」

Tips:Unicode 和 ASCII 一样,是一种字符集,UTF-8 则是一种编码方式。

遍历字符串

字符串遍历有两种方式,一种是下标遍历,一种是使用 range

下标遍历

由于在 Go 语言中,字符串以 UTF-8 编码方式存储,使用 len() 函数获取字符串长度时,获取到的是该 UTF-8 编码字符串的字节长度,通过下标索引字符串将会产生一个字节。因此,如果字符串中含有 UTF-8 编码字符,就会出现乱码:

testString := "Hello,世界"

for i := 0; i < len(testString); i++ {
c := testString[i]
fmt.Printf("%c 的类型是 %s\n", c, reflect.TypeOf(c))
}

/* Output:
H 的类型是 uint8(ASCII 字符返回正常)
e 的类型是 uint8
l 的类型是 uint8
l 的类型是 uint8
o 的类型是 uint8
ï 的类型是 uint8(从这里开始出现了奇怪的乱码)
¼ 的类型是 uint8
Œ 的类型是 uint8
ä 的类型是 uint8
¸ 的类型是 uint8
– 的类型是 uint8
ç 的类型是 uint8
• 的类型是 uint8
Œ 的类型是 uint8
*/

range

range 遍历则会得到 rune 类型的字符:

testString := "Hello,世界"

for _, c := range testString {
fmt.Printf("%c 的类型是 %s\n", c, reflect.TypeOf(c))
}

/* Output:
H 的类型是 int32
e 的类型是 int32
l 的类型是 int32
l 的类型是 int32
o 的类型是 int32
, 的类型是 int32
世 的类型是 int32
界 的类型是 int32
*/

总结

  • Go 语言中没有字符的概念,一个字符就是一堆字节,它可能是单个字节(ASCII 字符集),也有可能是多个字节(Unicode 字符集)
  • byteuint8 的别名,长度为 1 个字节,用于表示 ASCII 字符
  • rune 则是 int32 的别名,长度为 4 个字节,用于表示以 UTF-8 编码的 Unicode 码点
  • 字符串的截取是以字节为单位的
  • 使用下标索引字符串会产生字节
  • 想要遍历 rune 类型的字符则使用 range 方法进行遍历

参考资料

❌
❌