1 下载 header-editor 插件并安装。
2 添加一个要修改的请求头x-forwarded-for,值为1.1.1.1。
3 VPN开全局代理。
该方法设置后,Edge浏览器侧边栏New Bing无法正常使用,但网页版New Bing可以正常使用。
1 下载 header-editor 插件并安装。
2 添加一个要修改的请求头x-forwarded-for,值为1.1.1.1。
3 VPN开全局代理。
该方法设置后,Edge浏览器侧边栏New Bing无法正常使用,但网页版New Bing可以正常使用。
组件也叫构件。什么是组件?一个组件就是一个可以独立更换和升级的软件单元。组件具有如下特点:
1 能实现一定功能,或者提供一些服务。
2 不能单独运行,要作为系统的一部分来发挥作用。
3 是物理上的概念,不是逻辑上的概念。
4 可单独维护、可独立升级、可替换而不影响整个系统。
组件是物理上独立的一个东西,它可单独维护、升级、替换,画构件图的目的就是要做系统的构件设计,思考系统在物理上的划分,可利用现有哪些构件,哪些部分可做成构件供以后的项目重用等。
问题1:我们做软件设计时, 往往会提到 “模块 “ 这一词,“模块” 是不是构件呢?
不一定,每个人心中的“模块”的标准是不太一样的,有时候会按业务逻辑来划分模块,有时候从技术的角度来划分。模块只是为了方便说明问题,将软件人为地划分为几个部分而已,我们可以对照组件的上述几个特点来判断 “模块” 是不是构件。
问题2:软件常常会采用分层设计,那一层是一个构件吗?
大部分情况下分层设计中的每一层,仅是一个逻辑上的划分,物理上并不是单独的文件,这时这些分层不是组件。但具体要看实际的设计情况,可对照组件的上述几个特点来判断。
问题3:如何区分“服务”(service)和“组件”(component)?
所谓“组件”是指这样一个软件单元:它将被作者无法控制的其他应用程序使用,但后者不能对组件进行修改。也就是说,使用一个组件的应用程序不能修改组件的源代码,但可以通过作者预留的某种途径对其进行扩展,以改变组件的行为。
服务和组件有某种相似之处:它们都将被外部的应用程序使用。在我看来,两者之间最大的差异在于:组件是在本地使用的软件库(例如JAR文件、程序集、DLL、或者源码导入);而服务是进程外的组件,通过同步或异步的本机进程之间通信或远程接口调用(例如web service、消息系统、RPC,或者socket)这样的机制来被应用程序使用。
服务也可以调用其他服务,因为服务本身也是一个应用程序。
可以把一个个服务映射为一个个运行时的进程,但这仅仅是一个近似。一个服务也可以包括多个进程,比如一个服务应用程序的进程和仅被该服务使用的数据库进程。
服务可被独立部署。如果一个应用系统由在单个进程中的多个软件库所组成,那么对任一组件做一处修改,都不得不重新部署整个应用系统。但是如果该应用系统被分解为多个服务,那么对于一个服务的多处修改,仅需要重新部署这一个服务,例外是更改服务暴露出来的接口。
以服务的方式来实现组件化的另一个结果是,能获得更加显式的(explicit)组件接口。
服务的远程调用,比起进程内调用,远程调用更加昂贵。
参考
《微服务》 Martin Fowler
《IoC容器和依赖注入模式》 Martin Fowler
《火球 UML大战需求分析》
本文翻译自https://github.com/go-sql-driver/mysql/
Go-MySQL-Driver 用于Go语言database/sql
包的MySQL驱动。
特性
database/sql
包)sql.RawBytes
支持LONG DATA
LOAD DATA LOCAL INFILE
,也支持io.Reader
time.Time
解析要求
安装
使用shell中的go工具将包简单地安装到你的$GOPATH
中:
$ go get -u github.com/go-sql-driver/mysql
确保Git安装在你的机器上和系统的PATH
中。
用法
Go语言MySQL驱动是Go的database/sql/driver
包的接口的实现。你只需要导入这个驱动,然后就可以使用完整的database/sql
包的API。
使用mysql
作为driverName
,使用有效的DSN作为dataSourceName
:
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
)
// ...
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
panic(err)
}
// 参见“重要设置”小节。
db.SetConnMaxLifetime(time.Minute * 3)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(10)
重要设置
db.SetConnMaxLifetime()
确保连接在MySQL服务器、操作系统或其他中间件关闭之前由驱动程序安全地关闭。由于某些中间件会在5分钟后关闭空闲连接,因此我们建议把超时时间设置为短于5分钟的时间。此设置也有助于负载平衡和更改系统变量。
db.SetMaxOpenConns()
强烈建议使用该函数来限制应用程序使用的连接数。没有建议的限制数,因为它取决于具体应用程序和MySQL服务器。
db.SetMaxIdleConns()
建议设置为与db.SetMaxOpenConns()
的相同。当它小于SetMaxOpenConns()
的设置时,连接打开和关闭的频率可能比你预期的要高得多。空闲连接可以通过db.SetConnMaxLifetime()
设置超时时间。如果你想更快地关闭空闲连接,从Go 1.15开始,你可以使用db.SetConnMaxIdleTime()
函数来设置。
DSN(Data Source Name,数据源名称)
数据源名称具有一个通用格式,例如PEAR DB所使用的,但没有类型前缀(用方括号标记可选部分):
[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]
完整形式的DSN:
username:password@protocol(address)/dbname?param=value
除了数据库名称之外,所有值都是可选的。因此,最小DSN为:
/dbname
如果不想预先选择一个数据库实例,请将dbname
保留为空:
/
这与空DSN字符串的效果相同。
或者,我们也可以使用Config.FormatDSN通过填充一个结构体来创建DSN字符串。
密码
密码可以由任何字符组成,无需转义。
协议
有关可用网络的更多信息,请参阅net.Dial。一般来说,如果可以,你应该使用Unix域套接字,否则应该使用TCP,以获得最佳性能。
地址
对于TCP和UDP网络,地址的格式为host[:port]
。如果省略port
,将使用默认端口号。如果主机使用IPv6地址,则必须将其括在方括号中。函数net.JoinHostPort和net.SplitHostPort以这种形式处理地址。
对于Unix域套接字,地址是MySQL服务的套接字的绝对路径,例如:
/var/run/mysqld/mysqld.sock
或
/tmp/mysql.sock
参数
参数区分大小写!
请注意,true
、TRUE
、True
或1
中的任何一个表示布尔值“真”。毫不奇怪,布尔值“假”可以指定为以下任意值:false
、FALSE
、False
或0
。
allowAllFiles
Type: bool
Valid Values: true, false
Default: false
allowAllFiles=true
禁用LOAD DATA LOCAL INFILE
的文件白名单,并允许所有文件。可能不安全!
allowCleartextPasswords
Type: bool
Valid Values: true, false
Default: false
allowCleartextPasswords=true
在帐户需要时允许使用明文(cleartext)客户端插件,例如使用PAM身份验证插件定义的插件。以明文形式发送密码在某些配置中可能是一个安全问题。如果密码有可能被拦截,为避免出现问题,客户端应使用保护密码的方法连接到MySQL服务器,包括使用TLS/SSL、IPsec或专用网络。
allowFallbackToPlaintext
Type: bool
Valid Values: true, false
Default: false
allowFallbackToPlaintext=true
的作用类似于使用--ssl-mode=PREFERRED
选项的MySQL客户端,如用于连接到服务器的命令选项中所述
allowNativePasswords
Type: bool
Valid Values: true, false
Default: true
allowNativePasswords=false
禁止使用MySQL本机密码(native password)方式。
allowOldPasswords
Type: bool
Valid Values: true, false
Default: false
allowOldPasswords=true
允许使用不安全的旧密码(old password)方式。应该避免使用这种方式,但在某些情况下是必要的。另请参阅old_passwords维基页面。
charset
Type: string
Valid Values: <name>
Default: none
设置用于客户端-服务器交互的字符集(“SET NAMES <value>
“)。如果设置了多个字符集(用逗号分隔),则在设置当前字符集失败时继续尝试设置后续的字符集。例如, (charset=utf8mb4,utf8
)设置utf8mb4
(在MySQL 5.5.3中引入),如果是旧服务器就回退到utf8
。
不鼓励使用charset
参数,因为它会向服务器发出额外的查询。除非你需要回退行为,否则请改用collation
参数。
checkConnLiveness
Type: bool
Valid Values: true, false
Default: true
在支持的平台上,Go从连接池中检索连接,在使用该连接之前会检查其是否活动。如果不活动,则将相应的连接标记为坏连接,并尝试使用另一个连接。checkConnLiveness=false
不检查连接是否活动。
collation
Type: string
Valid Values: <name>
Default: utf8mb4_general_ci
设置连接时用于客户端-服务器交互的字符排序规则。与charset
不同,collation
不会发出额外的查询。如果指定的排序规则在目标服务器上不可用,则连接将失败。
可以使用SHOW COLLATION
检索服务器的有效字符集列表。
MySQL 5.5支持默认排序规则(utf8mb4_general_ci
)。对于较旧的MySQL,应该使用较旧的排序规则(例如utf8_general_ci
)。 不能使用字符集”ucs2″、”utf16″、”utf16le”和”utf32″的排序规则(参考)。
clientFoundRows
Type: bool
Valid Values: true, false
Default: false
clientFoundRows=true
会导致UPDATE语句返回匹配的行数,而不是更改的行数。
columnsWithAlias
Type: bool
Valid Values: true, false
Default: false
当columnsWithAlias=true
时,调用sql.Rows.Columns()
将返回表别名和用点分隔的列名。例如:
SELECT u.id FROM users as u
如果columnsWithAlias=true
,将返回u.id
而不仅仅是id
。
interpolateParams
Type: bool
Valid Values: true, false
Default: false
如果interpolateParams=true
,则调用db.Query()
和db.Exec()
时的占位符(?
)将插入到具有给定参数的单个查询字符串中。这减少了往返次数,因为当interpolateParams=false
时,驱动程序必须准备好一条语句,使用给定的参数执行它,然后再次关闭该语句。
这不能与多字节编码BIG5、CP932、GB2312、GBK或SJIS一起使用。这些都被拒绝,因为它们可能会引入SQL注入漏洞!
loc
Type: string
Valid Values: <escaped name>
Default: UTC
设置time.Time
值的时区(当使用parseTime=true
时)。如果时区设置为Local
,那么设置为系统的时区。有关详细信息,请参阅time.LoadLocation
。
注意,这将设置time.time
值的时区,但不会更改MySQL的time_zone设置。为此,请参见time_zone系统变量,该变量也可以设置为DSN的参数。
请记住,参数值必须是url.QueryEscape
编码的。或者,你可以手动将/
替换为%2F
。例如,US/Pacific
(美国/太平洋时区)应该被设置为loc=US%2FPacific
。
maxAllowedPacket
Type: decimal number
Default: 4194304
允许的最大数据包的大小(以字节为单位)。默认值为4MiB,应进行调整以匹配服务器设置。maxAllowedPacket=0
可用于在每次连接时从服务器自动获取max_allowed_packet
变量。
multiStatements
Type: bool
Valid Values: true, false
Default: false
是否允许在一个查询中使用多条语句。虽然这允许批量查询,但也大大增加了被SQL注入的风险。只返回第一个查询语句的结果,所有其他结果都被默默丢弃。当使用multiStatements
时,?
参数只能在第一条语句中使用。
parseTime
Type: bool
Valid Values: true, false
Default: false
parseTime=true
将DATE
和DATETIME
值的输出类型更改为time.Time
,而不是[]byte/string
类型的日期或日期时间,例如0000-00-00 00:00:00
将转换为time.Time
的零值。
readTimeout
Type: duration
Default: 0
I/O读取的超时时间。该值必须是带有单位后缀的十进制数字(”ms”、”s”、”m”、”h”),例如”30s”、”0.5m”或”1m30s”。
rejectReadOnly
Type: bool
Valid Values: true, false
Default: false
rejectReadOnly=true
会导致数据库驱动程序拒绝只读连接。这是针对自动故障切换期间可能出现的争用(race condition)情况,即故障切换后mysql客户端将连接到某个只读副本。
请注意,这应该是一种相当罕见的情况,因为自动故障转移通常发生在主服务器关闭时,并且竞态条件(race condition)不会发生,除非它在故障转移开始后立即恢复在线。另一方面,发生这种情况时,MySQL应用程序可能会卡在只读连接上,直到重新启动。然而,这种故障相当容易复现,例如在AWS Aurora的MySQL兼容集群上手动造成故障转移。
如果你不依赖只读事务来拒绝不应该发生的写入操作,那么在某些MySQL服务提供商(例如AWS Aurora)上设置此选项,对于故障切换来说更安全。
请注意,read-only
服务器可能会返回1290号错误,此选项将导致重试该错误。但是,存在其他一些情况,也使用相同的1290错误号。因此启用此选项时,应确保应用程序在read-only
模式之外,都不会导致1290号错误。
serverPubKey
Type: string
Valid Values: <name>
Default: none
可以使用mysql.RegisterServerPubKey
注册服务器的公钥,然后可以在DSN中通过参数使用该公钥。公钥用于传输加密数据,例如用于身份验证。如果服务器的公钥是已知的,则应手动设置它,以避免每次需要它时都从服务器向客户端传输,代价昂贵且可能不安全。
timeout
Type: duration
Default: OS default
建立连接时的超时时间,又称拨号超时。该值必须是带单位后缀(”ms”、”s”、”m”、”h”)的十进制数,例如”30s”、”0.5m”或”1m30s”。
tls
Type: bool / string
Valid Values: true, false, skip-verify, preferred, <name>
Default: false
tls=true
启用与服务器的TLS/SSL加密连接。如果要使用自签名或无效证书(服务器端),那么使用skip-verify
参数;或通过preferred
参数使用TLS(仅当得到服务器端建议时),这类似于skip-verify
,但还允许回退到未加密的连接。skip-verify
和preferred
都不添加任何可靠的安全性。你可以在向mysql.RegisterTLSConfig
注册后使用自定义的TLS配置。
writeTimeout
Type: duration
Default: 0
I/O写入超时。该值必须是带单位后缀(”ms”、”s”、”m”、”h”)的十进制数,例如”30s”、”0.5m”或”1m30s”。
系统变量
任何其他参数都被解释为系统变量:
<boolean_var>=<value>: SET <boolean_var>=<value>
<enum_var>=<value>: SET <enum_var>=<value>
<string_var>=%27<value>%27: SET <string_var>='<value>'
规则:
url.QueryEscape
编码过(这意味着字符串变量的值必须用%27
包裹)。例如:
autocommit=1: SET autocommit=1
time_zone=%27Europe%2FParis%27: SET time_zone='Europe/Paris'
transaction_isolation=%27REPEATABLE-READ%27: SET transaction_isolation='REPEATABLE-READ'
实例
user@unix(/path/to/socket)/dbname
root:pw@unix(/tmp/mysql.sock)/myDatabase?loc=Local
user:password@tcp(localhost:5555)/dbname?tls=skip-verify&autocommit=true
通过设置系统变量sql_mode将警告视为错误:
user:password@/dbname?sql_mode=TRADITIONAL
通过IPv6的TCP:
user:password@tcp([de:ad:be:ef::ca:fe]:80)/dbname?timeout=90s&collation=utf8mb4_unicode_ci
远程主机上的TCP,例如Amazon RDS:
id:password@tcp(your-amazonaws-uri.com:3306)/dbname
应用引擎上的谷歌云SQL:
user:password@unix(/cloudsql/project-id:region-name:instance-name)/dbname
使用本地主机上默认端口(3306)的TCP:
user:password@tcp/dbname?charset=utf8mb4,utf8&sys_var=esc%40ped
使用默认协议(tcp)和主机(localhost:3306):
user:password@/dbname
未预先选择数据库:
user:password@/
连接池和超时
连接池由Go的database/sql
包管理。有关如何配置池的大小以及连接在池中停留的时间的详细信息,请参阅database/sql
文档中的*DB.SetMaxOpenConns
、*DB.Set MaxIdleConns
和*DB.Set-ConnMaxLifetime
。每个单独连接的读取、写入和拨号超时分别使用DSN参数readTimeout、writeTimeout和timeout进行配置。
支持context.Context
此驱动程序支持Go 1.8中引入的ColumnType
接口,但ColumnType.Length()
除外,该接口目前不受支持。所有无符号的数据库类型都将返回带有Unsigned
的INT
、TINYINT
、SMALLINT
或BIGINT
名称。
支持ColumnType
此驱动程序支持Go 1.8中引入的ColumnType
接口,但ColumnType.Length()
除外,该接口目前不受支持。所有无符号的数据库类型都将返回带有Unsigned
的INT
、TINYINT
、SMALLINT
或BIGINT
名称。
支持context.Context
在Go 1.8,database/sql
包添加了对context.Context
的支持。此驱动程序支持查询超时和通过context
取消语句的执行。有关详细信息,请参阅database/sql
包中的context
支持。
支持LOAD DATA LOCAL INFILE
对于此功能,你需要直接访问软件包。因此,你必须更改导入路径(无_
):
import "github.com/go-sql-driver/mysql"
文件必须通过mysql.RegisterLocalFile(filepath)
注册来明确允许加载里面的数据(推荐使用这种做法),或者必须通过使用DSN参数allowAllFiles=true
来禁用allowlist检查(这种做法可能不安全!)。
要使用io.Reader
,必须使用mysql.RegisterReaderHandler(name, handler)
注册处理函数,该函数返回io.Reador
或io.ReadCloser
。然后,Reader
可以使用文件路径Reader::<name>
。可以为不同的处理程序 handler
注册不同的名字name
,,当你不再需要它时,使用DeregisterReaderHandler
反注册。
有关详细信息,请参阅Go MySQL驱动程序的godoc。
支持time.Time
MySQL DATE
和DATETIME
值的默认内部输出类型为[]byte
,这允许你将值扫描到程序中的[]byte
、string
或sql.RawBytes
变量中。
然而,许多人希望将MySQL的DATE
和DATETIME
值扫描到Go语言的time.Time
变量中,这在逻辑上等同于MySQL中的DATE
和DATETIME
值。你可以通过使用DSN参数parseTime=true
将内部输出类型从[]byte
更改为time.Time
。你可以使用DSN参数loc
设置默认time.Time
的时区位置。
注意:截至Go 1.1,这使得time.Time
成为唯一可以扫描DATE
和DATETIME
值的变量类型。这会破坏例如对sql.RawBytes
的支持。
支持Unicode
由于1.5版本的Go MySQL驱动程序默认情况下自动使用排序规则utf8mb4_general_ci
。
可以使用DSN参数collation
设置其他排序规则/字符集。
1.0版的驱动程序建议将&charset=utf8
(即MySQL命令SET NAMES utf8
的别名)添加到DSN中,以启用正确的UTF-8支持。但现在已经没有必要这么做了,如果要设置其他排序规则/字符集的话,应该首选collation
参数。
有关MySQL的Unicode支持的更多详细信息参见http://dev.mysql.com/doc/refman/8.0/en/charset-unicode.html。
测试/开发
要运行驱动程序测试,你可能需要调整配置。有关详细信息,请参阅测试的Wiki 页面。
Go语言的MySQL驱动程序的功能还不完善。如果你想做出贡献,我们将非常感谢你的帮助,你可以处理未解决的问题或查看拉取请求。
有关详细信息,请参阅贡献指南。
License
Go语言的MySQL驱动程序在Mozilla公共许可证2.0版(Mozilla Public License Version 2.0)下获得许可。
Mozilla将许可范围总结如下:
MPL:许可证适用于任何包含MPL代码的文件。
这意味着:
如果你对许可证有更多疑问,请阅读MPL 2.0的常见问题解答。
你可以在此处阅读完整条款:LICENSE。
本文翻译自https://github.com/go-sql-driver/mysql/wiki
2023/03/05
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))
}
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/capability-flags.html#flag-CLIENT_CONNECT_ATTRS
支持更多认证插件
网络压缩模式
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 添加数据。
注意:有关其他教程,请参阅教程。
先决条件
为你的代码创建一个文件夹
首先,为你要编写的代码创建一个文件夹。
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
表:title
、artist
和price
。每行的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
包中,以便可以独立执行它。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
作为全局变量简化了这个例子。在生产环境中,你应该避免使用全局变量,应该将变量传递给需要它的函数,或者将其封装在结构体中。
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
声明中。有关跟踪依赖项的详细信息,请参阅添加一个依赖项。
在命令提示符下,设置DBUSER
和DBPASS
环境变量以供这个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
函数。运行代码
在包含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
语句以向数据库添加新行。
你已经了解了如何将Query
和QueryRow
函数与返回数据的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
}
在此代码中,你:
DB.Exec
执行INSERT
语句。与Query
函数一样,Exec
函数接受SQL语句,后跟SQL语句的参数值。
INSERT
时是否有错误发生。Result.LastInsertId
函数检索插入到数据库的新行的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对关系数据库执行了简单的操作。
建议你进行下一个主题:
完成全部代码
本节给出本教程的全部代码:
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最佳实践
怎么与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几天,接连出现了几个此类应用。
基于 ChatGPT API 的文本翻译、文本润色、语法纠错 Bob 插件
http://editgpt.app是翻译类浏览器插件,改善了以往 grammarly 擅长的语法校对、改善书面表达功能。以我的弱鸡英文写作能力和中上英文阅读能力,我自己测试下来效果挺好的,可能跟美国人写的还有点不同(指习惯,不是说写作能力),但已经够强大了。
可以说,开放API后,ChatGPT已经可以取代目前大部分的翻译了。对比一下中信、机械工业等出版社每一年出版的国外图书的中文版的翻译质量,ChatGPT已经完全超出几条街。
生成内容摘要
glarity算是最近几天发现的一个宝贝,可以生成Google搜索摘要、Youtube视频摘要。
主要功能:
为Google 搜索结果生成摘要。
基于 ChatGPT 和 Youtube 字幕生成视频摘要。类似的chrome扩展还有 Glasp,但Glarity更还用。 还有一个 Language Reactor ,能够实时生成Youtube等视频站的字幕,虽然没有ChatGPT加持,但强烈推荐。
让你失业的不一定是AI,但一定是比你更早熟练掌握AI的人。最近体验AI真是大开眼界,文字,图片,设计,剪辑,配音无一不有,甚至还有色图生成工具,不禁为福利姬感到深深的担忧
整理了一批AI实用工具,图文,新媒体,编程等均有涉及,一句话:千万别错过,有工作的来练习技能,自由的来体验科技进步。
写作类
图片类
https://deepai.org
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://lordicon.com
全世界第一个使用自然语言/文字进行 UI 设计的设计工具Galileo。UI 设计师们最担心的一刻提前到来了。以前的Midjourney们都擅长美术创作,画到UI时只能输出效果图,没法得到设计稿。看起来Galileo能逐步生成真实UI设计稿。排队等试用地址:http://useGalileo.ai。
视频/剪辑
http://runwayml.com
http://veed.io
配音
编程
https://beta.openai.com/examples
小工具
智能取名:https://namelix.com
福利姬杀手(仅做研究用):https://pornpen.ai
AI工具软件聚合网站
https://futuretools.io/?tags-n5zn=finance…
https://www.vondy.com/explore/shubham
https://orelmizrahii.github.io/Web-AI-Archive/thelist.html
https://theresanaiforthat.com/
https://saasaitools.com/
本文翻译自《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
将为nil
,b
将是包含此JSON数据的[]byte
:
b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)
只有可以表示为有效JSON的数据结构才可以被编码:
Marshal
陷入无限循环。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
将只解码它可以在目标类型中找到的字段。在这种情况下,只会填充m
的Name
字段,而忽略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数据中存在的那个数据结构。具体要处理哪种消息,程序员只需测试Cmd
或Msg
是否不为nil。
数据流的编码器和解码器
json包提供了Decoder
和Encoder
类型来支持读写JSON数据流的操作。NewDecoder
和NewEncoder
函数包装了io.Reader
和io.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.Reader
和io.Writer
的广泛使用,这些Decoder
和Encoder
类型可以用于广泛的场景,例如读取和写入HTTP连接、WebSocket或文件等。
参考
本文翻译自《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.")
请注意,由于range
和len
都将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):系统中存在着一定程度的复杂性,并且不能减少。
系统中的某些复杂性是无意的。这是由于结构不良,错误或者糟糕的建模造成的。这种无意的复杂性可以减少或者消除。然而,由于待解决问题有固有的复杂性,这些复杂性是内在的。这些复杂性可以转移,但不能消除。
该定律有趣的一点是,即使简化整个系统,内在的复杂性也不会降低。它会转移给用户,并且用户必须以更复杂的方式行事。
现实世界的复杂度无法使用代码来消除,如果你想少写代码,就要多写配置;如果你想少写配置,就要多写注解……
业务逻辑无法使用代码来化简或消除,业务逻辑不写在代码里,那就一定会转移到配置文件里或者其他什么地方。
业务逻辑不是程序员能控制的,程序员只负责代码实现,公司的领导层或许可以通过流程再造来改变业务逻辑。
因为现实世界的复杂性无法在代码层面消除,过度地抽象、解耦、套用设计模式反而会增加代码的复杂度。
关于配置文件
配置文件尽量保持简单直白,不要有分支或循环逻辑。分支或循环逻辑应该放到控制器里,因为控制器就是写业务逻辑代码的,改需求改的是控制器里代码,不变化的代码封装在模型里。配置文件里的配置信息应该是控制器的辅助,而不是相反。
由于外部环境的改变而经常跟着改变的变量值应该写在配置文件里,例如数据库配置信息测试环境一套,生产环境另一套,系统环境变量,要启用的进程数、线程数,公司名称、学校名称等,而不要把业务逻辑中的流程控制语句写在配置文件里。
总之,不要过度设计,不要过早优化,等版本稳定下来了再用设计模式重构代码也不迟。当然如果你的项目不缺钱也不缺时间,那么过早优化完全没有问题。
参考
https://github.com/nusr/hacker-laws-zh
https://en.wikipedia.org/wiki/Law_of_conservation_of_complexity