diff --git a/ch3-asm/ch3-02-arch.md b/ch3-asm/ch3-02-arch.md index 0c06c0f..f61349c 100644 --- a/ch3-asm/ch3-02-arch.md +++ b/ch3-asm/ch3-02-arch.md @@ -1,6 +1,6 @@ # 3.2 计算机结构 -汇编语言是直面计算机的编程语言,因此理解计算机结构是掌握汇编语言的前提。当前流行的计算机基本采用的是冯 · 诺伊曼计算机体系结构(在某些特殊领域还有哈佛体系架构)。冯 · 诺依曼结构也称为普林斯顿结构,采用的是一种将程序指令和数据存储在一起的存储结构。冯 · 诺伊曼计算机中的指令和数据存储器其实指的是计算机中的内存,然后在配合 CPU 处理器就组成了一个最简单的计算机了。 +汇编语言是直面计算机的编程语言,因此理解计算机结构是掌握汇编语言的前提。当前流行的计算机基本采用的是冯·诺伊曼计算机体系结构(在某些特殊领域还有哈佛体系架构)。冯·诺依曼结构也称为普林斯顿结构,采用的是一种将程序指令和数据存储在一起的存储结构。冯·诺伊曼计算机中的指令和数据存储器其实指的是计算机中的内存,然后在配合 CPU 处理器就组成了一个最简单的计算机了。 汇编语言其实是一种非常简单的编程语言,因为它面向的计算机模型就是非常简单的。让人觉得汇编语言难学主要有几个原因:不同类型的 CPU 都有自己的一套指令;即使是相同的 CPU,32 位和 64 位的运行模式依然会有差异;不同的汇编工具同样有自己特有的汇编指令;不同的操作系统和高级编程语言和底层汇编的调用规范并不相同。本节将描述几个有趣的汇编语言模型,最后精简出一个适用于 AMD64 架构的精简指令集,以便于 Go 汇编语言的学习。 @@ -15,16 +15,16 @@ 下面是这八种状态的描述,其中每个状态由一个字符标识: -| 字符 | C 语言类比 | 含义 -| --- | ----------------- | ------ -| `>` | `++ptr;` | 指针加一 -| `<` | `--ptr;` | 指针减一 -| `+` | `++*ptr;` | 指针指向的字节的值加一 -| `-` | `--*ptr;` | 指针指向的字节的值减一 -| `.` | `putchar(*ptr);` | 输出指针指向的单元内容(ASCⅡ 码) -| `,` | `*ptr = getch();` | 输入内容到指针指向的单元(ASCⅡ 码) -| `[` | `while(*ptr) {}` | 如果指针指向的单元值为零,向后跳转到对应的 `]` 指令的次一指令处 -| `]` | | 如果指针指向的单元值不为零,向前跳转到对应的 `[` 指令的次一指令处 +| 字符 | C 语言类比 | 含义 | +| --- | ----------------- | ------ | +| `>` | `++ptr;` | 指针加一 | +| `<` | `--ptr;` | 指针减一 | +| `+` | `++*ptr;` | 指针指向的字节的值加一 | +| `-` | `--*ptr;` | 指针指向的字节的值减一 | +| `.` | `putchar(*ptr);` | 输出指针指向的单元内容(ASCⅡ 码) | +| `,` | `*ptr = getch();` | 输入内容到指针指向的单元(ASCⅡ 码) | +| `[` | `while(*ptr) {}` | 如果指针指向的单元值为零,向后跳转到对应的 `]` 指令的次一指令处 | +| `]` | | 如果指针指向的单元值不为零,向前跳转到对应的 `[` 指令的次一指令处 | 下面是一个 brainfuck 程序,向标准输出打印 "hi" 字符串: @@ -38,34 +38,34 @@ 《人力资源机器》(Human Resource Machine)是一款设计精良汇编语言编程游戏。在游戏中,玩家扮演一个职员角色,来模拟人力资源机器的运行。通过完成上司给的每一份任务来实现晋升的目标,完成任务的途径就是用游戏提供的 11 个机器指令编写正确的汇编程序,最终得到正确的输出结果。人力资源机器的汇编语言可以认为是跨平台、跨操作系统的通用的汇编语言,因为在 macOS、Windows、Linux 和 iOS 上该游戏的玩法都是完全一致的。 -人力资源机器的机器模型非常简单:INBOX 命令对应输入设备,OUTBOX 对应输出设备,玩家小人对应一个寄存器,临时存放数据的地板对应内存,然后是数据传输、加减、跳转等基本的指令。总共有 11 个机器指令: +人力资源机器的机器模型非常简单:`INBOX` 命令对应输入设备,`OUTBOX` 对应输出设备,玩家小人对应一个寄存器,临时存放数据的地板对应内存,然后是数据传输、加减、跳转等基本的指令。总共有 11 个机器指令: | 名称 | 解释 | -| -------- | --- -| INBOX | 从输入通道取一个整数数据,放到手中 (寄存器) -| OUTBOX | 将手中(寄存器)的数据放到输出通道,然后手中将没有数据(此时有些指令不能运行) -| COPYFROM | 将地板上某个编号的格子中的数据复制到手中(手中之前的数据作废),地板格子必须有数据 -| COPYTO | 将手中(寄存器)的数据复制到地板上某个编号的格子中,手中的数据不变 -| ADD | 将手中(寄存器)的数据和某个编号对应的地板格子的数据相加,新数据放到手中(手中之前的数据作废) -| SUB | 将手中(寄存器)的数据和某个编号对应的地板格子的数据相减,新数据放到手中(手中之前的数据作废) -| BUMP+ | 自加一 -| BUMP- | 自减一 -| JUMP | 跳转 -| JUMP =0 | 为零条件跳转 -| JUMP <0 | 为负条件跳转 +| -------- | --- | +| `INBOX` | 从输入通道取一个整数数据,放到手中 (寄存器) | +| `OUTBOX` | 将手中(寄存器)的数据放到输出通道,然后手中将没有数据(此时有些指令不能运行) | +| `COPYFROM` | 将地板上某个编号的格子中的数据复制到手中(手中之前的数据作废),地板格子必须有数据 | +| `COPYTO` | 将手中(寄存器)的数据复制到地板上某个编号的格子中,手中的数据不变 | +| `ADD` | 将手中(寄存器)的数据和某个编号对应的地板格子的数据相加,新数据放到手中(手中之前的数据作废) | +| `SUB` | 将手中(寄存器)的数据和某个编号对应的地板格子的数据相减,新数据放到手中(手中之前的数据作废) | +| `BUMP+` | 自加一 | +| `BUMP-` | 自减一 | +| `JUMP` | 跳转 | +| `JUMP =0` | 为零条件跳转 | +| `JUMP <0` | 为负条件跳转 | 除了机器指令外,游戏中有些环节还提供类似寄存器的场所,用于存放临时的数据。人力资源机器游戏的机器指令主要分为以下几类: -- 输入 / 输出 (INBOX, OUTBOX): 输入后手中将只有 1 份新拿到的数据, 输出后手中将没有数据。 -- 数据传输指令 (COPYFROM/COPYTO): 主要用于仅有的 1 个寄存器(手中)和内存之间的数据传输,传输时要确保源数据是有效的 -- 算术相关 (ADD/SUB/BUMP+/BUMP-) +- 输入/输出 (`INBOX`/`OUTBOX`): 输入后手中将只有 1 份新拿到的数据, 输出后手中将没有数据。 +- 数据传输指令 (`COPYFROM`/`COPYTO`): 主要用于仅有的 1 个寄存器(手中)和内存之间的数据传输,传输时要确保源数据是有效的 +- 算术相关 (`ADD`/`SUB`/`BUMP+`/`BUMP-`) - 跳转指令: 如果是条件跳转,寄存器中必须要有数据 主流的处理器也有类似的指令。除了基本的算术和逻辑运算指令外,再配合有条件跳转指令就可以实现分支、循环等常见控制流结构了。 下图是某一层的任务:将输入数据的 0 剔除,非 0 的数据依次输出,右边部分是解决方案。 -![](../images/ch3-1-arch-hsm-zero.jpg) +![图 3-1 人力资源机器](../images/ch3-1-arch-hsm-zero.jpg) *图 3-1 人力资源机器* @@ -80,7 +80,7 @@ LOOP: JUMP LOOP ``` -首先通过 INBOX 指令读取一个数据包;然后判断包裹的数据是否为 0,如果是 0 的话就跳转到开头继续读取下一个数据包;否则将输出数据包,然后再跳转到开头。以此循环无休止地处理数据包裹,直到任务完成晋升到更高一级的岗位,然后处理类似的但更复杂的任务。 +首先通过 `INBOX` 指令读取一个数据包;然后判断包裹的数据是否为 `0`,如果是 `0` 的话就跳转到开头继续读取下一个数据包;否则将输出数据包,然后再跳转到开头。以此循环无休止地处理数据包裹,直到任务完成晋升到更高一级的岗位,然后处理类似的但更复杂的任务。 ## 3.2.3 X86-64 体系结构 @@ -89,7 +89,7 @@ X86 其实是是 80X86 的简称(后面三个字母),包括 Intel 8086、8 在使用汇编语言之前必须要了解对应的 CPU 体系结构。下面是 X86/AMD 架构图: -![](../images/ch3-2-arch-amd64-01.ditaa.png) +![图 3-2 AMD64 架构](../images/ch3-2-arch-amd64-01.ditaa.png) *图 3-2 AMD64 架构* @@ -107,7 +107,7 @@ Go 汇编为了简化汇编代码的编写,引入了 PC、FP、SP、SB 四个 四个伪寄存器和 X86/AMD64 的内存和寄存器的相互关系如下图: -![](../images/ch3-3-arch-amd64-02.ditaa.png) +![图 3-3 Go 汇编的伪寄存器](../images/ch3-3-arch-amd64-02.ditaa.png) *图 3-3 Go 汇编的伪寄存器* @@ -120,64 +120,64 @@ Go 汇编为了简化汇编代码的编写,引入了 PC、FP、SP、SB 四个 很多汇编语言的教程都会强调汇编语言是不可移植的。严格来说汇编语言是在不同的 CPU 类型、或不同的操作系统环境、或不同的汇编工具链下是不可移植的,而在同一种 CPU 中运行的机器指令是完全一样的。汇编语言这种不可移植性正是其普及的一个极大的障碍。虽然 CPU 指令集的差异是导致不好移植的较大因素,但是汇编语言的相关工具链对此也有不可推卸的责任。而源自 Plan9 的 Go 汇编语言对此做了一定的改进:首先 Go 汇编语言在相同 CPU 架构上是完全一致的,也就是屏蔽了操作系统的差异;同时 Go 汇编语言将一些基础并且类似的指令抽象为相同名字的伪指令,从而减少不同 CPU 架构下汇编代码的差异(寄存器名字和数量的差异是一直存在的)。本节的目的也是找出一个较小的精简指令集,以简化 Go 汇编语言的学习。 -X86 是一个极其复杂的系统,有人统计 x86-64 中指令有将近一千个之多。不仅仅如此,X86 中的很多单个指令的功能也非常强大,比如有论文证明了仅仅一个 MOV 指令就可以构成一个图灵完备的系统。以上这是两种极端情况,太多的指令和太少的指令都不利于汇编程序的编写,但是也从侧面体现了 MOV 指令的重要性。 +X86 是一个极其复杂的系统,有人统计 x86-64 中指令有将近一千个之多。不仅仅如此,X86 中的很多单个指令的功能也非常强大,比如有论文证明了仅仅一个 `MOV` 指令就可以构成一个图灵完备的系统。以上这是两种极端情况,太多的指令和太少的指令都不利于汇编程序的编写,但是也从侧面体现了 `MOV` 指令的重要性。 通用的基础机器指令大概可以分为数据传输指令、算术运算和逻辑运算指令、控制流指令和其它指令等几类。因此我们可以尝试精简出一个 X86-64 指令集,以便于 Go 汇编语言的学习。 因此我们先看看重要的 MOV 指令。其中 MOV 指令可以用于将字面值移动到寄存器、字面值移到内存、寄存器之间的数据传输、寄存器和内存之间的数据传输。需要注意的是,MOV 传输指令的内存操作数只能有一个,可以通过某个临时寄存器达到类似目的。最简单的是忽略符号位的数据传输操作,386 和 AMD64 指令一样,不同的 1、2、4 和 8 字节宽度有不同的指令: -| Data Type | 386/AMD64 | Comment | -| --------- | ----------- | ------------- | -| [1]byte | MOVB | B => Byte | -| [2]byte | MOVW | W => Word | -| [4]byte | MOVL | L => Long | -| [8]byte | MOVQ | Q => Quadword | +| Data Type | 386/AMD64 | Comment | +| ----------- | ----------- | ------------- | +| `[1]byte` | MOVB | B => Byte | +| `[2]byte` | MOVW | W => Word | +| `[4]byte` | MOVL | L => Long | +| `[8]byte` | MOVQ | Q => Quadword | MOV 指令它不仅仅用于在寄存器和内存之间传输数据,而且还可以用于处理数据的扩展和截断操作。当数据宽度和寄存器的宽度不同又需要处理符号位时,386 和 AMD64 有各自不同的指令: | Data Type | 386 | AMD64 | Comment | | --------- | ------- | ------- | ------------- | -| int8 | MOVBLSX | MOVBQSX | sign extend | -| uint8 | MOVBLZX | MOVBQZX | zero extend | -| int16 | MOVWLSX | MOVWQSX | sign extend | -| uint16 | MOVWLZX | MOVWQZX | zero extend | +| `int8` | MOVBLSX | MOVBQSX | sign extend | +| `uint8` | MOVBLZX | MOVBQZX | zero extend | +| `int16` | MOVWLSX | MOVWQSX | sign extend | +| `uint16` | MOVWLZX | MOVWQZX | zero extend | -比如当需要将一个 int64 类型的数据转为 bool 类型时,则需要使用 MOVBQZX 指令处理。 +比如当需要将一个 `int64` 类型的数据转为 `bool` 类型时,则需要使用 `MOVBQZX` 指令处理。 -基础算术指令有 ADD、SUB、MUL、DIV 等指令。其中 ADD、SUB、MUL、DIV 用于加、减、乘、除运算,最终结果存入目标寄存器。基础的逻辑运算指令有 AND、OR 和 NOT 等几个指令,对应逻辑与、或和取反等几个指令。 +基础算术指令有 `ADD`、`SUB`、`MUL`、`DIV` 等指令。其中 `ADD`、`SUB`、`MUL`、`DIV` 用于加、减、乘、除运算,最终结果存入目标寄存器。基础的逻辑运算指令有 `AND`、`OR` 和 `NOT` 等几个指令,对应逻辑与、或和取反等几个指令。 | 名称 | 解释 | -| ------ | --- -| ADD | 加法 -| SUB | 减法 -| MUL | 乘法 -| DIV | 除法 -| AND | 逻辑与 -| OR | 逻辑或 -| NOT | 逻辑取反 +| ------ | --- | +| `ADD` | 加法 | +| `SUB` | 减法 | +| `MUL` | 乘法 | +| `DIV` | 除法 | +| `AND` | 逻辑与 | +| `OR` | 逻辑或 | +| `NOT` | 逻辑取反 | -其中算术和逻辑指令是顺序编程的基础。通过逻辑比较影响状态寄存器,再结合有条件跳转指令就可以实现更复杂的分支或循环结构。需要注意的是 MUL 和 DIV 等乘除法指令可能隐含使用了某些寄存器,指令细节请查阅相关手册。 +其中算术和逻辑指令是顺序编程的基础。通过逻辑比较影响状态寄存器,再结合有条件跳转指令就可以实现更复杂的分支或循环结构。需要注意的是 `MUL` 和 `DIV` 等乘除法指令可能隐含使用了某些寄存器,指令细节请查阅相关手册。 -控制流指令有 CMP、JMP-if-x、JMP、CALL、RET 等指令。CMP 指令用于两个操作数做减法,根据比较结果设置状态寄存器的符号位和零位,可以用于有条件跳转的跳转条件。JMP-if-x 是一组有条件跳转指令,常用的有 JL、JLZ、JE、JNE、JG、JGE 等指令,对应小于、小于等于、等于、不等于、大于和大于等于等条件时跳转。JMP 指令则对应无条件跳转,将要跳转的地址设置到 IP 指令寄存器就实现了跳转。而 CALL 和 RET 指令分别为调用函数和函数返回指令。 +控制流指令有 `CMP`、`JMP-if-x`、`JMP`、`CALL`、`RET` 等指令。`CMP` 指令用于两个操作数做减法,根据比较结果设置状态寄存器的符号位和零位,可以用于有条件跳转的跳转条件。`JMP-if-x` 是一组有条件跳转指令,常用的有 `JL`、`JLZ`、`JE`、`JNE`、`JG`、`JGE` 等指令,对应小于、小于等于、等于、不等于、大于和大于等于等条件时跳转。`JMP` 指令则对应无条件跳转,将要跳转的地址设置到 IP 指令寄存器就实现了跳转。而 `CALL` 和 `RET` 指令分别为调用函数和函数返回指令。 | 名称 | 解释 | -| -------- | --- -| JMP | 无条件跳转 -| JMP-if-x | 有条件跳转,JL、JLZ、JE、JNE、JG、JGE -| CALL | 调用函数 -| RET | 函数返回 +| -------- | --- | +| `JMP` | 无条件跳转 | +| `JMP-if-x` | 有条件跳转,`JL`、`JLZ`、`JE`、`JNE`、`JG`、`JGE` | +| `CALL` | 调用函数 | +| `RET` | 函数返回 | 无条件和有条件调整指令是实现分支和循环控制流的基础指令。理论上,我们也可以通过跳转指令实现函数的调用和返回功能。不过因为目前函数已经是现代计算机中的一个最基础的抽象,因此大部分的 CPU 都针对函数的调用和返回提供了专有的指令和寄存器。 -其它比较重要的指令有 LEA、PUSH、POP 等几个。其中 LEA 指令将标准参数格式中的内存地址加载到寄存器(而不是加载内存位置的内容)。PUSH 和 POP 分别是压栈和出栈指令,通用寄存器中的 SP 为栈指针,栈是向低地址方向增长的。 +其它比较重要的指令有 `LEA`、`PUSH`、`POP` 等几个。其中 LEA 指令将标准参数格式中的内存地址加载到寄存器(而不是加载内存位置的内容)。`PUSH` 和 `POP` 分别是压栈和出栈指令,通用寄存器中的 `SP` 为栈指针,栈是向低地址方向增长的。 -| 名称 | 解释 | -| ------ | --- -| LEA | 取地址 -| PUSH | 压栈 -| POP | 出栈 +| 名称 | 解释 | +| ---- | ------ | +| `LEA` | 取地址 | +| `PUSH` | 压栈 | +| `POP` | 出栈 | 当需要通过间接索引的方式访问数组或结构体等某些成员对应的内存时,可以用 LEA 指令先对目前内存取地址,然后在操作对应内存的数据。而栈指令则可以用于函数调整自己的栈空间大小。 -最后需要说明的是,Go 汇编语言可能并没有支持全部的 CPU 指令。如果遇到没有支持的 CPU 指令,可以通过 Go 汇编语言提供的 BYTE 命令将真实的 CPU 指令对应的机器码填充到对应的位置。完整的 X86 指令在 https://github.com/golang/arch/blob/master/x86/x86.csv 文件定义。同时 Go 汇编还正对一些指令定义了别名,具体可以参考这里 https://golang.org/src/cmd/internal/obj/x86/anames.go 。 +最后需要说明的是,Go 汇编语言可能并没有支持全部的 CPU 指令。如果遇到没有支持的 CPU 指令,可以通过 Go 汇编语言提供的 BYTE 命令将真实的 CPU 指令对应的机器码填充到对应的位置。完整的 X86 指令在 [https://github.com/golang/arch/blob/master/x86/x86.csv](https://github.com/golang/arch/blob/master/x86/x86.csv) 文件定义。同时 Go 汇编还正对一些指令定义了别名,具体可以参考这里 [https://golang.org/src/cmd/internal/obj/x86/anames.go](https://golang.org/src/cmd/internal/obj/x86/anames.go)。 diff --git a/ch5-web/ch5-02-router.md b/ch5-web/ch5-02-router.md index 6cc6c16..15d66ff 100644 --- a/ch5-web/ch5-02-router.md +++ b/ch5-web/ch5-02-router.md @@ -36,7 +36,7 @@ DELETE /user/starred/:owner/:repo ## 5.2.1 httprouter -较流行的开源 go Web 框架大多使用 httprouter,或是基于 httprouter 的变种对路由进行支持。前面提到的 github 的参数式路由在 httprouter 中都是可以支持的。 +较流行的开源 go Web 框架大多使用 httprouter,或是基于 httprouter 的变种对路由进行支持。前面提到的 Github 的参数式路由在 httprouter 中都是可以支持的。 因为 httprouter 中使用的是显式匹配,所以在设计路由的时候需要规避一些会导致路由冲突的情况,例如: @@ -50,7 +50,7 @@ GET /user/info/:name POST /user/:id ``` -简单来讲的话,如果两个路由拥有一致的 http 方法 (指 GET/POST/PUT/DELETE) 和请求路径前缀,且在某个位置出现了 A 路由是 wildcard(指: id 这种形式)参数,B 路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接 panic: +简单来讲的话,如果两个路由拥有一致的 http 方法 (指 `GET`、`POST`、`PUT`、`DELETE`) 和请求路径前缀,且在某个位置出现了 A 路由是 wildcard(指 `:id` 这种形式)参数,B 路由则是普通字符串,那么就会发生路由冲突。路由冲突会在初始化阶段直接 panic: ```shell panic: wildcard route ':id' conflicts with existing children in path '/user/:id' diff --git a/ch5-web/ch5-03-middleware.md b/ch5-web/ch5-03-middleware.md index 92d1d19..1d73c5c 100644 --- a/ch5-web/ch5-03-middleware.md +++ b/ch5-web/ch5-03-middleware.md @@ -77,7 +77,7 @@ func main() { 每一个 handler 里都有之前提到的记录运行时间的代码,每次增加新的路由我们也同样需要把这些看起来长得差不多的代码拷贝到我们需要的地方去。因为代码不太多,所以实施起来也没有遇到什么大问题。 -渐渐的我们的系统增加到了 30 个路由和 `handler` 函数,每次增加新的 handler,我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。 +渐渐的我们的系统增加到了 30 个路由和 handler 函数,每次增加新的 handler,我们的第一件工作就是把之前写的所有和业务逻辑无关的周边代码先拷贝过来。 接下来系统安稳地运行了一段时间,突然有一天,老板找到你,我们最近找人新开发了监控系统,为了系统运行可以更加可控,需要把每个接口运行的耗时数据主动上报到我们的监控系统里。给监控系统起个名字吧,叫 metrics。现在你需要修改代码并把耗时通过 HTTP Post 的方式发给 metrics 系统了。我们来修改一下 `helloHandler()`: @@ -153,7 +153,7 @@ func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { func (ResponseWriter, *Request) ``` -那么这个 `handler` 和 `http.HandlerFunc()` 就有了一致的函数签名,可以将该 `handler()` 函数进行类型转换,转为 `http.HandlerFunc`。而 `http.HandlerFunc` 实现了 `http.Handler` 这个接口。在 `http` 库需要调用你的 handler 函数来处理 http 请求时,会调用 `HandlerFunc()` 的 `ServeHTTP()` 函数,可见一个请求的基本调用链是这样的: +那么这个 handler 和 `http.HandlerFunc()` 就有了一致的函数签名,可以将该 `handler()` 函数进行类型转换,转为 `http.HandlerFunc`。而 `http.HandlerFunc` 实现了 `http.Handler` 这个接口。在 `http` 库需要调用你的 handler 函数来处理 http 请求时,会调用 `HandlerFunc()` 的 `ServeHTTP()` 函数,可见一个请求的基本调用链是这样的: ```go h = getHandler() => h.ServeHTTP(w, r) => h(w, r) @@ -282,7 +282,7 @@ throttler.go 每一个 Web 框架都会有对应的中间件组件,如果你有兴趣,也可以向这些项目贡献有用的中间件,只要合理一般项目的维护人也愿意合并你的 Pull Request。 -比如开源界很火的 gin 这个框架,就专门为用户贡献的中间件开了一个仓库,见 *图 5-9*: +比如开源界很火的 gin 这个框架,就专门为用户贡献的中间件开了一个仓库,见*图 5-9*: ![](../images/ch6-03-gin_contrib.png) diff --git a/ch5-web/ch5-05-database.md b/ch5-web/ch5-05-database.md index c056bab..7ac24ab 100644 --- a/ch5-web/ch5-05-database.md +++ b/ch5-web/ch5-05-database.md @@ -108,11 +108,8 @@ func main() { 在 Web 开发领域常常提到的 ORM 是什么?我们先看看万能的维基百科: -``` -对象关系映射(英语:Object Relational Mapping,简称 ORM,或 O/RM,或 O/R mapping), -是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。 -从效果上说,它其实是创建了一个可在编程语言里使用的 “虚拟对象数据库”。 -``` +> 对象关系映射(英语:Object Relational Mapping,简称 ORM,或 O/RM,或 O/R mapping),是一种程序设计技术,用于实现面向对象编程语言里不同类型系统的数据之间的转换。 +> 从效果上说,它其实是创建了一个可在编程语言里使用的 “虚拟对象数据库”。 最为常见的 ORM 做的是从 db 到程序的类或结构体这样的映射。所以你手边的程序可能是从 MySQL 的表映射你的程序内的类。我们可以先来看看其它的程序语言里的 ORM 写起来是怎么样的感觉: @@ -122,7 +119,7 @@ func main() { >>> b.save() ``` -完全没有数据库的痕迹,没错,ORM 的目的就是屏蔽掉 DB 层,很多语言的 ORM 只要把你的类或结构体定义好,再用特定的语法将结构体之间的一对一或者一对多关系表达出来。那么任务就完成了。然后你就可以对这些映射好了数据库表的对象进行各种操作,例如 save,create,retrieve,delete。至于 ORM 在背地里做了什么阴险的勾当,你是不一定清楚的。使用 ORM 的时候,我们往往比较容易有一种忘记了数据库的直观感受。举个例子,我们有个需求:向用户展示最新的商品列表,我们再假设,商品和商家是 1:1 的关联关系,我们就很容易写出像下面这样的代码: +完全没有数据库的痕迹,没错,ORM 的目的就是屏蔽掉 DB 层,很多语言的 ORM 只要把你的类或结构体定义好,再用特定的语法将结构体之间的一对一或者一对多关系表达出来。那么任务就完成了。然后你就可以对这些映射好了数据库表的对象进行各种操作,例如 save、create、retrieve、delete。至于 ORM 在背地里做了什么阴险的勾当,你是不一定清楚的。使用 ORM 的时候,我们往往比较容易有一种忘记了数据库的直观感受。举个例子,我们有个需求:向用户展示最新的商品列表,我们再假设,商品和商家是 1:1 的关联关系,我们就很容易写出像下面这样的代码: ```python # 伪代码 @@ -237,6 +234,6 @@ func GetAllByProductIDAndCustomerID(ctx context.Context, productIDs []uint64, cu } ``` -像这样的代码,在上线之前把 DAO 层的变更集的 const 部分直接拿给 DBA 来进行审核,就比较方便了。代码中的 sqlutil.Named 是类似于 sqlx 中的 Named 函数,同时支持 where 表达式中的比较操作符和 in。 +像这样的代码,在上线之前把 DAO 层的变更集的 const 部分直接拿给 DBA 来进行审核,就比较方便了。代码中的 `sqlutil.Named` 是类似于 sqlx 中的 `Named` 函数,同时支持 where 表达式中的比较操作符和 in。 这里为了说明简便,函数写得稍微复杂一些,仔细思考一下的话查询的导出函数还可以进一步进行简化。请读者朋友们自行尝试。