Go语言MySQL驱动程序GitHub wiki中文翻译

本文翻译自https://github.com/go-sql-driver/mysql/wiki

2023/03/05

例子

旧密码(old_passwords)

测试(Testing)

开发思路

例子

database/sql包的详细介绍可以在这里找到:http://go-database-sql.org/

浅谈sql.Open函数

首先,你应该了解sql.DB不是一个连接。当你使用sql.Open()时,你将获得数据库的句柄。database/sql包在后台管理一个连接池,并且在你需要它们之前不会打开任何连接。因此sql.Open()不直接打开连接。因此,如果服务器不可用或连接数据的(用户名,密码)不正确,sql.Open()不会返回一个错误。如果你想在进行查询之前检查是否成功连接到数据库(例如在应用程序启动时),你可以使用db.Ping()

db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
    panic(err.Error()) // 此处只是示例,在实际开发中你应该使用合适的错误处理函数,而不是使用panic
}
defer db.Close()

// 上面的Open函数不真的打开一个连接。可以使用以下函数来验证连接字符串是否能成功连接到数据库
err = db.Ping()
if err != nil {
    panic(err.Error()) // 在实际开发中不要使用painc,而是使用合适的错误处理函数
}

// 以下省略执行查询语句等代码
[...]

预处理语句

假设一个具有以下结构的空表,表名是squareNum:

+--------------+---------+------+-----+---------+-------+
| Field        | Type    | Null | Key | Default | Extra |
+--------------+---------+------+-----+---------+-------+
| number       | int(11) | NO   | PRI | NULL    |       |
| squareNumber | int(11) | NO   |     | NULL    |       |
+--------------+---------+------+-----+---------+-------+

在此示例中,我们准备了两条语句,一条用于插入元组(行),一条用于查询。

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, err := sql.Open("mysql", "user:password@/database")
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

// 用于插入数据的预处理语句
	stmtIns, err := db.Prepare("INSERT INTO squareNum VALUES( ?, ? )") // ?号是占位符
	if err != nil {
		panic(err.Error())
	}
	defer stmtIns.Close() // 当main函数快要运行完毕(程序终止)时,关闭预处理语句,释放占用的资源

	// 用于查询数据的预处理语句
	stmtOut, err := db.Prepare("SELECT squareNumber FROM squarenum WHERE number = ?")
	if err != nil {
		panic(err.Error())
	}
	defer stmtOut.Close()

	// 插入0到24数字到数据库
	for i := 0; i < 25; i++ {
		_, err = stmtIns.Exec(i, (i * i)) // 插入元组(i, i^2)
		if err != nil {
			panic(err.Error())
		}
	}

	var squareNum int // 我们在此处浏览结果

// 查询13的平方数
	err = stmtOut.QueryRow(13).Scan(&squareNum) // WHERE number = 13
	if err != nil {
		panic(err.Error())
	}
	fmt.Printf("The square number of 13 is: %d", squareNum)

	// 查询1的平方数试试
	err = stmtOut.QueryRow(1).Scan(&squareNum) // WHERE number = 1
	if err != nil {
		panic(err.Error())
	}
	fmt.Printf("The square number of 1 is: %d", squareNum)
}

忽略NULL值

也许你已经遇到了这个错误:sql: Scan error on column index 1: unsupported driver -> Scan pair: <nil> -> *string

通常在这种情况下你会使用sql.NullString。但有时你并不关心该值是否为NULL,你只想将其视为一个空字符串。

你可以使用一个变通方法来做到这一点,这利用了一个事实,即nil []byte被转换为空字符串。你可以简单地使用*[]byte(或*sql.RawBytes),而不是使用*string作为rows.Scan(...)的目标,因为它们可以接收nil值:

[...]
var col1, col2 []byte

for rows.Next() {
	// 用[]byte类型的变量作为Scan函数的出参
	err = rows.Scan(&col1, &col2)

	if err != nil {
		panic(err.Error())
	}

	// 再转换为字符串,用于输出
	fmt.Println(string(col1), string(col2))
}

RawBytes

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql"
)

func main() {
	// 打开数据库连接
	db, err := sql.Open("mysql", "user:password@/dbname")
	if err != nil {
		panic(err.Error())
	}
	defer db.Close()

	// 执行查询
	rows, err := db.Query("SELECT * FROM table")
	if err != nil {
		panic(err.Error())
	}

	// 获取列名
	columns, err := rows.Columns()
	if err != nil {
		panic(err.Error())
	}

	// 创建一个切片用于存储一行数据
	values := make([]sql.RawBytes, len(columns))

	// rows.Scan想要'[]interface{}'作为一个参数, 所以我们必须把值复制到这样一个切片里。更多细节参考http://code.google.com/p/go-wiki/wiki/InterfaceSlice
	scanArgs := make([]interface{}, len(values))
	for i := range values {
		scanArgs[i] = &values[i]
	}

	// 获取行数据
	for rows.Next() {
		// 从数据中获取RawBytes
		err = rows.Scan(scanArgs...)
		if err != nil {
			panic(err.Error())
		}

		// 现在使用获取到的数据做点事情,此处我们仅仅输出每列的值
		var value string
		for i, col := range values {
			// 这里我们可以检查值是否是nil(在数据库里就是NULL值)
			if col == nil {
				value = "NULL"
			} else {
				value = string(col) // 把列值转换为字符串用来输出
			}
			fmt.Println(columns[i], ": ", value)
		}
		fmt.Println("-----------------------------------")
	}
	// 检查在获取行数据的过程中,是否遇到了一个错误
	if err = rows.Err(); err != nil {
		panic(err.Error())
	}
}

旧密码(old_passwords)

什么是old_passwords?

MySQL 4.1版(2004年发布!)更改了协议,引入了更安全的密码认证。添加了变量old_password,它启用旧密码认证这种传统的认证方式,但禁用新的更安全的密码认证方式。旧密码认证方式使用非常弱的哈希,这就是为什么它被认为是不安全的。如果你不需要传统的旧密码认证方式,就不应该使用它!

由于旧密码不安全且已被弃用,因此Go语言的MySQL驱动程序在默认情况下不启用此认证方式。如果你依赖它,可以通过在DSN中添加allowOldPasswords=true来显式启用它。

如何禁用它?

在MySQL的配置文件my.cnf(Windows上为my.ini)中将old_passwords设置为false。在Linux上,你可以在/etc/my.cnf找到此文件。

变量old_passwords属于mysqld小节,如果在配置文件里找不到它,就添加它:

[mysqld]
old_passwords = 0

你可能还需要重新生成密码。有关如何升级,参考http://code.openark.org/blog/mysql/upgrading-passwords-from-old_passwords-to-new-passwords

测试(Testing)

也许你需要编辑已经打开的数据库连接的参数。以下是如何设置不同密码的示例:

Linux/Unix

$ export MYSQL_TEST_PASS=root

Windows

$ SET MYSQL_TEST_PASS=root

参数列表

MYSQL_TEST_USER ( User用户名 )
MYSQL_TEST_PASS ( Password 密码)
MYSQL_TEST_PROT ( Network Protocol 网络协议)
MYSQL_TEST_ADDR (网络地址,IPv4或IPv6)
MYSQL_TEST_DBNAME (数据库名称)
MYSQL_TEST_CONCURRENT ( 值为1启用并发测试)

开发思路

在Auth响应中发送连接参数[MySQL 5.6.6+]

http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse41

http://dev.mysql.com/doc/internals/en/capability-flags.html#flag-CLIENT_CONNECT_ATTRS

支持更多认证插件

  • .mylogin.cnf http://dev.mysql.com/doc/refman/5.6/en/mysql-nutshell.html
  • SHA-256 http://dev.mysql.com/doc/refman/5.6/en/sha256-authentication-plugin.html
  • auth_socket http://dev.mysql.com/doc/refman/5.6/en/socket-authentication-plugin.html

网络压缩模式

http://dev.mysql.com/doc/internals/en/compression.html

使用分离的goroutine从连接中读取

目前,我们发送请求数据包,然后接收响应数据包。

由于Go没有提供有效的非阻塞EOF检查方法,即使服务器已经关闭连接,我们也可能会发送请求数据包。在已关闭的连接上发送请求会使ErrBadConn不安全。 如果我们可以在发送数据包之前检测到EOF,我们就可以安全地使用ErrBadConn

使用分离的goroutine从连接中读取数据是Go的常规编程方式。读数据的goroutine 也可能需要实现对Context支持。

可能会有显着的性能衰退。但我认为我们应该这样做。

教程:访问关系型数据库

本文翻译自《Tutorial: Accessing a relational database》。

目录

先决条件

为你的代码创建一个文件夹

建立一个数据库

查找并导入数据库的驱动程序

获取数据库句柄并连接

查询多行

查询单行

添加数据

结论

完成全部代码

本教程介绍了使用Go及其标准库中的database/sql包访问关系数据库的基础知识。

如果你对Go及其工具有基本的了解,你将充分利用本教程。如果这是你第一次接触Go,请先参阅教程:Go入门以获得快速介绍。

你将使用的database/sql包包括用于连接数据库、执行事务、取消正在进行的操作等的类型和函数。有关使用该包的更多详细信息,请参阅访问关系型数据库

在本教程中,你将创建一个数据库,然后编写代码来访问该数据库。你的示例项目将是有关古典爵士乐唱片的数据存储库。

在本教程中,你将逐步完成以下部分:

1 为你的代码创建一个文件夹。

2 建立一个数据库。

3 导入数据库驱动。

4 获取数据库句柄并连接。

5 查询多行。

6 查询单行。

7 添加数据。

注意:有关其他教程,请参阅教程

先决条件

  • 安装MySQL关系数据库管理系统 (DBMS)。
  • 安装Go语言开发环境。有关安装说明,请参阅安装Go
  • 一个编辑代码的工具。你拥有的任何文本编辑器都可以正常工作。
  • 一个命令行终端。Go在Linux和Mac中的任何终端,以及Windows中的PowerShell或cmd上都能很好地工作。

为你的代码创建一个文件夹

首先,为你要编写的代码创建一个文件夹。

1 打开命令提示符并切换到你的家目录。 在Linux或Mac上:

$ cd

在Windows上:

C:\> cd %HOMEPATH%

对于本教程的其余部分,我们将显示$作为提示符。我们使用的命令也适用于 Windows。

2 在命令提示符下,为你的代码创建一个名为data-access的目录。

$ mkdir data-access
$ cd data-access

3 创建一个模块,你可以在其中管理将在本教程中添加的依赖项。

运行go mod init命令,为它提供新代码的模块路径。

$ go mod init example/data-access
go: creating new go.mod: module example/data-access

此命令创建一个go.mod文件,其中将列出你添加的依赖项以供追踪。有关更多信息,请参阅管理依赖项

注意:在实际开发中,你应该指定一个更符合你自己需求的模块路径。有关更多信息,请参阅管理依赖项

接下来,你将创建一个数据库。

建立一个数据库

在此步骤中,你将创建要使用的数据库。你将使用DBMS本身的CLI来创建数据库和表,以及添加数据。

你将创建一个数据库,其中包含有关黑胶唱片上的老式爵士乐唱片的数据。

此处的代码使用MySQL CLI,大多数DBMS都有自己的具有类似功能的CLI。

1 打开新的命令提示符。

2 在命令行中,登录DBMS,如下面的MySQL示例所示。

$ mysql -u root -p
Enter password:

mysql>

3 在mysql命令提示符下,创建一个数据库。

mysql> create database recordings;

4 切换到你刚刚创建的数据库,以便你可以添加表。

mysql> use recordings;
Database changed

5 在文本编辑器的数据访问文件夹中,创建一个名为create-tables.sql的文件来保存用于添加表的SQL脚本。

6 在文件中,粘贴以下SQL代码,然后保存文件。

DROP TABLE IF EXISTS album;
CREATE TABLE album (
  id         INT AUTO_INCREMENT NOT NULL,
  title      VARCHAR(128) NOT NULL,
  artist     VARCHAR(255) NOT NULL,
  price      DECIMAL(5,2) NOT NULL,
  PRIMARY KEY (`id`)
);

INSERT INTO album
  (title, artist, price)
VALUES
  ('Blue Train', 'John Coltrane', 56.99),
  ('Giant Steps', 'John Coltrane', 63.99),
  ('Jeru', 'Gerry Mulligan', 17.99),
  ('Sarah Vaughan', 'Sarah Vaughan', 34.98);

在此SQL代码中,你:

  • 删除一个名为album的表。如果你想重新开始使用该表,执行此命令即可。
  • 创建一个包含四列的album表:titleartistprice。每行的id值由DBMS自动创建。
  • 添加四行值。

7 在mysql命令提示符下,运行你刚刚创建的脚本。

使用以下形式的source命令:

mysql> source /path/to/create-tables.sql

8 使用SELECT语句来验证你是否成功创建包含数据的表。

mysql> select * from album;
+----+---------------+----------------+-------+
| id | title         | artist         | price |
+----+---------------+----------------+-------+
|  1 | Blue Train    | John Coltrane  | 56.99 |
|  2 | Giant Steps   | John Coltrane  | 63.99 |
|  3 | Jeru          | Gerry Mulligan | 17.99 |
|  4 | Sarah Vaughan | Sarah Vaughan  | 34.98 |
+----+---------------+----------------+-------+
4 rows in set (0.00 sec)

接下来,你将编写一些Go代码来连接数据库以便你可以查询数据。

查找并导入数据库的驱动程序

现在你已经有了一个包含一些数据的数据库,开始写你的Go代码。

找到并导入一个数据库驱动程序,该驱动程序会将你通过database/sql包中的函数发出的请求转换为数据库可以理解的请求。

1 在你的浏览器中,访问SQLDrivers wiki页面以确定你可以使用的驱动程序。

使用页面上的列表来确定你将使用的驱动程序。为了在本教程中访问MySQL,你将使用Go-MySQL-Driver

2 请注意驱动程序的包名称,此处为github.com/go-sql-driver/mysql

3 使用你的文本编辑器,创建一个用于编写你的Go代码的文件,命名为main.go保存在你之前创建的数据访问目录中。

4 进入main.go,粘贴以下代码导入驱动包。

package main

import "github.com/go-sql-driver/mysql"

在此代码中,你:

  • 将你的代码添加到main包中,以便可以独立执行它。
  • 导入MySQL驱动程序github.com/go-sql-driver/mysql

导入驱动程序后,你将开始编写代码来访问数据库。

获取数据库句柄并连接

现在编写一些Go代码,使用数据库句柄(handler)访问数据库。

你将使用指向sql.DB结构体的指针,该结构表示对特定数据库的访问。

编写代码

1 在main.go中,在你刚刚添加的import代码下面,粘贴以下go代码以创建数据库句柄。

var db *sql.DB

func main() {
    // 设置连接参数。
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // 获取一个数据库句柄。
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }

    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")
}

在这段代码中:

  • 声明一个类型为*sql.DB的数据库变量db。这就是你的数据库句柄。

db作为全局变量简化了这个例子。在生产环境中,你应该避免使用全局变量,应该将变量传递给需要它的函数,或者将其封装在结构体中。

  • 使用MySQL驱动程序的Config(配置)类型和这个类型的FormatDSN函数来收集连接选项,并将其格式化为连接字符串的DSN。

Config结构体使得代码比直接使用连接字符串DSN更具可读性。

  • 调用sql.Open初始化db变量,并传入FormatDSN函数的返回值。
  • 检查sql.Open返回的错误变量。例如,如果数据库连接格式不正确,它可能会连接失败。

为了简化代码,我们调用log.Fatal函数来结束执行并将错误打印到控制台。在生产环境中,你应该以更优雅的方式来处理错误。

  • 调用DB.Ping函数以确认是否正常连接到数据库。在运行时,sql.Open可能不会立即连接到数据库,具体取决于驱动程序。你在这里使用DB.Ping函数来确认database/sql包可以在需要时连接到数据库。
  • 检查Ping函数是否返回错误,以防连接失败。
  • 如果Ping函数连接成功,则打印消息。

2. 在main.go文件的顶部附近,就在包声明的下方,导入支持你刚刚编写的代码的包。

main.go文件的顶部现在应该是这样的:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

3. 保存main.go文件

运行这段代码

1. 开始将MySQL驱动程序模块作为一个依赖项进行跟踪。

使用go get添加github.com/go-sql-driver/mysql模块作为你自己模块的一个依赖项。使用句点参数表示“获取当前目录中的代码的所有依赖项”。

$ go get .
go get: added github.com/go-sql-driver/mysql v1.6.0

Go下载了这个依赖项,因为你在上一步中将它添加到了import声明中。有关跟踪依赖项的详细信息,请参阅添加一个依赖项

在命令提示符下,设置DBUSERDBPASS环境变量以供这个Go程序使用。

在Linux或Mac上:

$ export DBUSER=username
$ export DBPASS=password

在Windows上:

C:\Users\you\data-access> set DBUSER=username
C:\Users\you\data-access> set DBPASS=password

3. 在包含main.go的目录中的命令行中,通过键入go run和一个点参数来运行代码,意思是“运行当前目录中的main包”:

$ go run .
Connected!

你可以连接成功!接下来,你将查询一些数据。

查询多行

在本节中,你将使用Go执行一个SQL查询,该查询旨在返回多行。

对于可能返回多行的SQL语句,可以使用database/sql包中的Query方法,然后循环获取它返回的行。(稍后将在查询单行一节中学习如何查询单行。)

编写代码

1. 进入main.go,在func main的正上方,粘贴Album结构体的以下定义。你将使用它来保存从查询返回的行数据。

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

2. 在func main下,粘贴以下albumsByArtist函数以查询数据库:

// albumsByArtist函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
    // 一个切片用于保存返回行数据的相册(Album)信息。
    var albums []Album

    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
    if err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    defer rows.Close()
    // 遍历每一行,使用Scan函数将列数据分配给结构体的对应字段。
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        albums = append(albums, alb)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    return albums, nil
}

在此代码中,你:

  • 声明Album类型的一个album切片,用来存储来自返回行的数据。结构体字段名称和类型对应于数据库列的名称和类型。
  • 使用DB.Query执行SELECT语句以查询具有指定艺术家(Artist)姓名的专辑。

查询的第一个参数是SQL语句。在该参数之后,可以传递零个或多个任意类型的参数。这些参数的值将依次填入你在SQL语句中使用?号占据的位置。通过将SQL语句与参数值分开(而不是将它们用fmt.Sprintf函数连接成一个字符串),你可以让database/sql包将参数值与SQL文本分开发送,从而消除任何SQL注入风险。

  • defer rows.Close()延迟关闭rows,以便在函数退出时释放它持有的任何资源。
  • 遍历返回的行,使用Rows.Scan将每行的列值分别分配给Album结构体字段。

Scan函数获取指向Go变量的指针列表,对应的列值将写入其中。在这里,你传入&运算符加alb结构体变量中的字段。通过这些指针Scan函数写入以更新结构体的字段。

  • 在循环体内,也检查Scan函数是否有错误返回。
  • 在循环体内,将新的alb变量附加到albums切片。
  • 在循环体之后,使用rows.Err函数检查整个查询是否有错误发生。请注意,如果查询本身失败了,则检查此处的错误是发现结果不完整的唯一方法。

3. 更新你的main函数以调用albumsByArtist函数。

func main的末尾,添加以下代码:

albums, err := albumsByArtist("John Coltrane")
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)

在新代码中,你现在:

  • 调用albumsByArtist函数,将其返回值分配给新的albums变量。
  • 打印结果。

运行代码

从包含main.go文件的目录的命令行运行代码:

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]

接下来你将查询单行。

查询单行

在本节中,你将使用Go查询数据库中的一行。

对于你知道的最多返回一行数据的SQL语句,你可以使用QueryRow函数,它比使用Query函数更简单。

写出代码

1. 在albumsByArtist函数下,粘贴以下albumByID函数的代码:

// albumByID函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
    // 声明一个用来存储返回行数据的Album结构体变量。
    var alb Album

    row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
    if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
        if err == sql.ErrNoRows {
            return alb, fmt.Errorf("albumsById %d: no such album", id)
        }
        return alb, fmt.Errorf("albumsById %d: %v", id, err)
    }
    return alb, nil
}

在这段代码中,你:

  • 使用DB.QueryRow执行SELECT语句来查询具有指定ID的相册的信息。

它返回一个sql.Row。为了简化你的代码,QueryRow函数不会返回错误。相反,它安排稍后的Rows.Scan函数返回任何可能发生的查询错误(例如sql.ErrNoRows)。

  • 使用Row.Scan函数将列值复制到结构体字段中。
  • 检查Scan函数返回错误。

特殊错误sql.ErrNoRows表示查询未返回任何行。通常,该错误值得用更具体的文本来说明,例如此处的“没有这样的专辑(no such album)”。

2. 更新main函数以调用albumByID函数。

func main的末尾,添加以下代码:

// 在此处硬编码ID为2以测试查询。
alb, err := albumByID(2)
if err != nil {
    log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)

在这段代码中,你现在:

  • 调用你添加的albumByID函数。
  • 打印返回的相册的ID。

运行代码

在包含main.go的目录的命令行运行代码:

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}

接下来,你将向数据库添加一条相册信息数据。

添加数据

在本节中,你将使用Go语言执行SQL的INSERT语句以向数据库添加新行。

你已经了解了如何将QueryQueryRow函数与返回数据的SQL语句一起使用。如果要执行不返回数据的SQL语句,你可以使用Exec函数。

写代码

1. 在albumByID函数下方,粘贴以下addAlbum函数以在数据库中插入新专辑的数据:

// addAlbum函数添加指定专辑的数据到数据库,返回这条新记录的ID
func addAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    return id, nil
}

在此代码中,你:

Query函数一样,Exec函数接受SQL语句,后跟SQL语句的参数值。

  • 检查尝试INSERT时是否有错误发生。
  • 使用Result.LastInsertId函数检索插入到数据库的新行的ID。
  • 检查尝试检索ID时是否有错误发生。

2. 更新main函数以调用新的addAlbum函数。

func main的末尾,添加以下代码:

albID, err := addAlbum(Album{
    Title:  "The Modern Sound of Betty Carter",
    Artist: "Betty Carter",
    Price:  49.99,
})
if err != nil {
    log.Fatal(err)
}
fmt.Printf("ID of added album: %v\n", albID)

在新代码中,你现在:

  • 调用addAlbum函数,传入新的专辑数据,然后把添加的这行专辑数据的ID返回给albID变量。

运行代码

在包含main.go的目录的命令行运行代码:

$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
ID of added album: 5

结论

恭喜你刚刚使用Go对关系数据库执行了简单的操作。

建议你进行下一个主题:

  • 看看数据访问指南,其中包括更多关于此处仅涉及的主题的信息。
  • 如果你是Go语言新手,你可以在Effective GoHow to write Go code中找到有用的最佳实践。
  • Go Tour是对Go基础知识的一个很好的循序渐进的介绍。

完成全部代码

本节给出本教程的全部代码:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "os"

    "github.com/go-sql-driver/mysql"
)

var db *sql.DB

type Album struct {
    ID     int64
    Title  string
    Artist string
    Price  float32
}

func main() {
    // 设置连接参数
    cfg := mysql.Config{
        User:   os.Getenv("DBUSER"),
        Passwd: os.Getenv("DBPASS"),
        Net:    "tcp",
        Addr:   "127.0.0.1:3306",
        DBName: "recordings",
    }
    // 获取一个数据库句柄
    var err error
    db, err = sql.Open("mysql", cfg.FormatDSN())
    if err != nil {
        log.Fatal(err)
    }

    pingErr := db.Ping()
    if pingErr != nil {
        log.Fatal(pingErr)
    }
    fmt.Println("Connected!")

    albums, err := albumsByArtist("John Coltrane")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Albums found: %v\n", albums)

    // 获取ID为2的相册的信息,测试查询语句
    alb, err := albumByID(2)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Album found: %v\n", alb)

    albID, err := addAlbum(Album{
        Title:  "The Modern Sound of Betty Carter",
        Artist: "Betty Carter",
        Price:  49.99,
    })
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("ID of added album: %v\n", albID)
}

// albumsByArtist函数查询具有指定艺术家(Artist)姓名的专辑。
func albumsByArtist(name string) ([]Album, error) {
    // 一个切片用于保存返回行数据的相册(Album)信息。
    var albums []Album

    rows, err := db.Query("SELECT * FROM album WHERE artist = ?", name)
    if err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    defer rows.Close()
    // 遍历每一行,使用Scan函数将列数据分配给结构体的对应字段。
    for rows.Next() {
        var alb Album
        if err := rows.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
            return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
        }
        albums = append(albums, alb)
    }
    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
    }
    return albums, nil
}

// albumByID函数使用指定ID来查询相册(album)信息。
func albumByID(id int64) (Album, error) {
    // 声明一个用来存储返回行数据的Album结构体变量。
    var alb Album

    row := db.QueryRow("SELECT * FROM album WHERE id = ?", id)
    if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
        if err == sql.ErrNoRows {
            return alb, fmt.Errorf("albumsById %d: no such album", id)
        }
        return alb, fmt.Errorf("albumsById %d: %v", id, err)
    }
    return alb, nil
}

//addAlbum函数添加指定专辑的数据到数据库,返回这条新记录的ID
func addAlbum(alb Album) (int64, error) {
    result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    id, err := result.LastInsertId()
    if err != nil {
        return 0, fmt.Errorf("addAlbum: %v", err)
    }
    return id, nil
}

ChatGPT最佳实践和使用场景收集

ChatGPT最佳实践

怎么与ChaGPT对话能让它更好地理解你的意图,然后给出精彩的回答?以下工具可能可以帮到你:

ChatGPT使用场景收集

ChatGPT+Stable Diffusion画出文字描述的场景

刚才试了一下用 #stablediffusion 画版画,用到了一个很少人用的 LoRA,先让 ChatGPT 帮我想场景描述,然后直接复制到 SD 里开始画。可以转行当个版画大师了,看看这几幅画能卖出去多少钱。

from:https://twitter.com/decohack/status/1629078910947430400 @viggo

使用ChatGPT写Python爬虫

如何用 ChatGPT 帮你写 Python 爬虫?实际样例循序渐进手把手教程https://www.youtube.com/watch?v=jh-yX6KwIlY

使用ChatGPT做主题挖掘

如何用 ChatGPT 帮你做主题挖掘?自动 Python 编程 LDA 可视化 https://www.youtube.com/watch?v=jh-yX6KwIlY

ChatGPT角色扮演

https://github.com/f/awesome-chatgpt-prompts

https://www.youtube.com/watch?v=oF74vvgq4Kc

小爱同学+ChatGPT

https://github.com/yihong0618/xiaogpt

基于ChatGPT API的文本翻译、文本润色、语法纠错Bob插件

Bob是一款macOS平台翻译和OCR软件。

基于ChatGPT API的文本翻译、文本润色、语法纠错Bob插件:

https://github.com/yetone/bob-plugin-openai-translator

此插件已支持使用ChatGPT API对句子进行润色和语法修改,只需要把目标语言选成跟源语言一样即可,全面替代Grammarly!而且理论上任何语言都可以润色,不只是英语。

ChatPDF

地址:https://www.chatpdf.com

在网站上传 PDF 文件后,就可以对上传的PDF文件内容向ChatGPT提任何关于此本书的问题,包括快速生成PDF的内容摘要,结果支持输出为中文。

类似的应用还有:https://github.com/mckaywrigley/paul-graham-gpt

这个应用是将Paul Graham的一些论文整理成CSV文件,然后通过ChatGPT的 OpenAI Embeddings 接口提交给ChatGPT,然后可以向ChatGPT询问关于Paul Graham论文的问题。

这两个应用代表了ChatGPT最有用但刚刚开始的应用场景:让ChatGPT针对垂直行业知识学习,成为你真正的私人学习助理、工作助理。。。

例如: 如果给ChatGPT输入的是某个专业的教材(例如来自 https://openstax.org),配合ChatGPT的翻译能力、Wisper接口能力,那就得到了一个精通某个专业、多门语言的私人教师。

语言翻译及润色

原来用DeepL来做翻译,用DeepL Write(或Grammarly)来做语法润色和修改,已经觉得很好用了。

使用ChatGPT后,明显感觉用ChatGPT来做翻译和润色比DeepL和DeepL Write更加好用。但由于在交互界面上使用很不方便。

ChatGPT开放API几天,接连出现了几个此类应用。

OpenAI Translator

基于 ChatGPT API 的文本翻译、文本润色、语法纠错 Bob 插件

使用ChatGPT翻译制作双语电子书

将外文电子书翻译成双语对照版本,并在任何设备上阅读

http://editgpt.app是翻译类浏览器插件,改善了以往 grammarly 擅长的语法校对、改善书面表达功能。以我的弱鸡英文写作能力和中上英文阅读能力,我自己测试下来效果挺好的,可能跟美国人写的还有点不同(指习惯,不是说写作能力),但已经够强大了。

可以说,开放API后,ChatGPT已经可以取代目前大部分的翻译了。对比一下中信、机械工业等出版社每一年出版的国外图书的中文版的翻译质量,ChatGPT已经完全超出几条街。

生成内容摘要

glarity算是最近几天发现的一个宝贝,可以生成Google搜索摘要、Youtube视频摘要。

地址:https://glarity.app

主要功能:

为Google 搜索结果生成摘要。

基于 ChatGPT 和 Youtube 字幕生成视频摘要。类似的chrome扩展还有 Glasp,但Glarity更还用。 还有一个 Language Reactor ,能够实时生成Youtube等视频站的字幕,虽然没有ChatGPT加持,但强烈推荐。

好用的AI工具软件收集

让你失业的不一定是AI,但一定是比你更早熟练掌握AI的人。最近体验AI真是大开眼界,文字,图片,设计,剪辑,配音无一不有,甚至还有色图生成工具,不禁为福利姬感到深深的担忧

整理了一批AI实用工具,图文,新媒体,编程等均有涉及,一句话:千万别错过,有工作的来练习技能,自由的来体验科技进步。

写作类

http://copy.ai

https://copymatic.ai

图片类

https://openai.com/dall-e-2/

https://deepai.org

https://lexica.art

https://stablediffusionweb.com

Stable Diffusion是2022年发布的深度学习文生图模型。它主要用于根据文本的描述产生详细图像,尽管它也可以应用于其他任务,如内补绘制、外补绘制,以及在提示词指导下产生图生图的翻译。

Scribble Diffusion:https://scribblediffusion.com/ GitHub地址:https://github.com/replicate/scribble-diffusion

Scribble Diffusion,一款开源涂鸦AI绘画工具,能够将草图变成精致的图像,基于Replicate 提供的API来实现的,使用ControlNet来固定图形布局,使用比较简单,直接在草粗画图,然后配上描述,点击go/去即可生成一幅精致的图片了,喜欢的可以在线体验试试。

图标/UI设计

https://logo.com

https://lordicon.com

http://uiverse.io

全世界第一个使用自然语言/文字进行 UI 设计的设计工具Galileo。UI 设计师们最担心的一刻提前到来了。以前的Midjourney们都擅长美术创作,画到UI时只能输出效果图,没法得到设计稿。看起来Galileo能逐步生成真实UI设计稿。排队等试用地址:http://useGalileo.ai

视频/剪辑

http://runwayml.com

http://veed.io

配音

http://murf.ai

https://typecast.ai

编程

http://tabnine.com

https://beta.openai.com/examples

小工具

智能取名:https://namelix.com

福利姬杀手(仅做研究用):https://pornpen.ai

AI工具软件聚合网站

https://gpt3demo.com/map

https://futuretools.io/?tags-n5zn=finance…

https://futurepedia.io

https://allthingsai.com

https://www.ainavpro.com/

https://www.vondy.com/explore/shubham

https://orelmizrahii.github.io/Web-AI-Archive/thelist.html

https://theresanaiforthat.com/

https://saasaitools.com/

https://www.futurepedia.io/

https://ai-toolbox.codefuture.top/

https://17yongai.com/

JSON和Go

本文翻译自《JSON and Go》。

Andrew Gerrand

2011/01/25

介绍

JSON(JavaScript对象表示法)是一种简单的数据交换格式。在语法上,它类似于JavaScript的对象和列表。它最常用于web后端和浏览器中运行的JavaScript程序之间的通信,但也用于许多其他地方。它的主页json.org提供了一个非常清晰和简洁的标准定义。

使用json包,从Go程序读取和写入json数据很简单。

编码

为了编码JSON数据,我们使用Marshal函数。

func Marshal(v interface{}) ([]byte, error)

给定Go结构体Message

type Message struct {
    Name string
    Body string
    Time int64
}

Message的一个实例:

m := Message{"Alice", "Hello", 1294706395881547000}

我们可以使用json.Marshal函数得到m的JSON编码版本:

b, err := json.Marshal(m)

如果一切顺利,err将为nilb将是包含此JSON数据的[]byte

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

只有可以表示为有效JSON的数据结构才可以被编码:

  • JSON对象只支持字符串作为键;要编码Go的map类型,它必须是map[string]T格式(其中T是json包支持的任何Go类型)。
  • 无法对通道、复数和函数类型进行编码。
  • 不支持循环数据结构;他们会使Marshal陷入无限循环。
  • 指针将被编码为它们指向的值(如果指针为空,则编码为“null”)。

json包只访问结构体类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在JSON的输出中。

解码

要解码JSON数据,我们使用Unmarshal函数。

func Unmarshal(data []byte, v interface{}) error

我们必须首先创建一个存储解码数据的变量:

var m Message

并调用json.Unmarshal,将一个[]byte的JSON数据和一个指向m的指针传递给它:

err := json.Unmarshal(b, &m)

如果b包含适合m的有效JSON,则在调用之后err将为nil,并且来自b的数据将存储在结构体m中,就像通过如下赋值一样:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Unmarshal如何识别存储解码数据的字段?对于给定的JSON键“Foo”,Unmarshal将查看目标结构体的字段来查找(按以下优先级顺序):

  • 带有“Foo”标签(tag)的导出(公有)字段(有关结构体标签的更多信息,请参阅Go语言规范),
  • 名为“Foo”的导出字段,或
  • 其他“Foo”单词的不区分大小写的匹配项,例如名为“FOO”或“FoO”的导出字段。

当JSON数据的结构与Go类型不完全匹配时会发生什么?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal将只解码它可以在目标类型中找到的字段。在这种情况下,只会填充mName字段,而忽略Food字段。当你希望从大型JSON数据中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构体中任何未导出(私有)的字段都不会受到Unmarshal的影响。

但是,如果你事先不知道JSON数据的结构怎么办?

带接口的通用JSON

interface{}(空接口)类型描述了一个具有零个方法(没有一个方法)的接口。每个Go类型都至少实现了零个方法,因此都实现了空接口。

空接口可以用作通用的容器类型:

var i interface{}
i = "a string"
i = 2011
i = 2.777

类型断言访问底层的具体类型:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

或者,如果底层类型未知,则可以使用switch语句来确定类型:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i的类型不是以上类型中的一种
}

json包使用map[string]interface{}[]interface{}值来存储任意JSON对象或数组;它会愉快地将任何有效的JSON blob解码为一个普通的interface{}值。interface{}值默认使用的底层Go类型是:

  • bool用于JSON布尔值,
  • float64用于JSON数字值,
  • string用于JSON字符串值,以及
  • nil用于JSON的null(空值)。

解码任意数据

考虑这个存储在变量b中的JSON数据:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

在不知道此数据的内部结构的情况下,我们可以使用Unmarshal将其解码为interface{}值:

var f interface{}
err := json.Unmarshal(b, &f)

此时,f中的Go值将是一个map,其键为字符串,其值存储为空接口interface{}值:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

要访问此数据,我们可以使用类型断言来访问f的底层map[string]interface{}

m := f.(map[string]interface{})

然后我们可以使用range语句遍历这个map,并使用switch语句来判断其值的具体类型:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

通过这种方式,你可以使用未知内部结构的JSON数据,同时仍然享受类型安全的好处。

引用类型

让我们定义一个Go类型来包含上一个示例中的数据:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

将该数据解码为FamilyMember值按预期工作,但如果我们仔细观察,我们会发现发生了一件了不起的事情。通过var语句,我们分配了一个FamilyMember结构体,然后将指向该值的指针提供给Unmarshal函数,但此时Parents字段是一个nil切片值。为了填充Parents字段,Unmarshal函数在幕后分配了一个新切片。这是Unmarshal解码它支持的引用类型(指针、切片和映射)的典型方式。

考虑解码到这个数据结构中:

type Foo struct {
    Bar *Bar
}

如果JSON中有一个Bar字段,Unmarshal函数将会分配一个新的Bar实例并填充它。否则,Bar将被保留为nil指针。

由此产生了一个有用的模式:如果你有一个接收几种不同消息类型的应用程序,你可以定义“接收者”结构,例如

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

发送方可以填充JSON对象的Cmd字段和/或Msg字段,具体取决于他们想要传达的消息类型。Unmarshal函数在将JSON解码为IncomingMessage结构时,将仅分配JSON数据中存在的那个数据结构。具体要处理哪种消息,程序员只需测试CmdMsg是否不为nil。

数据流的编码器和解码器

json包提供了DecoderEncoder类型来支持读写JSON数据流的操作。NewDecoderNewEncoder函数包装了io.Readerio.Writer接口类型。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

下面是一个示例程序,它从标准输入流中读取一系列JSON对象,从每个对象中删除除了Name字段以外的所有字段,然后将对象写入标准输出流:

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

由于io.Readerio.Writer的广泛使用,这些DecoderEncoder类型可以用于广泛的场景,例如读取和写入HTTP连接、WebSocket或文件等。

参考

有关详细信息,请参阅json包的文档。有关json的示例用法,请参阅jsonrpc包的源文件。

Go映射(map)实战

本文翻译自《Go maps in action》。

Andrew Gerrand

2013/02/06

介绍

哈希表是计算机科学中最有用的数据结构之一。许多哈希表的实现具有不同的属性,但通常它们都提供快速查找、添加和删除这些功能。Go提供了实现哈希表的内置的map类型。

定义和初始化

Go的map类型如下所示:

map[KeyType]ValueType

其中KeyType可以是任何可比较的类型(稍后将详细介绍),ValueType可以是任何类型,包括另一个map!

此变量m是字符串键到int值的映射:

var m map[string]int

map类型是引用类型,类似指针或切片,因此上面的m值为nil;它不指向已初始化的map。读取时,nil map的行为类似于空map,但尝试写入nil map会导致运行时panic;不要那样做。初始化一个map,请使用内置的make函数:

m = make(map[string]int)

make函数分配并初始化map数据结构,并返回指向它的map值。该数据结构在运行时的实现细节,不由语言本身决定。在本文中,我们将关注map的使用,而不是它们的实现。

使用map

Go提供了一种熟悉的语法来处理map。以下语句将键“route”设置为值66

m["route"] = 66

以下语句检索存储在键“route”下的值并将其赋值给新变量i

i := m["route"]

如果请求的键不存在,我们将获得值的类型的零值。在本例的情况下,值类型是int,因此零值是0

j := m["root"]
// j == 0

内置的len函数返回map中的元素的个数:

n := len(m)

内置的delete函数从map中删除一个元素:

delete(m, "route")

delete函数不返回任何内容,如果指定的键不存在,就什么也不做。

双值赋值运算可以测试键是否存在:

i, ok := m["route"]

在此语句中,第一个值i被赋予存储在键“route”下的值。如果该键不存在,则i是值类型的零值0。第二个值ok是一个布尔值,如果键存在于map中则为真,否则为假。

要在不检索值的情况下测试键是否存在,可以使用下划线来省略第一个返回值:

_, ok := m["route"]

要遍历map的内容,请使用range关键字:

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

要使用一些数据初始化一个map,请使用map字面量:

commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

可以使用以下语法来初始化一个空map,这在功能上与使用make函数相同:

m = map[string]int{}

利用零值

当键不存在时,检索map返回零值是很有用的一个特性。

例如,map里的布尔值可以用作类似集合的数据结构(回想一下,布尔类型的零值为false)。此示例遍历链接列表的Nodes并打印它们的值。它使用Node指针的map来检测列表中的循环。

    type Node struct {
        Next  *Node
        Value interface{}
    }
    var first *Node

    visited := make(map[*Node]bool)
    for n := first; n != nil; n = n.Next {
        if visited[n] {
            fmt.Println("cycle detected")
            break
        }
        visited[n] = true
        fmt.Println(n.Value)
    }

如果n已被访问,表达式visited[n]为true,如果n不存在则为false。无需使用二值形式来测试map中是否存在n;默认返回零值已经够用了。

另一个有用的零值实例是map的切片值。append到一个nil切片会分配一个新的切片,所以把一个值append到一个map的切片值无需检查键是否存在。在以下示例中,切片people填充了Person值。每个Person都有一个Name字段和一个Likes切片字段。该示例创建了一个map,将每个爱好(作为likes的键)与喜欢它的那些人(作为likes的值)相关联。

    type Person struct {
        Name  string
        Likes []string
    }
    var people []*Person

    likes := make(map[string][]*Person)
    for _, p := range people {
        for _, l := range p.Likes {
            likes[l] = append(likes[l], p)
        }
    }

打印出所有喜欢奶酪的人:

for _, p := range likes["cheese"] {
        fmt.Println(p.Name, "likes cheese.")
    }

打印出喜欢培根的人数:

fmt.Println(len(likes["bacon"]), "people like bacon.")

请注意,由于rangelen都将nil切片视为零长度切片,因此即使没有人喜欢奶酪或培根(尽管不太可能),最后两个示例仍然能正常工作。

键的类型

如前所述,map的键可以是任何可比较的类型。Go语言规范对此进行了精确定义,但简而言之,可比较类型是布尔类型、数字类型、字符串类型、指针类型、通道类型和接口类型,以及仅包含这些类型的结构体或数组。值得注意的是,没有切片、map和函数,这些类型不能使用==进行比较,因此不能用作map的键。

显然,字符串、整数和其他基本类型应该可以用作map的键,但可能出乎意料的是结构体作为map的键。结构体可以从多个维度作为键。例如,以下map可用于按国家/地区统计网页点击率:

hits := make(map[string]map[string]int)

这个map的键是字符串类型,值是另一个map(字符串到整数的映射)类型。外部map的每个键是网页的路径。内部map的每个键都是两个字母的国家/地区代码。此表达式检索澳大利亚人加载某个网页的次数:

n := hits["/doc/"]["au"]

不幸的是,这种方法在添加数据时并不灵活,对于任何给定的外部键,你必须检查内部map是否存在,并在需要时创建它:

func add(m map[string]map[string]int, path, country string) {
    mm, ok := m[path]
    if !ok {
        mm = make(map[string]int)
        m[path] = mm
    }
    mm[country]++
}
add(hits, "/doc/", "au")

我们可以使用带有结构体键的单个map的设计来消除所有的复杂性:

type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

当越南人访问主页时,增加(并可能创建)适当的计数器,使用一行代码就能实现:

hits[Key{"/", "vn"}]++

同样,看看有多少瑞士人看过/ref/spec网页:

n := hits[Key{"/ref/spec", "ch"}]

并发

map对于并发使用是不安全的:Go没有定义当你同时读取和写入它们时会发生什么。如果你需要从并发执行的goroutine读取和写入map,则访问必须通过某种同步机制进行调解。保护map的一种常见方法是使用sync.RWMutex

此语句声明一个counter变量,它是一个包含map和内嵌sync.RWMutex的匿名结构体。

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

要从counter读取,请获取读锁:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

要写入counter,请获取写锁:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

迭代顺序

使用range循环遍历map时,Go语言没有指定迭代顺序,并且不保证从一次迭代到下一次迭代是相同顺序的。如果你需要稳定的迭代顺序,则必须维护一个单独的数据结构来指定该顺序。以下例子使用单独排序的键切片,来按键在切片里的顺序打印输出map[int]string

import "sort"

var m map[int]string
var keys []int
for k := range m {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", m[k])
}

复杂性守恒定律 (The Law of Conservation of Complexity or Tesler’s Law)

复杂性守恒定律 (The Law of Conservation of Complexity or Tesler’s Law):系统中存在着一定程度的复杂性,并且不能减少。

系统中的某些复杂性是无意的。这是由于结构不良,错误或者糟糕的建模造成的。这种无意的复杂性可以减少或者消除。然而,由于待解决问题有固有的复杂性,这些复杂性是内在的。这些复杂性可以转移,但不能消除。

该定律有趣的一点是,即使简化整个系统,内在的复杂性也不会降低。它会转移给用户,并且用户必须以更复杂的方式行事。

现实世界的复杂度无法使用代码来消除,如果你想少写代码,就要多写配置;如果你想少写配置,就要多写注解……

业务逻辑无法使用代码来化简或消除,业务逻辑不写在代码里,那就一定会转移到配置文件里或者其他什么地方。

业务逻辑不是程序员能控制的,程序员只负责代码实现,公司的领导层或许可以通过流程再造来改变业务逻辑。

因为现实世界的复杂性无法在代码层面消除,过度地抽象、解耦、套用设计模式反而会增加代码的复杂度。

关于配置文件

配置文件尽量保持简单直白,不要有分支或循环逻辑。分支或循环逻辑应该放到控制器里,因为控制器就是写业务逻辑代码的,改需求改的是控制器里代码,不变化的代码封装在模型里。配置文件里的配置信息应该是控制器的辅助,而不是相反。

由于外部环境的改变而经常跟着改变的变量值应该写在配置文件里,例如数据库配置信息测试环境一套,生产环境另一套,系统环境变量,要启用的进程数、线程数,公司名称、学校名称等,而不要把业务逻辑中的流程控制语句写在配置文件里。

总之,不要过度设计,不要过早优化,等版本稳定下来了再用设计模式重构代码也不迟。当然如果你的项目不缺钱也不缺时间,那么过早优化完全没有问题。

参考

https://github.com/nusr/hacker-laws-zh

https://en.wikipedia.org/wiki/Law_of_conservation_of_complexity

https://ferd.ca/complexity-has-to-live-somewhere.html

https://www.zhihu.com/question/429538225

Go切片(slice):用法和内部结构

本文翻译自《https://go.dev/blog/slices-intro》。

Andrew Gerrand

2011年1月5日

介绍

Go的切片(slice)类型提供了一种方便有效的方法来处理有类型的数据的序列。切片类似于其他语言中的数组,但具有一些不同寻常的属性。本文将介绍切片是什么以及它们的使用方法。

数组

切片类型是建立在Go的数组类型之上的抽象,因此要理解切片我们必须首先理解数组。

数组类型通过指定长度和元素类型来定义。例如,类型[4]int表示一个包含四个整数的数组。数组的大小是固定的;它的长度是其类型的一部分(例如[4]int[5]int是不同的、不兼容的类型)。数组可以用通常的方式索引,所以表达式s[n]访问第n个元素,从0开始。

var a [4]int
a[0] = 1
i := a[0]
// i == 1

数组不需要显式初始化;数组的零值是其所有元素本身为零值的现成(ready-to-use)数组:

// a[2] == 0,是int类型的零值

[4]int的内存表示形式仅为顺序排列的四个整数值:

Go语言的数组是值类型的。数组变量表示整个数组;它不是指向第一个数组元素的指针(C语言中的情况)。这意味着当你分配或传递数组值时,你将拷贝其所有内容(为了避免拷贝,你可以用一个指针指向数组,这是指向数组的指针,而不是数组本身)。可以将数组视为一种结构体,它具有索引字段而非命名字段,它是一种固定大小的复合值。

数组字面量可以这样指定:

b := [2]string{"Penn", "Teller"}

或者,你也可以让编译器为你计数数组元素的个数:

b := [...]string{"Penn", "Teller"}

这两种情况b的类型都是[2]string

切片

数组有它的空间,但有点不灵活,所以你不会在Go代码中经常看到数组。不过,切片无处不在。它们以数组为基础,提供强大的功能和便利。

切片的类型是[]T,其中T是切片里的元素的类型。与数组类型不同,切片类型没有指定长度。

切片字面量的声明就像数组字面量一样,只是你省略了元素计数:

letters := []string{"a", "b", "c", "d"}

可以使用内置函数make创建切片,该函数的签名如下:

func make([]T, len, cap) []T

其中T代表要创建的切片的元素的类型。make函数接收一个类型T、一个长度len和一个可选的容量cap。调用时,make分配一个数组并返回一个引用该数组的切片。

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

当省略cap参数时,它默认等于指定的长度len。这是同一代码的更简洁的版本:

s := make([]byte, 5)

可以使用内置的lencap函数检查切片的长度和容量。

len(s) == 5
cap(s) == 5

接下来的两节讨论长度和容量之间的关系。

切片的零值为nil。对于nil切片,lencap函数都将返回0。

切片也可以通过“切片”现有切片或数组来形成。切片是通过指定一个半开放区间来完成的,其中两个索引由冒号分隔。例如,表达式b[1:4]创建一个包含b的下标从1到3的元素的切片(所得切片的索引将是0到2)。

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

切片表达式的开始和结束索引是可选的;它们分别默认为0和切片的长度:

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

这也是在给定一个数组的情况下,创建一个指向它的切片的语法:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 一个指向数组x的切片

切片内部

一个切片是一个数组片段的描述符。它由指向数组的指针、片段的长度及其容量(片段的最大长度)组成。

之前由make([]byte, 5)创建的变量s的结构如下:

长度是切片引用的数组元素的个数。容量是底层数组中的元素的个数(从切片指针指向的元素开始数)。在接下来的几个示例中,长度和容量之间的区别将变得更加清晰。

当我们对s进行切片时,观察切片数据结构的变化及其与底层数组之间的关系:

s = s[2:4]

切片不会复制底层数组的数据。它将创建一个指向原始数组的新切片值。这使得切片操作与处理数组索引一样高效。因此,修改新切片值的元素(而不是新切片值本身)会修改原始切片的元素:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

之前我们将s切成比其容量短的长度。 我们可以通过再次切片来增加s的容量:

s = s[:cap(s)]

切片不能超出其容量。尝试这样做会导致运行时panic,就像索引超出切片或数组边界时一样。同样,不能将切片重新切片到0以下来访问数组中较早的元素。

增长切片的元素(copyappend函数)

为了增加切片的容量,必须创建一个新的、更大的切片,并将原始切片的内容复制到其中。这项技术是其他语言的动态数组在幕后的实现方式。下一个示例通过创建一个新的切片t,将s的内容复制到t,然后将t赋值给s,从而使s的容量加倍:

t := make([]byte, len(s), (cap(s)+1)*2) // +1是为了防止cap(s) == 0的情况
for i := range s {
        t[i] = s[i]
}
s = t

内置的copy函数使这种常见的循环操作变得更容易。顾名思义,copy将数据从源切片复制到目标切片。它返回复制的元素个数。

func copy(dst, src []T) int

copy函数支持在不同长度的切片之间进行复制(长度较短的那个切片复制或被复制完毕就不再继续)。此外,copy可以处理共享同一底层数组的源切片和目标切片,正确地处理元素部分重叠的切片。

使用copy函数,我们可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一个常见的操作是将数据追加到切片的末尾。此函数将字节元素附加到字节切片,必要时会增大切片的容量,返回更新后的切片:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // 如果有必要,重新分配一个底层数组
        // 考虑到未来的数据增长,在此处加倍底层数组的容量。
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

可以这样使用AppendByte

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

AppendByte这样的函数很有用,因为它们提供了对切片增长方式的完全控制。 根据程序的特性,可能需要分配更小或更大的块,或者对重新分配的大小设置上限。

但是大多数程序不需要完全控制,因此Go提供了一个适合大多数用途的内置append函数,签名如下:

func append(s []T, x ...T) []T

append函数将元素x附加到切片s的末尾,并在需要更大容量时扩大切片。

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

要将一个切片附加到另一个切片,请使用...将第二个参数展开为列表。

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 等价于"append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由于切片的零值(nil)就像一个零长度切片,你可以声明一个切片变量,然后在循环中附加到它:

// Filter函数返回一个新切片,它只包含s切片中那些使fn函数返回true的元素
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

一个可能的“陷阱”

如前所述,重新切片不会复制底层数组。整个数组将保存在内存中,直到不再被引用为止。有时这会导致程序只需要一小部分数据,但将所有数据保存在内存中。

例如,FindDigits函数将一个文件加载到内存中,并在其中搜索第一组连续数字,将它们作为一个切片返回。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

此代码的行为确实满足要求,但返回的[]byte指向包含整个文件的数组。由于切片引用了原始数组,只要切片一直存在,垃圾收集器就无法释放数组;为了文件的几个有用字节,就将文件的全部内容保存在内存中。

要解决此问题,可以在返回之前将感兴趣的数据复制到新切片:

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

可以使用append函数简化上述函数的代码。这留给读者作为练习。

进一步阅读

Effective Go包含对切片数组的深入处理,Go语言规范定义了切片及其相关的辅助函数。

SpringBoot运行单元时测试报错:java.lang.IllegalStateException Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or…

错误描述

SpringBoot运行单元测试时报错:java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=…) with your test

我的软件环境

JDK 11.0

SpringBoot 2.7

Maven 3.8.7

出错原因

src/test/java目录下的包结构和src/main/java目录下的包结构应该一样,否则就会报上述错误。

我的src/main/java目录下的包是com.comp.example,但是src/test/java目录下的包是com.sample.example,两者不一样,因此报上述错误。

解决方法

把src/test/java目录下的包结构和src/main/java目录下的包结构改成一样即可。

参考

https://stackoverflow.com/questions/47487609/unable-to-find-a-springbootconfiguration-you-need-to-use-contextconfiguration

JDBC连接MySQL数据库警告:Establishing SSL connection without server’s identity verification is not recommend

JDBC连接MySQL出现如下警告:

Establishing SSL connection without server’s identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn’t set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to ‘false’. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

原因是高版本MySQL需要指明是否进行SSL连接。

解决方法是在JDBC连接MySQL的URL字符串中加入useSSL=true或者useSSL=false即可,例如:

jdbc:mysql://127.0.0.1:3306/framework?serverTimezone=UTC&characterEncoding=utf8&useSSL=false

如果你的连接确实没有暴露给网络(仅限本地主机),或者你在没有真实数据的非生产环境中工作,那么可以肯定的是:通过包含选项useSSL=false来禁用SSL没有坏处。

如果要使用useSSL=true,需要以下一组选项才能使SSL使用证书、主机验证并禁用弱协议选项:

  • useSSL=true
  • sslMode=VERIFY_IDENTITY
  • trustCertificateKeyStoreUrl=file:path_to_keystore
  • trustCertificateKeyStorePassword=password
  • enabledTLSProtocols=TLSv1.2

因此,作为一个示例,让我们假设你希望使用SSL将其中一种Atlassian产品(Jira、Confluence、Bamboo等)连接到MySQL服务器,你需要执行以下主要步骤:

  • 首先,确保你有一个为MySQL服务器主机生成的有效的SSL/TLS证书,并且CA证书安装在客户端主机上(如果你使用自签名,那么你可能需要手动执行此操作,但对于流行的公共CA,它已经在那里了)。
  • 接下来,确保java密钥库包含所有CA证书。在Debian/Ubuntu上,这是通过运行以下命令来实现的:
update-ca-certificates -f
chmod 644 /etc/ssl/certs/java/cacerts
  • 最后,更新Atlassian产品使用的连接字符串以包含所有必需的选项,这在Debian/Ubuntu上类似于:
jdbc:mysql://mysql_server/confluence?useSSL=true&sslMode=VERIFY_IDENTITY&trustCertificateKeyStoreUrl=file%3A%2Fetc%2Fssl%2Fcerts%2Fjava%2Fcacerts&trustCertificateKeyStorePassword=changeit&enabledTLSProtocols=TLSv1.2&useUnicode=true&characterEncoding=utf8

更多相关信息请参见MySQL官方文档https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-using-ssl.html

参考

https://beansandanicechianti.blogspot.com/2019/11/mysql-ssl-configuration.html