diff --git a/Chapter 29 Data storage files and databases.md b/Chapter 29 Data storage files and databases.md
new file mode 100644
index 0000000000000000000000000000000000000000..5f7cc2f846b3c14174dd992179079002a9402fbb
--- /dev/null
+++ b/Chapter 29 Data storage files and databases.md
@@ -0,0 +1,1944 @@
+第 29 章:数据存储:文件和数据库
+
+[TOC]
+
+### **1 你将在本章节学到什么?**
+
+* 如何新建一个文件。
+* 如何将数据写入文件。
+* 如何新建 CSV 格式的文件。
+* 如何查询以下数据库:
+ * MySQL
+ * PostgreSQL
+ * MongoDB
+ * ElasticSearch
+
+### **2 技术概念梳理**
+
+* 文件权限
+* 八进制
+* SQL注入
+* 预查询
+* 无模式
+* 关系数据库管理系统
+
+### **3 介绍**
+
+此章节,会通过 Go 使用不同技术存储数据。
+
+### **4 新建一个文件**
+
+使用 os.Create
方法可以新建一个文件:
+
+```go
+// data-storage/file-save/simple/main.go
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func main() {
+ f, err := os.Create("test.csv")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ //... success
+}
+```
+
+我们新建一个命名为 "test.csv"
的文件。如果有报错的话,程序会打印错误并返回,程序代码如下:
+
+```go
+f, err := os.OpenFile("test.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+if err != nil {
+ fmt.Println(err)
+ return
+}
+```
+
+os.Create
的内部通过以下参数调用 os.Openfile
方法,参数如下:
+
+* 文件名(比如:"test.csv"
)
+* 文件模式(比如:os.O_RDWR|os.O_CREATE|os.O_TRUNC
)
+* 文件权限模式(比如:0666
)
+
+我们将在下面两个章节中知晓 **文件模式 和**文件权限模式**是什么。当然,如果你已经熟悉了这些章节内容,可以跳过。
+
+### **5 文件模式标记**
+
+你将通过系统调用的方式,使用 os.Openfile
打开一个文件。通过调用,系统需要清楚**文件路径**以及需要达到目的其他额外信息。这一系列信息包含在一个模式标记列表中,他们有不同类型的模式标记:
+
+* **允许访问模式标记**:在新建一个文件时,必须指定其中一个标记作为入参参数。
+* **允许执行模式标记**:系统执行文件操作时会被限制。
+* **文件状态模式标记**:当文件打开时,会得到一个对文件操作的状态结果反馈。
+
+
+
+| **标记类型** | 描述 |
+| ------------ | ------------------------------------------------------------ |
+| os.O_RDONLY | 以只读模式打开,只能读。 |
+| os.O_WRONLY | 以只写模式打开,只能写不能读。 |
+| os.O_RDWR | 以读写模式打开。 |
+| os.O_CREATE | 新建文件若不存在。 |
+| os.O_EXCL | 若已使用 os.O_CREATE
模式,且文件已存在,则返回一个错误。 |
+| os.O_TRUNC | 打开文件时会被清空,若文件有内容则会清除。 |
+| os.O_APPEND | 将内容写入追加到文件文本后面。 |
+| os.O_SYNC | 以同步模式打开,程序会等待所有数据被完全写入为止。 |
+
+文件打开模式标记
+
+每种模式标记的值都是整型。你必须使用按位计算使用这些模式标记(看章节 [Bitmasks])。当你使用 os.Create
时,文件将会通过以下模式标记打开:
+
+```
+os.O_RDWR|os.O_CREATE|os.O_TRUNC
+```
+
+这样意味着文件是通过**读写模式**打开的,若文件不存在则会新建。另外,系统会进行文件内容清空操作。
+
+举例:若需要确定文件是否新建,可以通过添加模式标记:os.O_EXCL
+
+```go
+f, err := os.OpenFile("test.csv", os.O_RDWR|os.O_CREATE|os.O_TRUNC|os.O_EXCL, 0666)
+if err != nil {
+ fmt.Println(err)
+ return
+}
+fmt.Println(f)
+```
+
+上面程序执行后将输出:
+
+```
+open test.csv: file exists
+```
+
+哪种模式标记才是期望的呢?
+
+### **6 文件模式**
+
+在 UNIX 系统中,每一个文件都有权限设置:
+
+* 文件权限属性的拥有者
+* 文件权限所属用户组别
+* 文件权限所属其他用户
+
+若你想查看文件的权限信息,可以打开你的终端窗口,并输入 ls -l
命令。
+
+```shell
+$ ls -al
+-rw-r--r-- 1 maximilienandile staff 242 25 nov 19:47 main.go
+```
+
+* **-rw-r--r--** :文件权限模式
+* **1** :链接数字
+* **maximilienandile** :文件权限属性的拥有者
+* **staff** :文件权限所属用户组别
+* **242** :以 byte
为单位的文件大小
+* **25 nov 19:47** :最后修改日期时间
+
+###### 6.0.0.1 符号表示
+
+文件权限模式由三个区块组成(权限拥有者 user,权限所属组别 group,以及其他人 others )
+
+![File mode schema[fig:File-mode-schema]](https://www.practical-go-lessons.com/img/permissions.7a548570.png)
+
+当看到第一个区块时,可以通过 3 个字母去定义权限,它们总是以这样的方式排序:
+
+* **r** :读
+* **w** :写
+* **x** :执行
+
+第一个字符是连接号 -
。当文件是一个目录类型时,相当于目录英文第一个字母 d
。如果文件权限模式的第一个字符是-
,这表示权限不可用。
+
+下面通过 **-rw-r--r--** 例子的方式,说明:
+
+* **First char**:这是一个文件
+* **User(rw-)**:文件拥有者可读、可写、不可执行。
+* **Group(r--)**:文件所属组别可读、不可写、不可执行。
+* **Others(r--)**:文件所属组别可读、不可写、不可执行。
+
+下面用 Go 实现文件模式:
+
+```go
+// data-storage/file-save/mode/main.go
+
+// 打开文件
+f, err := os.Open("myFile.test")
+if err != nil {
+ fmt.Println(err)
+ return
+}
+// 读取文件信息
+info, err := f.Stat()
+if err != nil {
+ fmt.Println(err)
+ return
+}
+fmt.Println(info.Mode())
+```
+
+上述程序将输出:-rw-r--r-- 。
+
+###### 6.0.0.2 数字表示
+
+此模式还可以通过转换为 八进制 数字转换 (通过 %o
格式化)。
+
+```go
+fmt.Printf("Mode Numeric : %o\n", info.Mode())
+fmt.Printf("Mode Symbolic : %s\n", info.Mode())
+// 数字模式 : 644
+// 字符模式 : -rw-r--r--
+```
+
+Go 在调用 os.Openfile
使用数字表示,由 3个数字组成:
+
+![File mode numeric notation[图2]](https://www.practical-go-lessons.com/img/octal_file_mode.65204000.png)
+
+数字(八进制)表示一组权限(见图2)。每个八进制数字(从0到7)都有特定的含义。
+
+| 权限 | 符号 | 八进制 |
+| :------------------: | ---------------- | ------ |
+| 空 | ---
| 0 |
+| 只执行 | --x
| 1 |
+| 只写 | -w-
| 2 |
+| 可写 & 可执行 | -wx
| 3 |
+| 只读 | r--
| 4 |
+| 可读 & 可执行 | r-x
| 5 |
+| 可读 & 可写 | rw-
| 6 |
+| 可读 & 可写 & 可执行 | rwx
| 7 |
+
+让我们举个例子: 644,我们分解:
+
+* Owner(拥有者):6
表示可读、可写
+* Group(所属组别): 4
表示可读
+* Others(其他人): 4
表示可读
+
+另一个例子 777:
+
+* Owner(拥有者):7
表示可读、可写、可执行
+* Group(所属组别): 7
表示可读、可写、可执行
+* Others(其他人): 7
表示可读、可写、可执行
+
+###### 6.0.0.3 通过 Go 更改文件权限
+
+在一个文件上,你可以使用 Chmod
方法来更改文件模式
+
+```go
+// data-storage/file-save/mode/main.go
+
+err = f.Chmod(0777)
+if err != nil {
+ fmt.Println(err)
+}
+```
+
+这里我们将文件的模式更改为0777。这意味着所有者、组和其他人的都更改为全部权限。
+
+为什么 0777
要加0 ?它向 Go 表示该数不是十进制数而是八进制数。
+
+### **7 写文件:以 CSV 为例**
+
+#### 7.1 什么是 CSV
+
+CSV 表示逗号分隔值,它是一种用于存储数据的文件格式。
+
+* 一个 CSV 文件由多行数据组成
+* 每一行代表数据记录
+* 每一个数据由 多个“区域” 组成
+* 每一个逗号分隔符,分割每个“区域”
+* 第一行的位置,通常被用作表头标题信息进行描述
+
+举例如下:
+
+```
+age,genre,name
+23,M,Hendrick
+65,F,Stephany
+```
+
+#### 7.2 Code
+
+在本节中,我们将看到如何将数据写入文件。为了立即应用我们的知识,我们将使用一个真实的用例:从一个切片创建一个CSV文件。
+
+```go
+// data-storage/file-save/writing/main.go
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+)
+
+func main() {
+ s := [][]string{
+ {"age", "genre", "name"},
+ {"23", "M", "Hendrick"},
+ {"65", "F", "Stephany"},
+ }
+ // 打开文件或新建
+ f, err := os.Create("myFile.csv")
+ defer f.Close()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ var buffer bytes.Buffer
+ // 将切片 s 进行迭代
+ for _, data := range s {
+ buffer.WriteString(fmt.Sprintf("%s,%s,%s\n", data[0], data[1], data[2]))
+ }
+ n, err := f.Write(buffer.Bytes())
+ fmt.Printf("%d bytes written\n", n)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+```
+
+首先,我们新建一个切片 s
,这是我们想要写入 CSV 文件的数据。
+
+我们新建一个类型为 bytes.Buffer
变量 buffer
。
+
+然后我们迭代 s
将数据添加到 buffer
中,每个值由逗号分隔,并且通过字符“\ n”添加新行。
+当 buffer
已就绪时,我们将其通过方法 write
写入文件中。 此方法将 **字节类型的切片变量** 作为参数,并返回两个值:
+
+* 写入文件的字节数
+* 错误结果
+
+#### 7.3 bytes.Buffer
+
+我们在前面的程序中使用了一个数据缓冲区,缓冲区是物理存储器的一个区域,当数据从一处移动到另一处时,用来临时存储数据。
+
+若你需要高效的连接字符串或者操作 byte类型的切片时,bytes.Buffer
将非常有用。
+
+### **8 用例**
+
+我们将通过下一节内容说明以下用例:
+
+* 你被学校聘请了
+* 学校需要软件程序来管理学生和教师们:
+ - 查询、新增、更新、删除一名老师
+ - 查询、新增、更新、删除一名学生
+
+在接下来的章节内容中,我们将看到如何使用不同数据库进行操作:
+
+### **9 MySQL数据库**
+
+MySQL是一个开源的关系数据库管理系统。在本节中,我们将看到如何连接到 MySQL 数据库,以及如何用 Go 执行基本的 CRUD 操作。
+
+Go 没有附带 MySQL 驱动程序,但是标准库定义了操作 SQL 数据库的接口。
+
+在撰写本文时,在 Go wiki 中提出了两个驱动程序。
+
+我选择了最受欢迎的 github https://github.com/go-sql-driver/mysql,它是Mozilla公共许可证 2.0 版本。
+
+#### 9.1 连接
+
+```go
+// 目录:data-storage/mysql/create/main.go
+package main
+
+import (
+ "database/sql"
+ "fmt"
+
+ _ "github.com/go-sql-driver/mysql"
+)
+
+func main() {
+ db, err := sql.Open("mysql", "root:root@tcp(localhost:8889)/school")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ err = db.Ping()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+```
+
+在此,我们导入标准包数据库/ SQL,它定义在 SQL 数据库上执行操作所需的所有功能。
+
+然后我们使用空白处导入,进行注册驱动程序 github.com/go-sql-driver/mysql 。
+
+如果不导入驱动程序,可能会遇到以下错误
+
+```go
+// 未知 mysql 驱动
+sql: unknown driver "mysql" (forgotten import?)
+```
+
+sql.Open
接收两个参数:
+
+1. 驱动名称
+2. 数据源名称(通常叫法:DSN)
+
+这个 DSN 参数是你定义的一个字符串:
+
+* 用户名:root
+* 密码:root
+* 数据库连接协议:TCP
+* 主机:localhost
+* 端口:8889
+* 数据库名称:school
+
+在数据库驱动文档中,DSN 等同于以下:
+
+```
+username:password@protocol(address)/dbname?param=value
+```
+
+sql.Open
函数会返回 *sql.DB
指针
+
+通过 Ping
方法可以检查是否连接有效,如果无效,则新建一个。
+
+#### 9.2 建表
+
+第一件事,你应该想在数据库中创建一张数据表,开始编写 SQL 脚本:
+
+```sql
+CREATE TABLE `teacher` (
+ `id` INT(11) NOT NULL AUTO_INCREMENT,
+ `create_time` TIMESTAMP DEFAULT NULL,
+ `update_time` TIMESTAMP DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+ `firstname` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+ `lastname` VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
+```
+
+我们新建了一张 **教师表**,表中会存储学校内所有老师的以下数据:
+
+* 名字
+* 姓氏
+
+每个老师将有一个id(我们的表的主键)。我们还添加了两个字段列,用于保存每行数据的创建日期(创建时间)和最后更新日期(更新时间)。
+
+接下来,执行这个 SQL 脚本吧!
+
+首先,我们将这个 SQL 脚本保存到一个命名为 create_table.sql 的文件中。我们的脚本程序将首先加载此SQL 脚本:
+
+```go
+f, err := os.Open("create_table.sql")
+if err != nil {
+ fmt.Println(err)
+ return
+}
+b, err := ioutil.ReadAll(f)
+if err != nil {
+ fmt.Println(err)
+ return
+}
+```
+
+在变量 b,我们有一个字节类型的切片可以代替查询条件,执行如下:
+
+```go
+res, err := db.Exec(string(b))
+if err != nil {
+ fmt.Println(err)
+ return
+}
+```
+
+我们使用 string(b)
将字节切片转换为字符串,因为 Exec
方法以字符串作为参数。
+
+注意,返回的两个值要么是结果(类型为 sql.Result
),要么是错误。 sql.Result
是一个具有两个方法的类型接口:
+
+* LastInsertId() (int64, error)
+* RowsAffected() (int64, error)
+
+我们将检查结果是否没有错误,创建表不会影响现有的行,也不会插入数据
+
+#### 9.3 插入数据
+
+要创建一个 teacher,我们将使用 INSERT 语句。我们将使用 Exec
方法
+
+```go
+// data-storage/mysql/insert/main.go
+
+func createTeacher(firstname string, lastname string, db *sql.DB) (int64, error) {
+ res, err := db.Exec("INSERT INTO `teacher` (`create_time`, `firstname`, `lastname`) VALUES (NOW(), ?, ?)", firstname, lastname)
+ if err != nil {
+ return 0, err
+ }
+ id, err := res.LastInsertId()
+ if err != nil {
+ return 0, err
+ }
+ return id, nil
+}
+```
+
+* 有一个函数 createTeacher,它接受两个字符串参数 ( firstname
,lastname
) 和一个指向 sql.DB
的指针参数。
+* 我们传递给 Exec
方法三个参数。
+ * SQL查询字符串(带有?作为值的占位符)和我们想注入到数据库中的值。
+* db.Exec()
生成预编译语句。
+
+###### 9.3.0.1 预编译语句(Prepared Statements)
+
+遵循MySQL的文档(https://dev.mysql.com/doc/apis-php/en/apis-php-php/en/apis-php-hpli.quickstart.prepared-statements.html),准备好的语句进行两个不同的操作
+
+1. 准备阶段(prepare stage):这里,客户端(我们的Go程序)把模板发送到数据库服务器。服务器将对请求执行语法检查,它还将初始化后续步骤的资源。
+2. 绑定和执行阶段:客户端然后将值发送给服务器(在我们的例子中是 John 和 Doe ),然后,服务器将使用这些值构建查询,并最终执行查询。
+
+###### 9.3.0.2 关于 SQL 注入
+
+ SQL 注入是一个常见的漏洞,从1988年到2012年,SQL 注入占高严重漏洞的20%。当攻击者(黑客)通过操纵输入到应用程序中的数据,将一系列 SQL 语句插入到查询中时,就发生了 SQL 注入。一种解决方案(不是唯一的)是在代码中使用准备好的语句来防止这种类型的攻击, 我强烈建议您使用准备好的语句,即使它们会导致对服务器的两次调用。
+
+#### 9.4 读取一行数据
+
+我们将用 select
语句进行读取数据,你可以使用 QueryRow
方法在数据库中进行读取一行数据,这个方法会返回一个 sql.Row
类型的指针实例, 该 sql.Row
类型 有一个 Scan
方法,我们可以用它将从 DB 查询到的数据注入到Go变量中
+
+```go
+// data-storage/mysql/select/main.go
+
+func teacher(id int, db *sql.DB) (*Teacher, error) {
+ teacher := Teacher{id: id}
+ err := db.QueryRow("SELECT firstname, lastname FROM teacher WHERE id = ?", id).Scan(&teacher.firstname, &teacher.lastname)
+ if err != nil {
+ return &Teacher{}, err
+ }
+ return &teacher, nil
+}
+```
+
+* 函数 teacher
将接受一个 int
整型和一个指向 sql.DB
的指针作为参数。
+* 该整型是数据库中教师的id。
+* 这个函数将使用 QueryRow
对数据库进行查询.。
+* 然后通过 *sql.Row
返回结果行。
+* 然后我们使用 Scan
方法。
+
+此方法将从查询结果中提取数据,并将其复制到作为参数传递的变量中。注意Go将为您处理类型转换。
+
+在我们的例子中,我们创建了一个 Teacher
结构体类型:
+
+```go
+type Teacher struct {
+ id int
+ firstname string
+ lastname string
+}
+```
+
+不能直接将 Teacher
变量传递给 Scan
方法,必须传递指针类型:
+
+```go
+Scan(&teacher.firstname, &teacher.lastname)
+```
+
+使用 QueryRow
时,请注意两种常见情况:
+
+###### 9.4.0.1 查询没有结果
+
+Scan
方法将返回一个错误: sql.ErrNoRows
+
+```go
+var ErrNoRows = errors.New("sql: no rows in result set")
+```
+
+###### 9.4.0.2 多行数据结果
+
+Scan
方法从结果集中读取第一条记录返回。
+
+#### 9.5 读取多行数据
+
+当您查询多行时,必须使用 query
方法并遍历这些行。
+
+```go
+// data-storage/mysql/selectMultiple/main.go
+
+rows, err := db.Query("SELECT id, firstname, lastname FROM teacher ")
+if err != nil {
+ return nil, err
+}
+```
+
+db.Query
查询将返回一个类型为 *sql.Rows
的元素,它表示由数据库服务器发送的结果流。读取完查询结果后,调用Close
方法,我们将在函数的末尾使用一个 defer
语句来执行它。
+
+```go
+defer rows.Close()
+teachers := make([]Teacher, 0)
+for rows.Next() {
+ // 对结果集的每行数据进行循环迭代
+}
+```
+
+在前面的代码中,您可以看到一个用于遍历行的惯用方法。我们对 rows.Next()
使用 for
循环。其目的是创建一个空的Teacher
切片 (类型为 Teacher
),然后遍历每一行:
+
+```go
+for rows.Next() {
+ teacher := Teacher{}
+ if err := rows.Scan(&teacher.id, &teacher.firstname, &teacher.lastname); err != nil {
+ return nil, err
+ }
+ teachers = append(teachers, teacher)
+}
+```
+
+当没有更多的行数据时,for
循环将停止(因为 rows. next()
将返回false)。Scan
用于获取行数据并设置结构字段:
+
+* for
循环的最后一条指令将把当前的 teacher
变量添加到 teachers
切片中。
+* 在这个循环执行的最后,我们有一个 teachers
切片!
+
+这里是源代码的完整功能,我们必须进行设计:
+
+```go
+func teachers(db *sql.DB) (*[]Teacher, error) {
+ rows, err := db.Query("SELECT id, firstname, lastname FROM teacher ")
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+ teachers := make([]Teacher, 0)
+ for rows.Next() {
+ teacher := Teacher{}
+ if err := rows.Scan(&teacher.id, &teacher.firstname, &teacher.lastname); err != nil {
+ return nil, err
+ }
+ teachers = append(teachers, teacher)
+ }
+ return &teachers, nil
+}
+```
+
+#### 9.6 更新行(多行)
+
+如果想要更新一行(或一次更新几行),必须使用SQL的 update
语句,Exec
方法对于这种操作是最优的。它返回一个 sql.Result
是一个具有两个方法的类型接口:LastInsertId()
和 rowsaffaffected()
,最后一个方法允许检查表中更改了多少行。
+
+让我们举个例子,您想要更新 id为1 的老师名字:
+
+```go
+// data-storage/mysql/update/main.go
+
+res, err := db.Exec("UPDATE teacher SET firstname = ? WHERE id = ?", "Daniel", 1)
+if err != nil {
+ fmt.Println(err)
+ // 查询不成功时,会有一个报错信息
+}
+```
+
+通过 sql.Result
,我们可以使用 RowsAffected()
方法来检查只有一行受到影响:
+
+```go
+affected, err := res.RowsAffected()
+if err != nil {
+ fmt.Println(err)
+ return
+}
+
+if affected != 1 {
+ fmt.Printf("Something went wrong %d rows were affected expected 1\n", affected)
+} else {
+ fmt.Println("Update is a success")
+}
+```
+
+RowsAffected()
也会返回一个 int64
类型数值或者一个错误。
+
+#### 9.7 并发
+
+sql.DB
类型表示一个或多个数据库连接的池,我们可以安全地在并发程序中使用它。
+
+### **10 PostgreSQL 数据库**
+
+正如MySQL, PostgreSQL 在1996年正式版发布,也是一个开源关系型数据库管理系统。
+
+#### 10.1 驱动
+
+我们需要通过一个 PostgreSQL的驱动程序,连接到数据库。
+
+这个例子将使用Go仓库的 Wiki 推荐的驱动程序 https://github.com/lib/pq,它似乎也是一个非常受欢迎的仓库,总计超过4000 stars和70名贡献者。
+
+驱动的文档可以在 godoc 上找到:https://godoc.org/github.com/lib/pq。
+
+#### 10.2 连接
+
+我们将使用 sql
包的 Open
函数:
+
+```go
+// data-storage/postgresql/connection/main.go
+package main
+
+import (
+ "database/sql"
+ "fmt"
+
+ _ "github.com/lib/pq"
+)
+
+func main() {
+ db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=school password=pg123 sslmode=disable")
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ err = db.Ping()
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+```
+
+* 你会注意到 sql.Open
的第一个参数是 "postgres"
。
+* 在 Go程序内部,会根据参数进行匹配对应的驱动程序
+* 而第二个参数是 DSN (数据库名称)
+* 这个字符串是如何连接数据库的作用, 它与 MySQL 的版本有点轻微不同, 我认为更多的是表达上:
+* “host=localhost port=5432 user=postgres dbname=school password=pg123 sslmode=disable”
+
+在实际的应用程序中,您将激活 sslmode
(在这里,为了测试目的,我们禁用了它)。此外,不建议在源代码中存储这些参数。例如,您应该从配置文件的值或环境变量构建 DSN 字符串。
+
+其他有效参数如下:
+
+* connect_timeout
这是为等待连接打开而指定的超时。如果你将它设置 为零
或 为空
,它将无限期地等待。
+* sslcert
,sslkey
,sslrootcert
: SSL 支持参数。
+
+#### 10.3 新建表
+
+PostgreSQL使用SQL语言,但与 MySQL 相比:CREATE TABLE
的语法是不同的。Postgres 类型和 MySQL 类型不一样。
+
+这是相似的建表脚本:
+
+```sql
+CREATE TABLE public.teacher
+(
+ id serial,
+ create_time timestamp without time zone,
+ update_time timestamp without time zone,
+ firstname character varying(255),
+ lastname character varying(255),
+ PRIMARY KEY (id)
+);
+```
+
+部分特别的说明:
+
+* serial
被用来作为主键key
+* 你可以添加时区作为时间戳。(在 MySQL 中,你可以为每一个数据库连接指定一个时区,默认会使用服务器的时区)
+* character varying(255)
被用作存储字符串。
+
+为了新建表,我们会使用 Exec
方法执行建表脚本:
+
+```go
+// load SQL script
+// use os.Open("create_table.sql") to open the script
+// then ioutil.ReadAll to load it's content
+// execute the script
+res, err := db.Exec(string(b))
+if err != nil {
+ fmt.Println(err)
+ return
+}
+// success, table created !
+```
+
+语法类似于 MySQL 数据库中使用的语法。
+
+#### 10.4 插入数据
+
+对于插入(或更新),适合使用 Exec
方法 用于 MySQL 数据库,我们可以对 PostgreSQL 数据库也使用这种方法。但它不是最优的。Exec
将返回一个 Row
,它定义了 LastInsertId
方法。此方法将发出请求
+
+```sql
+SELECT LAST_INSERT_ID();
+```
+
+对于PostgreSQL,这个方法将返回一个错误(若这个函数不存在)。相反,我们可以添加 SQL INSERT 语句查询 RETURNING id
+
+完整的SQL请求如下:
+
+```sql
+INSERT INTO public.teacher (create_time, firstname, lastname)
+VALUES (NOW(),$1, $2)
+RETURNING id;
+```
+
+PostgreSQL 不支持"?"字符作为占位符,你需要使用占位符($1,$2)
+
+我们必须使用 QueryRow
方法,然后调用 Scan
方法获取插入后的 id值:
+
+```go
+// data-storage/postgresql/insert/main.go
+
+func createTeacher(firstname string, lastname string, db *sql.DB) (int, error) {
+ insertedId := 0
+ err := db.QueryRow("INSERT INTO public.teacher (create_time, firstname, lastname) VALUES (NOW(),$1, $2) RETURNING id;", firstname, lastname).Scan(&insertedId)
+ if err != nil {
+ return 0, err
+ }
+ if insertedId == 0 {
+ return 0, errors.New("something went wrong id inserted is equal to zero")
+ }
+ return insertedId, nil
+}
+```
+
+* createTeacher
方法需要三个参数入参(firstname, lastname, 指向 sql.DB
的指针)。
+* 指向变量 InsertedID
的指针将作为参数传递给 Scan
方法。
+* Go 将使用 SQL 查询的结果(即插入的id)更新 insertedId
的值
+
+如果报错,Go 会调用 db.QueryRow(..).Scan(...)
返回一个错误。
+
+#### 10.5 读取一行数据
+
+我们已经创建了一个类型结构体 Teacher ,它将保存与数据库中一个 teacher相关的数据
+
+```go
+type Teacher struct {
+ id int
+ firstname string
+ lastname string
+}
+```
+
+下面是一个在数据库中检索教师的示例函数:
+
+```go
+// data-storage/postgresql/select/main.go
+
+func teacher(id int, db *sql.DB) (*Teacher, error) {
+ teacher := Teacher{}
+ err := db.QueryRow("SELECT id, firstname, lastname FROM teacher WHERE id > $1 ", id).Scan(&teacher.id, &teacher.firstname, &teacher.lastname)
+ if err != nil {
+ return &teacher, err
+ }
+ return &teacher, nil
+}
+```
+
+QueryRow
将从数据库中返回一个单独的数据行:
+
+* 你只会从中获取一行已处理的数据,即使你进行多行数据查询。
+* Scan
函数将从返回数据中提出对应的值。
+
+#### 10.6 读取多行数据
+
+为了读取多行数据,你应该使用 Query
方法:
+
+```go
+// data-storage/postgresql/select-multiple/main.go
+rows, err := db.Query("SELECT id, firstname, lastname FROM teacher")
+```
+
+Query
会返回一个指向 sql.Rows
的指针,或者一个错误。
+
+你必须调用 rows.Close()
释放连接资源:
+
+```go
+// close the connexion at the end of the function
+defer rows.Close()
+```
+
+接着,我们使用 rows.Next()
进行循环迭代:
+
+```go
+for rows.Next() {
+ //这这里处理每行当前行数据
+}
+```
+
+在循环中,我们使用 Scan
方法从当前行中提取数据:
+
+```go
+err := rows.Scan(&id, &firstname, &lastname)
+```
+
+##### 10.6.1 Zoom(扫描方法)
+
+convertAssign
函数(在 sql
包中)会被调用:
+
+```go
+func convertAssign(dest, src interface{}) error {
+ //...
+}
+```
+
+* dest
参数是一个指向终点的变量
+* src
是数据库驱动返回的源参数
+* 此函数会检索 dest
和 src
参数,且此函数超过200+行的源代码。我们只介绍前面部分代码,不进行全局介绍。
+
+```go
+// extract of function convertAssign(提取 convertAssign 函数)
+// (line 209, src/database/sql.go)
+func convertAssign(dest, src interface{}) error {
+ // Common cases, without reflect.
+ switch s := src.(type) {
+ case string:
+ switch d := dest.(type) {
+ case *string:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = s
+ return nil
+ case *[]byte:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = []byte(s)
+ return nil
+ case *RawBytes:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = append((*d)[:0], s...)
+ return nil
+ }
+ //...
+}
+```
+
+函数由一个 switch
语句组成,我们使用了以下语句转换了 src
类型:
+
+```go
+s := src.(type)
+```
+
+如果 src
变量是一个字符串,我们将提取 dest
参数的类型。
+
+```go
+d := dest.(type)
+```
+
+另外启动一个 switch
语句,若 src
是一个字符串,则 dest
是一个 *[]byte
指针,将执行以下小片段代码:
+
+```go
+if d == nil {
+ return errNilPtr
+}
+*d = []byte(s)
+return nil
+```
+
+该函数将测试目标指针是否为空。如果不是,dest
的值将被更改为 []byte(s)
+
+#### 10.7 更新一行(or 多行)数据
+
+像 MySQL 一样,合适的方法是Exec。该方法将返回一个实现 sql.Result 接口的类型。res.RowsAffected
将返回表中受 UPDATE 语句影响的行数,你可以通过控制这个值来控制请求对表的影响:
+
+```go
+// data-storage/postgresql/update/main.go
+
+res, err := db.Exec("UPDATE teacher SET firstname = $1 WHERE id = $2", "Daniel", 1)
+// check that there is no error
+// get the number of affected rows
+affected, err := res.RowsAffected()
+//...
+```
+
+对整个例子上看,与 MySQL 不同的只是格式化的占位符,使用 "$n"
代替 "?"
,n 是SQL语句中对应的参数位置。
+
+### **11 MongoDB**
+
+2007年,MongoDB 由 10gen 公司创建。MongoDB是一个专有系统,是平台即服务应用程序的一部分,由其创建者作为一个开源数据库发布。
+
+MongoDB 是 NoSQL 趋势的一部分,NoSQL 这个词是 Johan Oskarsson 在2009年一次关于数据库的聚会上创造的。
+
+NoSQL 数据库没有严格的定义,但是它们有两个共同的特征:
+
+* 它们是面向文档的(它们存储文档而不是表中的行)
+* 它们被设计成高度可伸缩的
+
+本节将重点介绍基本的 MongoDB 操作(CRUD)。但是首先,让我们学习一些重要的词汇:
+
+#### 11.1 MongoDB 词汇
+
+使用 MongoDB
BSON格式,存储 JSON
格式的字符串;
+
+使用 MongoDB
文档(documents)格式,存储 JSON
的二进制数据。
+
+文档设置(没有规定在集合中存储同类型的文档数据)
+
+集合设置
+
+#### 11.2 驱动
+
+要连接使用我们的数据库,可以使用 MongoDB 的官方驱动程序:github.com/mongodb/mongo-go-driver。
+
+#### 11.3 连接
+
+```go
+// data-storage/mongodb/official-driver/connection/main.go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/mongodb/mongo-go-driver/mongo"
+)
+
+func main() {
+ client, err := mongo.NewClient(`mongodb://username:password@localhost:27094/db`)
+ if err != nil {
+ panic(err)
+ }
+ //...
+
+}
+```
+
+我们通过数据库连接的字符串,使用函数 NewClient
的方式,新建一个 mongo 客户端,其如下格式:
+
+```go
+mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]
+```
+
+您可以指定多个主机创建客户端。 请注意,我在例子中指定了连接字符串末尾的数据库,这不是强制性的。对于某些云数据库提供商,它必须在连接字符串中指定数据库以进行身份认证。 这些云提供商共享其客户端之间的 MongoDB 服务器,并通过数据库处理身份验证。
+
+创建客户端之后,我们将连接到数据库:
+
+```go
+ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+defer cancel()
+err = client.Connect(ctx)
+if err != nil {
+ panic(err)
+}
+```
+
+我们在以前代码片段中所做的第一件事是为连接创建context,context 是标准 Go Package(我专门为此包的章节),它将定义连接的特定超时(此处3秒)。 如果程序没有成功进行连接,则会将通过以下信息进行 panic
:
+
+```go
+panic: context deadline exceeded
+```
+
+连接到数据库之后,可以使用以下代码片段 ping 服务器
+
+```go
+err = client.Ping(ctx, nil)
+if err != nil {
+ panic(err)
+}
+```
+
+#### 11.4 新建一个文档
+
+数据存储在文档中,文档是JSON字符串,文档存储在集合中, 因此,第一步是在数据库中检索集合:
+
+```go
+collection := client.Database("db").Collection("school")
+```
+
+注意,如果集合不存在,它将被自动创建。
+
+```go
+// data-storage/mongodb/official-driver/create/main.go
+
+res, err := collection.InsertOne(context.Background(), bson.M{"hello": "world"})
+if err != nil {
+ panic(err)
+}
+id := res.InsertedID
+fmt.Println(id)
+```
+
+这里我们使用 InsertOne
方法,该方法将 context 作为其第一个参数(这里同样可以定义一个超时参数),将文档作为第二个参数。此包没有强制使用指定的类型作为第二个参数。
+
+```go
+func (coll *Collection) InsertOne(ctx context.Context, document interface{}, opts...*options.InsertOneOptions) (*InsertOneResult, error)
+//第二个参数类型是空接口类型
+```
+
+在例子中,可以成功调用该函数:
+
+```go
+bson.M{"firstname":"John", "lastname":"Doe", "create_time":time.Now()}
+```
+
+bson
是驱动程序的一个子包,M类型是 map[string]interface{}
的类型别名。下面是插入到数据库中的文档
+
+```json
+{
+ "_id": {
+ "$oid": "5c0919b285d8ae1a8afe6c80"
+ },
+ "lastname": "Doe",
+ "create_time": {
+ "$date": "2018-12-06T12:44:34.168Z"
+ },
+ "firstname": "John"
+}
+```
+
+MongoDB 生成 id,然后您可以看到 date 属性被专门处理(稍后在过滤器中使用)。
+
+#### 11.5 读取一个文档
+
+###### 11.5.0.1 属性查询
+
+检索文档中的一个属性,我们新建一个过滤器变量:
+
+```go
+filter := bson.D{{"firstname", "Jeanne"}}
+```
+
+bson
是 Go MongoDB Driver的子包,它作为实用工具对 BSON
进行解析和生成。bson.D
类型表示一个BSON文档(D代表文档)。内部,bson.D
作为一个元素,是 primitive.E
的切片, primitive.E
带有两个公有字段Key(字符串类型)和Value(空类型接口)。
+
+这里,我们过滤一个属性 **firstname** 设置为 **“Jeanne”**的文档,我们定义一个可以存储结果的变量:
+
+```go
+type Teacher struct {
+ Firstname string
+ Lastname string
+}
+```
+
+接着,我们新建一个类型为 Teacher
的结构体赋值给查询结果变量:
+
+```go
+result := Teacher{}
+```
+
+然后,我们用集合的实例查询数据库:
+
+```go
+// data-storage/mongodb/official-driver/read/main.go
+
+collection := client.Database(databaseName).Collection(collectionName)
+err = collection.FindOne(context.Background(), filter).Decode(&result)
+if err != nil {
+ panic(err)
+}
+// One document was found
+fmt.Printf("%+v", result)
+```
+
+* 首先,检索集合(我们查询数据库,然后收集起来)
+* 数据库命名和集合命名保存在里面的变量中
+* 使用 FindOne
方法,服务器仅会返回一个结果,匹配提供的过滤器。而此方法需要两个参数:
+ * context类型参数
+ * filter
+* 使用 FindOne
方法返回一个指向 SingleResult
元素类型的指针。
+* 然后我们直接调用 SingleResult
方法。(我们可以用两个不同的步骤来完成,但是这样代码更简单)
+
+如果没有结果怎么办?在这种情况下,Decode 将返回一个错误,我们的程序会 panic
:
+
+```go
+panic: mongo: no documents in result
+```
+
+###### 11.5.0.1 文档 id 查询
+
+MongoDB 中的每个文档都有一个 property_id,我们可以将它与关系数据库引擎中的主键进行比较。
+
+这个字段有一个特殊的类型,叫做 ObjectId
,它有以下属性:
+
+* 由 12 个字节组成
+* 由 4 个部分组成(从UNIX时代以来的秒数、机器标识符、进程id和计数器)
+* 由客户端生成
+
+在构建查询之前,必须创建一个 ObjectId
类型的变量
+
+我们正在寻找 id="5c091d3b734016209db89f76"
的文档:
+
+```go
+// data-storage/mongodb/official-driver/readById/main.go
+
+import "github.com/mongodb/mongo-go-driver/bson/objectid"
+
+//...
+oid , err := objectid.FromHex("5c091d3b734016209db89f76")
+if err != nil {
+ panic(err)
+}
+```
+
+变量 oid
可以在查询中使用,让我们创建过滤器变量来检索文档:
+
+```go
+filter := bson.M{"_id": oid}
+```
+
+接着,我们调用 FindOne
方法:
+
+```go
+result := Teacher{}
+err = collection.FindOne(context.Background(), filter).Decode(&result)
+if err != nil {
+ panic(err)
+}
+fmt.Printf("%+v", result)
+// {Id:ObjectID("5c091d3b734016209db89f76") Firstname:Jeannoti Lastname:Doe CreateTime:2018-12-06 13:59:39.338 +0100 CET}
+```
+
+我们创建一个全新的 Teacher
类型变量来保存查询结果。
+
+结果集的属性通过Decode方法进行解析绑定(我们向 Decode
传递一个指向变量 result
的指针)。
+
+###### 11.5.0.3 查询选择器
+
+MongoDB 查询包含一个类似 json 的对象。要构建查询,必须使用查询选择器。这些选择器以美元符号开始,例如 **$eq** 表示等于,**$neq** 表示不等于。本节将回顾,然后,我们将构建一个查询。
+
+**比较选择器**
+
+| 选择器 | 定义 | 查询示例 | 解释描述 |
+| ------ | --------------------------------- | --------------------------------- | ---------------------------------- |
+| $eq | equal(等于) | { firstname: { $eq: John } } | firstname属性值等于 John |
+| $ne | not equal(不等于) | { lastname: { $ne: Doe } } | lastname属性值不等于 Doe |
+| $gt | greater than(大于) | { age: { $gt: 12 } } | age属性值严格大于12 |
+| $gte | greater than or equal(大于等于) | { age: { $gte: 42 } } | age属性值大于或等于42 |
+| $lt | less than(小于) | { monthlyIncome: { $lt: 1000 } } | monthlyIncome属性值小于 1000 |
+| $lte | less than or equal(小于等于) | { monthlyIncome: { $lte: 1000 } } | monthlyIncome属性值小于或等于 1000 |
+| $in | in array (在数组内) | { age: { $in: [20,42] } } | age属性值等于 20 或 42 |
+| $nin | not in array(不在数组内) | { age: { $nin: [20,30] } } | age属性值不等于 20 或 3 |
+
+**逻辑选择器**
+
+| 选择器 | 定义 | 查询示例 | 解释描述 |
+| ------ | ------------- | ------------------------------------------------------------ | ------------------------------------------- |
+| $and | AND(且) | { $and: [{ age: { $gt: 60 } },{ firstname: { $eq: John} } ] } | age属性值大于60 且 firstname属性值等于 John |
+| $or | OR(或) | { $or: [{ age: { $gt: 12 } }, { age: { $lt: 3 } }] } | age属性值大于 12 或 小于 3 |
+| $nor | NOR(异或) | { $nor: [{ age: { $gt: 12 } },{ age: { $lt: 3 } } ] } | age属性值不大于2 异或小于且不小于3 |
+| $not | NOT(异或非) | { $not: [{ age: { $gt: 12 } }] } | age属性值不大于 12 |
+
+让我们为学校集合创建一个查询,我们想检索将 John
作为 FirstName
和 Doe
作为 LastName
的所有老师:
+
+```json
+{
+ "$and": [{
+ "firstname": {
+ "$eq": "John"
+ }
+ },
+ {
+ "lastname": {
+ "$eq": "Doe"
+ }
+ }
+ ]
+}
+```
+
+这里我们使用 $and
逻辑选择,$and的值是一个查询子句的数组。
+
+这里的数组有两个元素:2个查询将由一个逻辑 AND 连接起来。我们查询 firstname的值等于 John 并且 lastname 的值等于 Doe的老师。
+
+```go
+// data-storage/mongodb/official-driver/query-advanced/main.go
+
+//..
+
+q1 := bson.M{"firstname": bson.M{"$eq": "John"}}
+q2 := bson.M{"lastname": bson.M{"$eq": "Doe"}}
+```
+
+q1
和 q2
是 以 string 为 key 、空接口类型为 value 的 map 结构的 bson.M
类型数据。
+
+开始分析 q1
变量:"firstname"
作为 key 同时 value 是以 "$eq"
为 key、"John"
为值的 bson.M
类型数据。要加入到两个查询,需要先创建一个 bson.A
类型的 BSON
数组(代替数组类型):
+
+```go
+clauses := bson.A{q1, q2}
+```
+
+接着, 使用 $and
选择器定义主查询,然后作为 bson.M
类型的一个元素:
+
+```go
+filter := bson.M{"$and":clauses }
+```
+
+发送查询请求后,可以通过使用 **Collection** 类型的Find
方法:
+
+```go
+cur, err := collection.Find(context.Background(), filter)
+if err != nil {
+ panic(err)
+}
+```
+
+常见, Find
方法会返回一个 Cursor
游标或者报错信息,Cursor
是一个指向查询结果集的指针,我们可以通过使用 Next
方法循环迭代 Cursor
,此方法执行后,如果下一行记录存在且没有错误,会返回一个布尔类型的值 true
+
+每个数据结果代表一个文档,通过调用 DecodeBytes
获取文档数据:
+
+```go
+defer cur.Close(context.Background())
+for cur.Next(context.Background()) {
+ raw, err := cur.DecodeBytes()
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("%s\n", raw)
+}
+if err := cur.Err(); err != nil {
+ panic(err)
+}
+```
+
+当函数返回(returns)时,首先需要调用 **defer** cur.Close(context.Background())
关闭 cursors
。在此之前,通过 for
循环,迭代指针游标(cursor
)。在循环中,检索每个 raw 的文档值,将它赋值给 raw
变量。
+
+在出错情况下,for
循环会停止, 下一个语句会继续执行。而我们会通过调用 Err
方法,获取错误信息,在这种情况程序会进行 panic
。
+
+####### 11.5.0.3.1 关于MongoDB cursors 的说明
+
+Cursors
会出现超时,在一定的非 活动期间,意味着如果对结果集进行长时间迭代循环,会导致 cursor
在服务端超时关闭。在查询过程中,你可以指定 cursor
的类型,它主要有以下两种:
+
+* 可跟踪的:此类 cursor
可以保持打开状态,甚至循环到最后一条数据后,仍然可以重新使用起来。
+* 不可跟踪的:当循环到最后一条数据后,此类 cursor
会自动关闭。
+
+默认是不可跟踪的 cursor
。
+
+#### 11.6 更新文档
+
+在 MongoDB 中更新文档,你可以使用其提供的两个 BSON
对象:
+
+* 查询在更新的时候会查找文档(documents),因为它是过滤器
+* 需要更新时,查找到的文档(document)会作为数据被更新。
+
+举个例子,我们需要更新一个文档,这个文档的属性名 **firstname**
的值为 John
,同时更新属性名 **lastname**
的值为 CoffeeBean
。
+
+过滤器:
+
+```json
+{
+ "firstname": {
+ "$eq": "John"
+}
+```
+
+简化查询后:
+
+```json
+{"firstname":"John"}
+```
+
+更新如下:
+
+```json
+{
+ "$set": {
+ "lastname": "CoffeeBean"
+}
+```
+
+接下来,创建 2 个 BSON
对象:
+
+```go
+// data-storage/mongodb/official-driver/update/main.go
+//...
+filter := bson.M{"firstname": "John"}
+update := bson.M{"$set": bson.M{"lastname": "CoffeeBean"}}
+```
+
+我们将使用 FindOneAndUpdate
方法,将 context
类型变量,bson
类型过滤变量,还有需要更新的 bson
,如下:
+
+```go
+res := collection.FindOneAndUpdate(context.Background(), filter, update)
+```
+
+这个方法会返回一个指向 DocumentResult
类型的指针,我们通过 Decode
方法解析返回的文档(document)。如果过滤器(filter)没有返回文档数据,则也没有数据被更新,也就没有文档数据会被返回。这种情况下,会返回一个错误信息:
+
+```go
+resDecoded := Teacher{}
+err = res.Decode(&resDecoded)
+if err != nil {
+ panic(err)
+}
+fmt.Printf("%+v", resDecoded)
+```
+
+程序将输出:
+
+```json
+{Id:ObjectID("5c091d3b734016209db89f76") Firstname:John Lastname:CoffeeBean CreateTime:2018-12-06 23:59:39.338 +0100 CET}
+```
+
+### **12 ElasticSearch **
+
+ElasticSearch 是一款开源搜索引擎软件,由 **Shay Banon** 开发,其第一个版本在2010年进行发布。2018年,在 **Stackoverflow**开发者调查中,ElasticSearch 跻身前10名最受欢迎的数据库。
+
+ElasticSearch主要用于开发搜索引擎。它是用 Java 开发的,基于Apache Lucene。Apache Lucene是由Apache软件基金会开发的,是一个完全用 Java 编写的高性能、全功能的文本搜索引擎库。这种技术几乎适用于任何需要全文搜索的应用程序,特别是跨平台的应用程序。ElasticSearch被设计为易于扩展,并且它公开了一个REST接口来执行CRUD操作。
+
+搜索引擎的基本单位不是一行而是一个文档。弹性搜索文档是一个 JSON 对象。每个文档的结构不是固定的。它是一个无模式数据库。
+
+在本节中,我们将在 MIT 许可证下使用客户端库 **https://github.com/olivere/elastic**
+
+#### 12.1 词汇
+
+定义一些重要概念:
+
+**Indexing**
+
+将数据存储到ElasticSearch中的动作。数据被组织成文档。
+
+**Document**
+
+表示数据的 JSON
对象。一个文档有一个属于其对应的索引(index)
+
+**Index**
+
+ElasticSearch集群可以包含多个索引(索引的复数 indces)。从关系的角度来看,索引可以看作是一个表。
+
+**Type**
+
+ElasticSearch中存储的每个文档都有一个特定的类型。如果您想存储教师的名字、姓氏和到校日期,您将创建一个类型为teacher的文档。
+
+**Field**
+
+类型由字段组成。 例如,类型教师将拥有FightName和LastName的字段
+
+###### 12.1.0.1废弃类型(Type)
+
+请注意,类型在已弃用的过程中。 在未来的 Elasticsearch API 中将去掉类型的概念。 如果您想了解更多有关该弃用的信息,请参阅 https://www.elastic.co/guide/en/elasticsearch/reference/current/removal-of-types.html
+
+我们将遵循推荐的方法,是创建一个有字段类型的类型文档
+
+#### 12.2 连接:初始化客户端
+
+ElasticSearch 被设计为在集群服务器上运行的软件。服务器称为节点,集群有一个主节点,主节点不固定,主容量可以归属于其他节点。Go 客户端被设计为自动检测新节点并删除集群中不再使用的节点。
+
+要创建客户端,你必须传递集群节点的地址(主机+端口):
+
+```go
+// data-storage/elasticsearch/create-index/client/main.go
+//...
+client, err := elastic.NewClient(elastic.SetURL("http://127.0.0.1:9200"))
+```
+
+如果你的集群中有其他节点,只需将其他地址添加到 SetURL 函数即可:
+
+```go
+client, err := elastic.NewClient(elastic.SetURL("http://127.0.0.1:9200", "http://127.0.0.1:9300"))
+```
+
+这个客户端被设计成可以在整个应用程序中使用,有了这个客户端,您可以执行 CRUD 操作。注意,您也可以用一个简单的 HTTP 请求来执行这些操作。
+
+#### 12.3 新建一个索引
+
+在存储任何文档之前,我们必须创建索引。ElasticSearch 中的文档存储在索引中,索引具有一个用字段定义类型的映射。我们将命名我们的索引“学校”。
+
+首先需要定义映射:
+
+```go
+const Mapping = `{
+ "mappings": {
+ "_doc": {
+ "properties": {
+ "type": { "type": "keyword" },
+ "firstname": { "type": "text" },
+ "lastname": { "type": "keyword" },
+ "create_time": { "type": "date" },
+ "update_time": { "type": "date" }
+ }
+ }
+ }
+}`
+```
+
+##### 12.3.1 使用 Http 请求
+
+要创建索引,你必须发出一个 http 请求
+
+```http
+PUT http://127.0.0.1:9200/{indexName}
+{
+"mappings" : {
+ //...
+ }
+}
+```
+
+要使用 Go 发出请求,可以使用以下代码片段:
+
+```go
+// data-storage/elasticsearch/create-index/rest/main.go
+//...
+client := &http.Client{}
+
+// prepare the request
+request, err := http.NewRequest("PUT", "http://127.0.0.1:9200/school",strings.NewReader(Mapping))
+request.Header.Add("Content-Type", "application/json")
+
+// execute the request
+response, err := client.Do(request)
+ if err != nil {
+ log.Fatal(err)
+ } else {
+ defer response.Body.Close()
+ contents, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s", contents)
+ }
+}
+```
+
+它将输出内容如下:
+
+```json
+{"acknowledged":true,"shards_acknowledged":true,"index":"school"}
+```
+
+##### 12.3.2 使用 Go 客户端
+
+```go
+// data-storage/elasticsearch/create-index/client/main.go
+//...
+
+createIndex, err := client.CreateIndex("school").BodyString(Mapping).Do(ctx)
+if err != nil {
+ // Handle error
+ fmt.Println(err)
+ return
+}
+if !createIndex.Acknowledged {
+ // Not acknowledged
+ fmt.Println("not acknoledge !")
+ return
+}
+fmt.Println("OK")
+```
+
+#### 12.4 插入/更新文档
+
+使用 ElasticSearch,创建文档的端点也是更新文档的端点。
+
+##### 12.4.1 使用 Http 的 Put 方式请求
+
+要创建文档,你只需在URL ""http://127.0.0.1:9200/{indexName}/{typeName}/{id}""
中提出请求,其中 **{id}** 是你想要在索引 **{indexName}** 添加的文档 ID。
+
+例子如下:
+
+```http
+PUT http://127.0.0.1:9200/school/_doc/42
+{
+ "type":"teacher",
+ "firstname":"John",
+ "lastname":"Doe",
+ "create_time":"2018-09-10 00:00:00"
+}
+```
+
+使用以下代码块执行插入
+
+```go
+// data-storage/elasticsearch/insert/rest/main.go
+
+//...
+
+const NewTeacher = `
+{
+ "type":"teacher",
+ "firstname":"John",
+ "lastname":"Doe",
+ "create_time":"2018-09-10T00:00:00"
+}`
+
+const ClusterUrl = "http://127.0.0.1:9200"
+const IndexName = "school"
+const Username = "myUsername"
+const Password = "myPassword"
+const TypeName = "_doc"
+const ObjectId = 42
+
+func main() {
+
+ url := fmt.Sprintf("%s/%s/%s/%d", ClusterUrl, IndexName, TypeName, ObjectId)
+ client := &http.Client{}
+ request, err := http.NewRequest("PUT", url, strings.NewReader(NewTeacher))
+ request.Header.Add("Content-Type", "application/json")
+ // if your cluster requires authentification
+ token := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", Username, Password)))
+ request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token))
+
+ response, err := client.Do(request)
+ if err != nil {
+ log.Fatal(err)
+ } else {
+ defer response.Body.Close()
+ contents, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s", contents)
+ }
+}
+```
+
+以上程序的代码将输出一个 JSON
对象:
+
+```json
+{
+ "_index": "school",
+ "_type": "_doc",
+ "_id": "42",
+ "_version": 1,
+ "result": "created",
+ "_shards": {
+ "total": 2,
+ "successful": 2,
+ "failed": 0
+ },
+ "_seq_no": 3,
+ "_primary_term": 1
+}
+```
+
+可以看到我们的文档已经成功地在两个碎片上创建,文档版本号为1。你将看到,每次更新文档时,此属性都会增加。
+
+##### 12.4.2 使用 Go 客户端
+
+首先,新建一个 Teacher
结构体
+
+```go
+// data-storage/elasticsearch/insert/client/main.go
+
+type Teacher struct {
+ Firstname string `json:"Firstname"`
+ Lastname string `json:"Lastname"`
+ CreateTime time.Time `json:"create_time"`
+ UpdateTime time.Time `json:"update_time"`
+}
+```
+
+你可以新建一个元素,插入到 ElasticSearch:
+
+```go
+teacher := Teacher{firstname: "John", lastname: "Doe", createTime: time.Now()}
+```
+
+客户端的 API 非常简单:
+
+```go
+put, err := client.Index().
+ Index("school").
+ Type("_doc").
+ Id("42").
+ BodyJson(teacher).
+ Do(ctx)
+if err != nil {
+ // Handle error
+ panic(err)
+}
+```
+
+你必须用 BodyJson
方法精确索引名称、类型、Id以及最后的 JSON文档
+
+put变量的类型是 elastic.IndexResponse
,我们可以稍后检查文档是否已经创建或更新:
+
+```go
+if put.Result == "updated" {
+ fmt.Println("document has been updated")
+ fmt.Printf("version N. : %d\n", put.Version)
+}
+if put.Result == "created" {
+ fmt.Println("document has been created")
+}
+```
+
+#### 12.5 通过 Id 读取文档
+
+要获得具有 id 的文档,你必须发送一个 GET 类型的 HTTP 请求。例如,如果您想在索引”学校“中检索 id 为 42 的 type_doc 文档,你必须发起以下请求:
+
+```http
+GET http://127.0.0.1:9200/school/_doc/42
+```
+
+##### 12.5.1 其他
+
+首先,构建一个 URL:
+
+```go
+url := fmt.Sprintf("%s/%s/%s/%d?pretty=true", ClusterUrl, IndexName, TypeName, ObjectId)
+```
+
+这里我们使用 Sprintf
来生成请求URL, ClusterUrl
、IndexName
、TypeName
、ObjectId
是在文件开头定义的常量。请注意,我们在请求中添加了参数 ?pretty=true
,以通知服务器我们希望返回缩进的JSON结果
+
+接着,我们开始新建一个标准的 Http 客户端,然后构建请求:
+
+```go
+// data-storage/elasticsearch/read/by-id/rest/main.go
+//...
+
+client := &http.Client{}
+request, err := http.NewRequest("GET", url, nil)
+// add the authorization header
+request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token))
+```
+
+下一步,发送请求并获得返回结果:
+
+```go
+response, err := client.Do(request)
+if err != nil {
+ log.Fatal(err)
+} else {
+ defer response.Body.Close()
+ contents, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Printf("%s", contents)
+}
+```
+
+上面代码片段将输出:
+
+```go
+{
+ "_index" : "school",
+ "_type" : "_doc",
+ "_id" : "42",
+ "_version" : 3,
+ "found" : true,
+ "_source" : {
+ "type" : "teacher",
+ "firstname" : "John",
+ "lastname" : "Doe",
+ "create_time" : "2018-09-10T00:00:00"
+ }
+}
+```
+
+然后,您可以解析这个 JSON 字符串(使用 JSON . unmarshall
)并提取 found
的布尔属性的值,并对其进行测试,以告知用户您的应用程序已经检索到一个结果
+
+#### 12.5.2 Go 客户端
+
+你可以使用 GET
方法:
+
+```go
+// data-storage/elasticsearch/read/by-id/client/main.go
+//...
+get, err := client.Get().
+ Index(IndexName).
+ Type(TypeName).
+ Id(ObjectId).
+ Do(ctx)
+if err != nil {
+ // error
+ // index not found
+ // document with this id not found
+ // ...
+ panic(err)
+}
+if get.Found {
+ // document was found
+}
+```
+
+这里有一个 *elastic.GetResult
类型的元素将被返回。 Found
属性允许你检查是否检索了文档,还可以获得版本(Version)、Id 等
+
+#### 12.6 搜索
+
+ElasticSearch 是一个很好的搜索引擎, 我们可以使用其余的 API 对索引执行搜索操作。我们的目标不是涵盖所有的引擎搜索功能,而是向你展示如何让它与 Go 一起工作。让我们开始吧!
+
+##### 12.6.1 基本请求
+
+ElasticSearch 公布了一个为搜索而设计的 REST GET端点。例如,如果你想找到所有名字等于 John
的老师,我们可以使用下面的请求:
+
+```
+GET school/_search
+{
+ "query": {
+ "bool": {
+ "must": {
+ "match": {
+ "firstname": "Danny"
+ }
+ },
+ "filter": {
+ "match": {
+ "type": "teacher"
+ }
+ }
+ }
+ }
+}
+```
+
+##### 12.6.2 其他
+
+我们已经创建了常量 SearchJSON
,它将保存指向服务器的 JSON 查询。在实际情况中,你可能希望让用户来发起一个请求情况,但是要小心潜在的查询注入!
+
+```go
+// data-storage/elasticsearch/read/search/rest/main.go
+// ...
+
+url := fmt.Sprintf("%s/_search?pretty=true", ClusterUrl)
+client := &http.Client{}
+request, err := http.NewRequest("GET", url, strings.NewReader(SearchJSON))
+request.Header.Add("Content-Type", "application/json")
+request.Header.Add("Authorization", fmt.Sprintf("Basic %s", token))
+```
+
+接着,我们发起一个请求,准确来说前面小节的代码...
+
+如果没有返回结果,ElasticSearch 会返回 JSON 格式的结果,如下所示:
+
+```json
+{
+ "took" : 1,
+ "timed_out" : false,
+ "_shards" : {
+ "total" : 4,
+ "successful" : 4,
+ "skipped" : 0,
+
+ "failed" : 0
+ },
+ "hits" : {
+ "total" : 0,
+ "max_score" : null,
+ "hits" : [ ]
+ }
+}
+```
+
+如果你一个或多个结果返回,你必须处理以下 JSON:
+
+```json
+{
+ "took" : 2,
+ "timed_out" : false,
+ "_shards" : {
+ "total" : 4,
+ "successful" : 4,
+ "skipped" : 0,
+ "failed" : 0
+ },
+ "hits" : {
+ "total" : 1,
+
+ "max_score" : 0.9808292,
+ "hits" : [
+ {
+ "_index" : "school",
+ "_type" : "_doc",
+ "_id" : "72",
+ "_score" : 0.9808292,
+ "_source" : {
+ "type" : "teacher",
+ "firstname" : "Danny",
+ "lastname" : "Doe",
+ "create_time" : "2018-09-10T00:00:00"
+ }
+ }
+ ]
+ }
+}
+```
+
+##### 12.6.3 Go 客户端
+
+Go 客户端有很多方法来处理对 ElasticSearch 的查询,但也有聚合。它的 API 非常丰富。因此,本节将不深入讨论细节。
+
+下面是如何构建一个查询的示例,该查询将返回 **firstname** 属性为 Danny 的所有文档:
+
+```go
+// data-storage/elasticsearch/read/search/client/main.go
+// ...
+
+q := elastic.NewBoolQuery()
+q.Must(elastic.NewMatchQuery("firstname", "Danny"))
+```
+
+然后可以将此查询传递到客户端
+
+```go
+res, err := client.Search().
+ Index(IndexName). // search in index "twitter"
+ Query(q).
+ Pretty(true). // pretty print request and response JSON
+ Do(ctx) // execute
+if err != nil {
+ // do something when an error is raised
+}
+// Success! the query returned 0 or more results
+```
+
+res 变量是 *elastic.SearchResult
指针,该类型具有有趣的属性:
+
+int64
类型的请求耗时
+
+ *elastic.SearchHits
指针类型的结果集
+
+如果查询超时,布尔值将被赋值 true
值
+
+看看,如何使用 res.Hits :
+
+```go
+if res.Hits.TotalHits > 0 {
+ fmt.Printf("Found %d hits\n", res.Hits.TotalHits)
+ // Iterate through results
+ for _, hit := range res.Hits.Hits {
+ var t Teacher
+ err := json.Unmarshal(*hit.Source, &t)
+ if err != nil {
+ panic("impossible to deserialize")
+ }
+ // 享受你的最新 Teacher 实例吧!
+ fmt.Printf("Teacher found firsname %s - lastname : %s\n", t.firstname, t.lastname)
+ }
+} else {
+ // No results
+}
+```
+
+首先检查 TotalHits 属性是否大于零,如果是真的,那就值得检验一下结果
+
+然后使用 for 循环遍历 Hits,它是 \*SearchHit
指针类型元素的一个片段。对于 \*hit.Source
我们可以访问由服务器返回的 **_source** json属性:
+
+```json
+//...
+"_source" : {
+ "type" : "teacher",
+ "firstname" : "John",
+ "lastname" : "Doe",
+ "create_time" : "2018-09-10T00:00:00"
+}
+//...
+```
+
+我们使用 JSON.unmarshal
将 JSON 字符串转换为 Teacher
类型的变量。变量 t 可以在之后被程序使用。
+
+### 13 自我测试
+
+#### 13.1 问题
+
+1. 什么是文件模式?
+2. 如何新建一个 SQL 数据库连接?
+3. 在SQL数据库中,你可以使用哪个方法执行 INSERT 和 UPDATE 的查询构造?
+4. 你可以使用哪个方法来解析 SQL请求的结果集?
+
+#### 13.2 答案
+
+ 1. 什么是文件模式?
+ 1. 文件模式说明权限的设置与文件相关联的。
+ 2. 如何新建一个 SQL 数据库连接?
+ 1. 安装匹配的驱动(driver)
+ 2. 调用 sql.Open
方法
+ 3. 在SQL数据库中,你可以使用哪个方法执行 INSERT 和 UPDATE 的查询构造?
+ 1. Exec
+ 4. 你可以使用哪个方法来解析 SQL请求的结果集?
+ 1. Scan
+
+### 14 关键点
+
+* 使用 os.Create
方法新建一个文件
+* 使用 ioutil.WriteFile
方法新建一个文件并写入
+* 在 UNIX 系统中,每个文件都有设置权限
+* 你可以通过 os.Chmod
函数改变文件权限
+* 安装数据库驱动后,就可以在程序中使用 SQL 数据库了。
+* 不要忘了匿名导入你的 SQL 驱动
+ * sql.Open
可以连接一个数据库
+ * 它将返回一个指向 sql.DB
指针,并拥有以下方法
+ * Exec
:执行原生查询(inserts、update、。。。)
+ * QueryRow
:查询单行数据
+ * Query
:查询多行数据
+ * 你可以使用 Scan
方法将结果集赋值给变量
+* 你还可以使用 **MongoDB** 和 **ElasticSearch** 数据库来扩大开源客户端。
+
+------
+
+1. 八进制是一种以8为基数的数字系统 (如十进制十六进制或二进制)。八进制中的每一个数字都等于0、1、2、3、4、5、6或7
+2. 资源:https://en.wikipedia.org/wiki/Data_buffer
+3. 访问:https://www.postgresql.org/docs/9.0/libpq-ssl.html
+4. NoSQL 表示不仅仅只有 SQL
\ No newline at end of file