第一章:Go语言与数据库基础

1.1 Go语言发展史:从“C++太复杂”说起

说起Go语言,我总想起2007年的那个秋天。Google内部,几位大佬——Rob Pike、Ken Thompson、Robert Griesemer——坐在一起吐槽。吐槽什么呢?C++编译太慢,Java太啰嗦,Python在大规模系统里又不够稳。说白了,他们想要一门新语言:编译快、写起来爽、天生支持并发。

嗯,这就是Go的起点。2009年正式对外发布,2012年推出1.0版本。我记得那时候国内用Go的人还很少,大家觉得“Google又搞了个玩具”。结果呢?Docker、Kubernetes、etcd这些重量级项目全用Go写的。你想想看,一个语言能撑起整个云原生生态,这本身就说明问题了。

我个人习惯把Go的发展分成三个阶段:

  • 2007-2012:孕育期——内部使用,解决Google的痛点
  • 2012-2018:爆发期——Docker和Kubernetes带火Go,国内大厂开始跟进
  • 2018至今:成熟期——Go 1.11引入模块,1.18加入泛型,生态越来越完善

我在项目中遇到过不少从Python转Go的团队。刚开始大家都不适应,觉得写个接口怎么这么麻烦。但一旦上了生产环境,那个稳定性和性能,真香。

1.2 Go语言核心特性:为什么它适合做后端

Go的核心特性,我总结为四个字:简单、高效。别小看这两个词,能做到的编程语言真不多。

1.2.1 并发模型:goroutine + channel

这是Go最亮眼的地方。传统的多线程编程,锁来锁去,死锁、竞态条件,头都大了。Go换了个思路:用goroutine代替线程,用channel通信代替共享内存。

goroutine有多轻量?启动时只占几KB栈空间,可以轻松创建上万个。我做过一个压测,单机开了10万个goroutine,CPU和内存都还扛得住。换成Java线程试试?早就OOM了。

核心思想:不要通过共享内存来通信,而应该通过通信来共享内存。

1.2.2 编译速度:快到离谱

Go的编译速度,我用一个词形容:丝滑。一个中等规模的项目,从改代码到跑起来,基本就是秒级。对比Java的Maven或Gradle,你懂的。我曾经在团队里推广Go,就靠这一条说服了很多人——谁不想省下喝咖啡等编译的时间呢?

1.2.3 静态类型 + 垃圾回收

静态类型保证了代码的健壮性,编译期就能发现很多低级错误。垃圾回收又解放了程序员,不用手动管理内存。有人担心GC会影响性能,其实Go的GC一直在优化,1.8版本之后延迟已经降到毫秒级了。

1.2.4 标准库:够用且好用

Go的标准库非常强大。HTTP服务器、JSON解析、加密、压缩、模板引擎……开箱即用。我刚开始用Go写Web服务时,连第三方框架都没用,光靠标准库就撑起了一个日活百万的API网关。

个人建议:新手入门Go,先别急着上框架。把标准库玩熟了,你才能真正理解Go的设计哲学。

1.3 数据库基本概念:从“存数据”说起

数据库,说白了就是存数据的地方。但怎么存、怎么取、怎么保证不丢、怎么支持高并发,这里面的门道就多了。

1.3.1 关系型数据库 vs NoSQL

特性 关系型数据库(如MySQL) NoSQL(如MongoDB)
数据模型 表、行、列,严格Schema 文档、键值、图,灵活Schema
事务支持 ACID,强一致性 BASE,最终一致性
扩展方式 垂直扩展为主,水平扩展复杂 天然支持水平扩展
适用场景 金融、电商、ERP等强一致性场景 日志、社交、IoT等海量数据场景

我个人习惯:核心业务数据用MySQL,日志和缓存用NoSQL。别想着一个数据库打天下,那是不现实的。

1.3.2 SQL:和数据库对话的语言

SQL(Structured Query Language)是操作关系型数据库的标准语言。你不需要精通它,但增删改查(CRUD)必须手到擒来。

-- 创建表
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    age INT DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入数据
INSERT INTO users (name, age) VALUES ('张三', 28);

-- 查询数据
SELECT * FROM users WHERE age > 18;

-- 更新数据
UPDATE users SET age = 29 WHERE name = '张三';

-- 删除数据
DELETE FROM users WHERE id = 1;

避坑指南:我曾经在生产环境执行过不带WHERE条件的UPDATE……嗯,那是一次刻骨铭心的教训。从那以后,我写任何UPDATE和DELETE之前,都会先写SELECT确认范围。

1.3.3 事务:要么全做,要么全不做

转账这个例子大家都懂:A扣钱,B加钱。如果A扣完钱系统挂了,B没加上,那钱就丢了。事务就是解决这个问题的。

事务的四个特性(ACID):

  • 原子性(Atomicity):事务里的操作,要么全部成功,要么全部回滚
  • 一致性(Consistency):事务前后,数据状态保持一致
  • 隔离性(Isolation):并发事务之间互不干扰
  • 持久性(Durability):事务提交后,数据永久保存

你想想看,没有事务的数据库,谁敢用来做支付系统?

1.4 Go连接MySQL初体验:动手写第一行代码

好了,理论说够了,咱们来点实际的。用Go连接MySQL,其实就三步:装驱动、建连接、写查询。

1.4.1 安装MySQL驱动

Go连接MySQL需要第三方驱动。最常用的是 go-sql-driver/mysql。安装命令:

go get -u github.com/go-sql-driver/mysql

这里要注意:Go的数据库接口是标准化的(database/sql),驱动只是实现这个接口。所以不管你用MySQL、PostgreSQL还是SQLite,操作方式几乎一样。这就是Go设计的高明之处。

1.4.2 写一个简单的连接示例

package main

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

    _ "github.com/go-sql-driver/mysql" // 匿名导入,注册驱动
)

func main() {
    // 数据源名称格式:用户名:密码@tcp(主机:端口)/数据库名?参数
    dsn := "root:123456@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True"

    // 打开连接(注意:Open不会真正建立连接,只是验证参数)
    db, err := sql.Open("mysql", dsn)
    if err != nil {
        log.Fatal("打开数据库失败:", err)
    }
    defer db.Close() // 函数结束时关闭连接

    // Ping一下,真正建立连接
    err = db.Ping()
    if err != nil {
        log.Fatal("连接数据库失败:", err)
    }

    fmt.Println("连接MySQL成功!")

    // 查询数据库版本
    var version string
    err = db.QueryRow("SELECT VERSION()").Scan(&version)
    if err != nil {
        log.Fatal("查询失败:", err)
    }
    fmt.Println("MySQL版本:", version)
}

小技巧parseTime=True这个参数很重要。不加的话,Go会把MySQL的DATETIME类型当成字符串返回,你还要手动解析。加上之后,直接映射成time.Time,省心多了。

1.4.3 连接池:别每次都新建连接

数据库连接是个昂贵的资源。每次请求都新建连接,性能会非常差。Go的database/sql自带连接池,你只需要配置几个参数:

// 设置最大打开连接数
db.SetMaxOpenConns(100)

// 设置最大空闲连接数
db.SetMaxIdleConns(10)

// 设置连接最大存活时间
db.SetConnMaxLifetime(5 * time.Minute)

我在项目中遇到过一个问题:连接池设置得太小,高并发时请求排队,接口响应时间飙升。后来把MaxOpenConns从20调到100,问题就解决了。但也不能设得太大,否则数据库扛不住。

经验之谈:连接池的大小不是越大越好。一般建议 MaxOpenConns 设为 (CPU核心数 × 2 + 有效磁盘数) 这个公式算出来的值。当然,最终还是要靠压测来调优。

1.4.4 常见错误与排查

第一次连接MySQL,你可能会遇到这些问题:

  • Access denied for user:用户名或密码错了,检查一下
  • dial tcp 127.0.0.1:3306: connect: connection refused:MySQL没启动,或者端口不对
  • unknown database:数据库不存在,先CREATE DATABASE一下
  • packet too large:查询结果太大,调大MySQL的max_allowed_packet参数

嗯,这些坑我都踩过。尤其是第一个,有一次我熬夜排查,最后发现是密码里有个特殊字符没转义……从那以后,我密码只用字母和数字。

小结

这一章我们从Go的诞生聊到了它的核心特性,又回顾了数据库的基本概念,最后亲手用Go连上了MySQL。你可能会觉得内容有点多,但这些都是后面章节的基础。尤其是连接池和事务的概念,后面讲ORM框架时会反复用到。

下一章,我们会深入database/sql标准库,看看Go是怎么抽象数据库操作的。到时候你会明白,为什么我说Go的接口设计是“少即是多”的典范。

课后练习:动手写一个Go程序,连接你自己的MySQL数据库,执行一条INSERT语句插入一条用户记录,再查询出来。试试看,你会发现Go操作数据库其实很简单。