重新开启simplebank学习!!! 一.创建数据库表 https://dbdiagram.io 可视化数据库工具
1.创建账户表 1 2 3 4 5 6 7 Table accounts as A { id bigserisal [pk] owner varchar balance bigint currency varchar created_at timestamp [default : `now()` ] }
2.创建条目表 //记录账户余额的变化
1 2 3 4 5 6 Table entries { id bigint [pk] account_id bigint [ref : > A.id] amount bigint [not null note:`可以是负或者正` ] created_at timestamp [default : `now()` ] }
3.创建 转账表 1 2 3 4 5 6 7 Table transfers { id bigint [pk] from_account_id bigint [ref : > A.id] to_account_id bigint [ref : > A.id] amount bigint [not null note: `一定不能为空` ] created_at timestamp [default : `now()` ] }
在此之后向列中添加非空约束 例如 :
balance bigint [not null] // 非空约束是一种用于限制数据库表中某列不能为空的约束
枚举
1 2 3 4 Enum Currency{ USD EUR }
向表中添加索引
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 Table accounts as A { id bigserisal [pk] owner varchar [not null] balance bigint [not null] currency varchar [not null] created_at timestamp [default : `now()` ] Indexes { owner } } Table entries { id bigint [pk] account_id bigint [ref : > A.id] amount bigint [not null] created_at timestamp [default : `now()` ] Indexes { account_id } } Table transfers { id bigint [pk] from_account_id bigint [ref : > A.id] to_account_id bigint [ref : > A.id] amount bigint [not null] created_at timestamp [default : `now()` ] Indexes { from_account_id to_account_id (from_account_id,to_account_id) } }
这些做好之后使用导出功能 生成代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 CREATE TABLE "accounts" ( "id" bigserisal PRIMARY KEY, "owner" varchar NOT NULL, "balance" bigint NOT NULL, "currency" varchar NOT NULL, "created_at" timestamp DEFAULT (now()) ); CREATE TABLE "entries" ( "id" bigint PRIMARY KEY, "account_id" bigint, "amount" bigint NOT NULL, "created_at" timestamp DEFAULT (now()) ); CREATE TABLE "transfers" ( "id" bigint PRIMARY KEY, "from_account_id" bigint, "to_account_id" bigint, "amount" bigint NOT NULL, "created_at" timestamp DEFAULT (now()) ); CREATE INDEX ON "accounts" ("owner" ); CREATE INDEX ON "entries" ("account_id" ); CREATE INDEX ON "transfers" ("from_account_id" ); CREATE INDEX ON "transfers" ("to_account_id" ); CREATE INDEX ON "transfers" ("from_account_id" , "to_account_id" ); ALTER TABLE "entries" ADD FOREIGN KEY ("account_id" ) REFERENCES "accounts" ("id" ); ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id" ) REFERENCES "accounts" ("id" ); ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id" ) REFERENCES "accounts" ("id" );
二.Docker 用指令创建容器的时候 一定要注意-p参数 将容器的端口映射到主机上 一定要保证端口不要被占用 否则将会产生问题
拉取镜像语法
1 docker pull <image>:<tag>
开始一个容器指令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 1.docker run --name <container_name> -e <environment_variable> -d <image>:tag : 2.docker run --name some-postgres -e POSTGRES_PASSWORD=mysecret -d postgres # !!!端口映射 -p 5432:5432 //注意防止端口冲突自行更改 示例: docker run --name postgres12 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:12-alpine 3. docker exec -it <contain_name_or_id> <comman> [args] 示例:#进入psql控制台 docker exec -it postgres12 psql -U root 4.显示容器日志 docker logs <container_name_or_id> 示例: docker logs postgres12 5.连接shell 指令: docker exec -it postgres12 /bin/sh # 创建新的数据库 createdb --username=root --owner=root simple_bank # 使用psql连接 psql simple_bank # 删除数据库 dropdb [名称] exit退出shell # 指令结合 docker exec -it postgres12 createdb --username=root --owner=root simple_bank docker exec -it postgres12 psql -U root simple_bank # 查找指令 history | grep "docker run" //linux history | Select-String "docker run"//windows
区分 docker中的 镜像和容器
docker image中包含多个运行 容器的应用实例 类似结构:
- docker image
├──container2
├──container3
三.Tableplus 将sql文件导入到tableplus中
在tableplus中删除表 使用sql指令
1 DROP TABLE accounts CASCADE; / / 注意替换表名称
四.DB migration 迁移指令:
1 migrate create -ext sql -dir db/migration -seq init_schema
up/down migration:理解迁移 类比栈结构 向上新数据表 向下 旧数据表
使用migrate up指令时 Old DB 在文件中 一次按照 1.up.sql 2.up.sql 3.up.sql 依次运行到New DB
使用migrate down指令时 New DB 在文件中依次按照 3.up.sql 2.up.sql 1.up.sql 依次运行到Old DB
old DB schema —–> migrate up —––> x.up.sql —–>New DB schema
<—————————- x.down.sql <———migrate down<————-
将最开始的.sql文件放入 .up.sql中
五.Makefile文件 创建规则后使用 make指令 快速创建
如果你是萌新开始给到你一个项目 你可以通过makefile文件快速构建
1 2 3 4 5 6 7 8 9 10 migrate -help # 通过看日志 知道使用什么指令来工作 # 迁移指令 migrate -path simplebank/db/migration -database "postgresql://root:secret@localhost:5432/simple_bank" -verbose up # 出现ssl错误 添加sslmode=disabled # 出现了一系列的迁移错误 解决方案 强制更改版本 migrate -path simplebank/db/migration -database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" -verbose force 1
六.数据库的CRUD DATAVASE/SQL库
GORM
sqlx(兼容多)
sqlc(最好的 融合了以上两者的优点)
七.使用sqlc
介绍:
sqlc 从 SQL 生成类型安全的代码 。以下是它的工作原理:
您使用 SQL 编写查询。
运行 sqlc 来生成具有这些查询的类型安全接口的代码。
编写调用生成的代码的应用程序代码。
查看一个交互式示例 来了解它的实际应用,以及 sqlc 背后的动机的介绍性博客文章 。
八.sqlc.yaml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 version: "2" sql: - schema: "simplebank/db/migration" queries: "db/query" engine: "postgresql" gen: go : package : "db" out: "simplebank/db/sqlc" sql_package: "pgx/v5" emit_json_tags: true emit_interface: false emit_empty_slices: true overrides: - db_type: "timestamptz" go_type: "time.Time" - db_type: "uuid" go_type: "github.com/google/uuid.UUID"
account.sql
1 2 3 4 5 6 7 8 INSERT INTO accounts ( owner, balance, currency ) VALUES ( $1 , $2 , $3 ) RETURNING * ;
make sqlc 生成代码 1.account.sql.go 2.db.go 3.models.go 在生成之后由于没有 初始化项目 使得项目报红
1 2 go mod init project/simplebank go mod tidy
九.编写单元测试用例 1.导入未使用的包在前面添加_可以防止系统自动将它删除
例如:
_ “github.com/lib/pq “
错误
1 2 3 4 5 cannot use conn (variable of type *sql.DB) as DBTX value in argument to New: *sql.DB does not implement DBTX (wrong type for method Exec) have Exec(string, ...any) (sql.Result, error) want Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, 你遇到的错误信息表明,你尝试将 *sql.DB 类型的 conn 变量用作 DBTX 类型的参数,但 *sql.DB 并不满足 DBTX 接口的要求。具体来说,DBTX 接口期望的 Exec 方法签名是 (context.Context, string, ...interface{}) (pgconn.CommandTag, error),而 *sql.DB 提供的 Exec 方法签名是 (string, ...any) (sql.Result, error)。
目前不理解 方法和接口 开始学习
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 type DBTX interface { Exec(context.Context, string , ...interface {}) (pgconn.CommandTag, error ) Query(context.Context, string , ...interface {}) (pgx.Rows, error ) QueryRow(context.Context, string , ...interface {}) pgx.Row } string , ...interface {}例如,如果有一个函数接受一个字符串和任意数量的其他参数: func myFunction (s string , args...interface {}) { } 可以这样调用这个函数: myFunction("hello" , 1 , "world" , true ) !!!!!!!!!!!!!!!!!!!!!!gpt给出解决问题的方法在我不知道用pgx的情况下 gpt给出用pgx连接 给gpt看完接口定义之后 由于你的 DBTX 接口使用了 pgx 库,你可以使用 pgx 的 Conn 类型而不是 *sql.DB。这里是一个简单的解决方案: 使用 pgx 连接:确保在代码中使用 pgx 的连接,而不是 *sql.DB。修改你的数据库连接部分: import ( "context" "log" "os" "testing" "github.com/jackc/pgx/v4" ) const ( dbDriver = "pgx" dbSource = "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" ) var testQueries *Queriesfunc TestMain (m *testing.M) { conn, err := pgx.Connect(context.Background(), dbSource) if err != nil { log.Fatal("cannot connect to db:" , err) } testQueries = New(conn) os.Exit(m.Run()) } 确保 New 函数接受 pgx.Conn:在你的 New 函数中,确保它接受 pgx.Conn 而不是 *sql.DB。 这样,你的 DBTX 接口与 pgx 的连接就能正常工作,避免了类型不匹配的问题。这样也简化了代码,直接使用了 pgx 连接而不需要适配器。
10.8日 解决目前为止遇见的一个问题 遇见的第一个大问题 在account_test.go中和main_test.go中出现的一些问题
*和姐聊了一下 解决了 **testQueries = New(conn)*中的问题
main_test.go使用 pgx来连接数据库 而不是视频中讲解的sql.open 他们的返回值类型不同造成了错误
测试函数的拼写错误 可能也影响了一大部分
还有最后一处问题
require.NotEmpty(t, err)
这一行存在问题。这里应该是检查account
是否不为空,而不是检查错误err
是否不为空。正确的应该是require.NotEmpty(t, account)
。
不熟悉的地方go语言的包 接口 方法
1.最终的account_test.go代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package dbimport ( "context" "testing" "github.com/stretchr/testify/require" ) func TestCreateAccount (t *testing.T) { arg := CreateAccountParams{ Owner: "xiaozhao" , Balance: 100 , Currency: "USD" , } account, err := testQueries.CreateAccount(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, account) require.Equal(t, arg.Owner, account.Owner) require.Equal(t, arg.Balance, account.Balance) require.Equal(t, arg.Currency, account.Currency) require.NotZero(t, account.ID) require.NotZero(t, account.CreatedAt) }
2.最终的main_test.go代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package dbimport ( "context" "fmt" "os" "testing" "github.com/jackc/pgx/v5" ) var testQueries *Queriesconst ( DATABASE_URL = "postgres://root:secret@localhost:5432/simple_bank?sslmode=disable" ) func TestMain (m *testing.M) { conn, err := pgx.Connect(context.Background(), DATABASE_URL) if err != nil { fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n" , err) os.Exit(1 ) } defer conn.Close(context.Background()) testQueries = New(conn) os.Exit(m.Run()) }
上面是指定了一个一个账户 我们想让账户的主人 货币 钱是随机的 编写util中的random代码 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package utilimport ( "math/rand" "strings" "time" ) const alphabet = "abcdefghijklmnopqrstuvwxyz" var rng *rand.Randfunc init () { source := rand.NewSource(time.Now().UnixNano()) rng = rand.New(source) } func RandomInt (min, max int64 ) int64 { return min + rng.Int63n(max-min+1 ) } func RandomString (n int ) string { var sb strings.Builder k := len (alphabet) for i := 0 ; i < n; i++ { c := alphabet[rng.Intn(k)] sb.WriteByte(c) } return sb.String() } func RandomOwner () string { return RandomString(6 ) } func RandomMoney () int64 { return RandomInt(0 ,1000 ) } func RandowCurrency () string { currencies := []string {"RMB" ,"USD" ,"CAD" } n := len (currencies) return currencies[rand.Intn(n)] }
**学会如何把自己写的包导入到别的文件夹下 这个需要看go mod下的 module project/simplebank **
把moudle中的包作为起始路径 导入到别的文件夹下 就是:”project/simplebank/util”
ok 截止到 10.8日随机生成的数据生成功
问题2
makefile文件中的下面这个指令
test:
go test -v -cover ./… 这个指令必须在当前目录下找到go的测试文件
就是go.mod文件应该和makefile保持在一起 解决方法在本地的go.mod文件夹下又创建了一个makefile 用来测试 make test
类型断言 interface代表为止类型 使用前需要 转换为具体类型 (从未知类型转为已知类型)
如:
1 2 var i interface{} = 2 num1, ok := i.(int)//断言
3.account_test.go代码: 全部测试通过!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 package dbimport ( "context" "project/simplebank/util" "testing" "time" "github.com/stretchr/testify/require" ) func createRandomAccount (t *testing.T) Account { arg := CreateAccountParams{ Owner: util.RandomOwner(), Balance: util.RandomMoney(), Currency: util.RandomCurrency(), } account, err := testQueries.CreateAccount(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, account) require.Equal(t, arg.Owner, account.Owner) require.Equal(t, arg.Balance, account.Balance) require.Equal(t, arg.Currency, account.Currency) require.NotZero(t, account.ID) require.NotZero(t, account.CreatedAt) return account } func TestCreateAccount (t *testing.T) { createRandomAccount(t) } func TestGetAccount (t *testing.T) { account1 := createRandomAccount(t) account2, err := testQueries.GetAccount(context.Background(), account1.ID) require.NoError(t, err) require.NotEmpty(t, account2) require.Equal(t, account1.ID, account2.ID) require.Equal(t, account1.Owner, account2.Owner) require.Equal(t, account1.Balance, account2.Balance) require.Equal(t, account1.Currency, account2.Currency) require.WithinDuration(t, account1.CreatedAt.Time, account2.CreatedAt.Time, time.Second) } func TestUpdateAccount (t *testing.T) { account1 := createRandomAccount(t) arg := UpdateAccountParams{ ID: account1.ID, Balance: util.RandomMoney(), } account2, err := testQueries.UpdateAccount(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, account2) require.Equal(t, account1.ID, account2.ID) require.Equal(t, account1.Owner, account2.Owner) require.Equal(t, arg.Balance, account2.Balance) require.Equal(t, account1.Currency, account2.Currency) require.WithinDuration(t, account1.CreatedAt.Time, account2.CreatedAt.Time, time.Second) } func TestDeleteAccount (t *testing.T) { account1 := createRandomAccount(t) err := testQueries.DeleteAccount(context.Background(), account1.ID) require.NoError(t, err) account2, err := testQueries.GetAccount(context.Background(), account1.ID) require.Error(t, err) require.Empty(t, account2) } func TestListAccount (t *testing.T) { var lastAccount Account for i := 0 ; i < 10 ; i++ { lastAccount = createRandomAccount(t) } arg := ListAccountsParams{ Owner: lastAccount.Owner, Limit: 5 , Offset: 0 , } accounts, err := testQueries.ListAccounts(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, accounts) for _, account := range accounts { require.NotEmpty(t, account) require.Equal(t, lastAccount.Owner, account.Owner) } }
4.entry_test.go 条目上的account.id要和account表单上的di相对应
问题
id为null
在 PostgreSQL 中,如果一个表的 id
字段没有设置为自增序列(如 bigserial
),并且你在插入数据时没有显式地为 id
字段指定值,那么 id
字段的值将会是 NULL
,除非该字段设置了默认值。
解决办法
1 2 创建一个序列:首先,你需要创建一个序列,这个序列将用于生成 id 列的值。 CREATE SEQUENCE entries_id_seq;
1 2 设置序列的所有权:将序列与 id 列关联起来。 ALTER SEQUENCE entries_id_seq OWNED BY entries.id;
1 2 3 设置 id 列的默认值为序列的下一个值:这样,每当你插入新行而没有指定 id 值时,PostgreSQL 会自动使用序列的下一个值。 ALTER TABLE entries ALTER COLUMN id SET DEFAULT nextval('entries_id_seq' );
4确保 id
列是主键 :从你提供的信息来看,id
列已经是主键。确保这一点很重要,因为主键约束可以保证 id
列的值是唯一的。
1 2 测试:插入一条新记录,不指定 id 值,检查是否自动生成了 id。 INSERT INTO entries (account_id, amount, created_at) VALUES (1 , 100 , now());
1 DELETE FROM entries WHERE id= 4 ; 删除特定行的指令
创建账单成功!
但是只能生成一个数据???
我发现了输出的区别 Running tool: D:\Go\bin\go.exe test -timeout 30s -run ^TestCreateEntry$ project/simplebank/db/sqlc
ok project/simplebank/db/sqlc (cached) 这是第二次输出 第一次输出没有cached字样 数据正确的加载到了数据库 但是这个带有cached的数据没有加载到数据库
因为 cached 是因为两次的数据相同 所以才没有被加载到数据库 这个可能是随机数代码的问题
5.transfer_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 package dbimport ( "context" "time" "project/simplebank/util" "testing" "github.com/stretchr/testify/require" ) func createRandomTransfer (t *testing.T, account1, account2 Account) Transfer { arg := createTransferParams{ FromAccountID: account1.ID, ToAccountID: account2.ID, Amount: util.RandomMoney(), } transfer, err := testQueries.createTransfer(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, transfer) require.Equal(t, arg.FromAccountID, transfer.FromAccountID) require.Equal(t, arg.ToAccountID, transfer.ToAccountID) require.Equal(t, arg.Amount, transfer.Amount) require.NotZero(t, transfer.ID) require.NotZero(t, transfer.CreatedAt) return transfer } func TestCreateTransfer (t *testing.T) { account1 := createRandomAccount(t) account2 := createRandomAccount(t) createRandomTransfer(t, account1, account2) } func TestGetTransfer (t *testing.T) { account1 := createRandomAccount(t) account2 := createRandomAccount(t) transfer1 := createRandomTransfer(t, account1, account2) transfer2, err := testQueries.GetTransfer(context.Background(), transfer1.ID) require.NoError(t, err) require.NotEmpty(t, transfer2) require.Equal(t, transfer1.ID, transfer2.ID) require.Equal(t, transfer1.FromAccountID, transfer2.FromAccountID) require.Equal(t, transfer1.ToAccountID, transfer2.ToAccountID) require.Equal(t, transfer1.Amount, transfer2.Amount) require.WithinDuration(t, transfer1.CreatedAt.Time, transfer2.CreatedAt.Time, time.Second) } func TestListTransfer (t *testing.T) { account1 := createRandomAccount(t) account2 := createRandomAccount(t) for i := 0 ; i < 5 ; i++ { createRandomTransfer(t, account1, account2) createRandomTransfer(t, account2, account1) } arg := ListTransfersParams{ FromAccountID: account1.ID, ToAccountID: account1.ID, Limit: 5 , Offset: 5 , } transfers, err := testQueries.ListTransfers(context.Background(), arg) require.NoError(t, err) require.Len(t, transfers, 5 ) for _, transfer := range transfers { require.NotEmpty(t, transfer) require.True(t, transfer.FromAccountID == account1.ID || transfer.ToAccountID == account1.ID) } }
十.db transaction BEGIN语句启动事务
成功 则更新数据库
失败 则回滚事务(保持原来的状态)
代码对不上了 决定先复制粘贴 学习数据库中的知识点
先从config.go开始
十一.config.go 使用viper
创建app.env文件存储配置信息
config.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package utilimport ( "github.com/spf13/viper" ) type Config struct { DATABASE_URL string `mapstructure:"DATABASE_URL"` } func LoadConfig (path string ) (config Config, err error ) { viper.AddConfigPath(path) viper.SetConfigName("app" ) viper.SetConfigType("env" ) viper.AutomaticEnv() err = viper.ReadInConfig() if err != nil { return } err = viper.Unmarshal(&config) return }
使用接口来简化一些操作好好学接口
目前为止更正了大部分问题接着往下学。。。。
store.test.go出现了大问题
报错:
**Running tool: D:\Go\bin\go.exe test -timeout 30s -run ^TestTransferTx$ project/simplebank/db/sqlc >> before: 1984 3906 — FAIL: TestTransferTx (0.03s) e:\projects\simplebank\db\sqlc\store_test.go:83: Error Trace: e:/projects/simplebank/db/sqlc/store_test.go:83 Error: Should NOT be empty, but was {0 0 {0001-01-01 00:00:00 +0000 UTC finite false}} Test: TestTransferTx FAIL FAIL project/simplebank/db/sqlc **0.571s
因为还没编写代码。。。。。草草草草操操操操哦哦操操操这视频叫我看的
十二.需要仔细处理并发 交易 以避免死锁 数据库事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Running tool: D:\Go\bin\go.exe test -timeout 30s -run ^TestTransferTx$ project/simplebank/db/sqlc > > before: 6892 6969 > > tx: 6882 6979 > > tx: 6882 6989 --- FAIL: TestTransferTx (0.04s) e:\projects\simplebank\db\sqlc\store_test.go:102: Error Trace: e:/projects/simplebank/db/sqlc/store_test.go:102 Error: Not equal: expected: 10 actual : 20 Test: TestTransferTx FAIL FAIL project/simplebank/db/sqlc 0.602s FAIL
这个问题出在
account.sql.go他无法阻止一些东西
1 2 3 -- name: GetAccount :one SELECT * FROM accounts WHERE id = $1 LIMIT 1;
在两个终端中并行运行两个事务来观察这个问题
BEGIN;开始事务
ROLLBACK;回滚事务
:第一个终端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 simple_bank=# BEGIN; BEGIN simple_bank=# BEGIN; WARNING: there is already a transaction in progress BEGIN simple_bank=# ROLLBACK; ROLLBACK simple_bank=# BEGIN; BEGIN simple_bank=# SELECT * FROM accounts WHERE id=1 FOR UPDATE; id | owner | balance | currency | created_at ----+----------+---------+----------+---------------------------- 1 | xiaozhao | 100 | USD | 2024-10-08 09:03:03.272176 (1 row)
第二个终端
1 2 3 4 5 simple_bank=# BEGIN; BEGIN simple_bank=# SELECT * FROM accounts WHERE id=1 FOR UPDATE; 这里会被阻止 并且必须等待第一个事务提交或回滚
纠正方法1: 在sql中添加 : 重新用make sqlc生成
1 2 3 4 SELECT * FROM accountsWHERE id = $1 LIMIT 1 FOR UPDATE ;
但是接下来出现了死锁错误:
添加日志寻找错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Running tool: D:\Go\bin\go.exe test -timeout 30s -run ^TestTransferTx$ project/simplebank/db/sqlc >> before: 1826 5993 tx 5 Create transfer tx 5 Create entry 1 tx 5 Create entry 2 tx 5 get account 1 tx 2 Create transfer tx 5 update account 1 tx 5 get account 2 tx 4 Create transfer tx 5 update account 2 tx 3 Create transfer tx 2 Create entry 1 tx 4 Create entry 1 tx 3 Create entry 1 tx 1 Create transfer tx 2 Create entry 2 tx 4 Create entry 2 tx 3 Create entry 2 tx 2 get account 1 tx 4 get account 1 tx 3 get account 1 >> tx: 1816 6003 tx 1 Create entry 1 tx 1 Create entry 2 tx 1 get account 1 --- FAIL: TestTransferTx (0.95s) e:\projects\simplebank\db\sqlc\store_test.go:52: Error Trace: e:/projects/simplebank/db/sqlc/store_test.go:52 Error: Received unexpected error: ERROR: deadlock detected (SQLSTATE 40P01) Test: TestTransferTx FAIL FAIL project/simplebank/db/sqlc 1.470s FAIL
终端事务出现错误:
1 INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 ,2 ,10 ) RETURNING * ;
第一个错误 INSERT INFO transfers
是语法错误,正确的语法是 INSERT INTO transfers
。
第二个错误 INSERT INTO transfers (from_account_id to_account_id amount)
也存在语法错误,缺少逗号分隔列名。正确的写法是 INSERT INTO transfers (from_account_id, to_account_id, amount)
。
当前事务已中止 :
由于之前的 SQL 语句(可能是第一条插入语句)出错,事务被标记为 “aborted”。这意味着在该事务中的所有后续 SQL 命令都将失败,直到事务被回滚。
解决方法
结束当前事务 :
终端阻塞 事务状态 1 2 3 4 5 6 1. 确认当前事务状态 在 PostgreSQL 中,如果一个事务因为某种原因(例如错误或未处理的异常)而中断,那么所有后续的 SQL 语句将会被忽略,直到你执行 ROLLBACK 或 COMMIT。首先,确保没有事务在进行中。 你可以使用以下命令查看当前活动的事务: SELECT * FROM pg_stat_activity WHERE state = 'active';
1 2 3 4 5 6 7 8 9 10 11 12 13 14 simple_bank= # INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 ,2 ,10 ) RETURNING * ; ^ CCancel request sentERROR: canceling statement due to user request CONTEXT: SQL statement "SELECT 1 FROM ONLY "public"."accounts" x WHERE "id" OPERATOR(pg_catalog.=) $1 FOR KEY SHARE OF x" simple_bank= # SELECT * FROM pg_stat_activity WHERE state = 'active' ; datid | datname | pid | usesysid | usename | application_name | client_addr | client_hostname | client_port | backend_start | xact_start | query_start | state_change | wait_event_type | wait_event | state | backend_xid | backend_xmin | query | backend_type 16385 | simple_bank | 810 | 10 | root | psql | | | -1 | 2024 -10 -10 12 :58 :51.115963 + 00 | 2024 -10 -10 13 :00 :09.094842 + 00 | 2024 -10 -10 13 :00 :09.094842 + 00 | 2024 -10 -10 13 :00 :09.094867 + 00 | | | active | | 935 | SELECT * FROM pg_stat_activity WHERE state = 'active' ; | client backend 16385 | simple_bank | 802 | 10 | root | psql | | | -1 | 2024 -10 -10 12 :58 :07.854964 + 00 | 2024 -10 -10 12 :58 :09.850035 + 00 | 2024 -10 -10 12 :58 :09.850035 + 00 | 2024 -10 -10 12 :58 :09.850039 + 00 | Lock | tuple | active | 965 | 935 | INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 ,2 ,10 ) RETURNING * ; | client backend 16385 | simple_bank | 782 | 10 | root | psql | | | -1 | 2024 -10 -10 12 :51 :58.685461 + 00 | 2024 -10 -10 12 :57 :47.014409 + 00 | 2024 -10 -10 12 :57 :47.014409 + 00 | 2024 -10 -10 12 :57 :47.014413 + 00 | Lock | tuple | active | 963 | 935 | INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 ,2 ,10 ) RETURNING * ; | client backend 16385 | simple_bank | 775 | 10 | root | psql | | | -1 | 2024 -10 -10 12 :51 :51.971697 + 00 | 2024 -10 -10 12 :52 :16.478898 + 00 | 2024 -10 -10 12 :57 :54.280622 + 00 | 2024 -10 -10 12 :57 :54.280627 + 00 | Lock | tuple | active | 964 | 935 | INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 ,2 ,10 ) RETURNING * ; | client backend 16385 | simple_bank | 556 | 10 | root | psql | | | -1 | 2024 -10 -10 11 :29 :44.776508 + 00 | 2024 -10 -10 11 :56 :11.909182 + 00 | 2024 -10 -10 11 :56 :24.551023 + 00 | 2024 -10 -10 11 :56 :24.551027 + 00 | Lock | transactionid | active | | 935 | SELECT * FROM accounts WHERE id= 1 FOR UPDATE ; | client backend 16385 | simple_bank | 638 | 10 | root | psql
从你提供的 pg_stat_activity
的查询结果来看,有多个活跃的事务正在等待锁。这可能是导致你无法执行 INSERT
操作的原因。以下是一些步骤,可以帮助你诊断并解决这个问题:
1. 识别并终止阻塞的事务 你的查询结果显示了多个 INSERT
操作在等待锁。可以尝试强制终止这些阻塞的事务,以释放锁并允许你的操作继续进行。首先,记录下阻塞的 pid
(进程 ID),然后使用以下命令终止它们:
1 SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid IN (802, 782, 775);
在这个例子中,802
、782
和 775
是阻塞的 pid
。请根据你的实际情况调整这条命令。
2. 确认事务的状态 如果终止了这些连接,确保你没有其他活跃的事务在运行。你可以再次运行以下命令确认:
1 SELECT * FROM pg_stat_activity WHERE state = 'active';
3. 尝试再次执行插入 在确保没有其他事务阻塞后,尝试重新执行你的插入操作:
1 2 3 4 5 6 7 BEGIN ; INSERT INTO transfers (from_account_id, to_account_id, amount) VALUES (1 , 2 , 10 ) RETURNING * ; COMMIT ; 指令; SELECT * FROM accounts WHERE id= 1 FOR UPDATE ;UPDATE accounts SET balance = 600 WHERE id= 1 ;
4. 如果问题依然存在 如果问题依然存在,建议检查数据库的锁状态:
1 SELECT * FROM pg_locks WHERE granted = false;
又产生错误
1 2 3 4 5 6 simple_bank=# SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid IN (802, 782, 775,556,638,810); FATAL: terminating connection due to administrator command server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. The connection to the server was lost. Attempting reset: Succeeded.
回答:
看起来你在尝试终止阻塞的连接时,PostgreSQL 服务器意外关闭了连接。这可能是由于以下几个原因:
服务器崩溃 :可能是因为在终止连接时,某个后台进程遇到了问题,导致服务器崩溃。
资源不足 :如果你的 PostgreSQL 服务器资源不足(例如内存或 CPU),在处理请求时可能会出现这种情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 由于服务器崩溃,重启 Docker 容器可能会帮助你恢复连接: docker restart postgres12 在重启后检查连接: 重启后,尝试重新连接到数据库,并检查活动连接: SELECT * FROM pg_stat_activity; 再次终止阻塞的连接: 如果连接正常,尝试再次运行终止命令: SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid IN (802, 782, 775,
git上传一个项目没有共同历史
1 2 检查是否有共同历史 git log --oneline --graph --all
1 2 强制合并冲突 git pull origin main --allow-unrelated-histories
终端1:在没有阻塞的情况下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 simple_bank= # BEGIN ; BEGIN simple_bank= # SELECT * FROM accounts WHERE id= 1 FOR UPDATE ; id | owner | balance | currency | created_at 1 | xiaozhao | 100 | USD | 2024 -10 -08 09 :03 :03.272176 (1 row ) simple_bank= # UPDATE accounts SET balance = 600 WHERE id= 1 ; UPDATE 1 simple_bank= # COMMIT ; COMMIT simple_bank= #
在终端一提交事务时 终端二会显示出结果
1 2 3 4 5 6 7 8 9 10 11 simple_bank= # BEGIN ; BEGIN simple_bank= # SELECT * FROM accounts WHERE id= 1 FOR UPDATE ; id | owner | balance | currency | created_at 1 | xiaozhao | 600 | USD | 2024 -10 -08 09 :03 :03.272176 (1 row ) simple_bank= #
sql QUERIER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 BEGIN ;INSERT INTO transfers (from_account_id,to_account_id,amount) VALUE (1 ,2 ,10 ) RETURNING * ;INSERT INTO entries (account_id,amount) VALUES (1 ,-10 ) RETURNING * ;INSERT INTO entries (account_id,amount) VALUES (2 ,10 ) RETURNING * ;SELECT * FROM accounts WHERE id= 1 FOR UPDATE ;UPDATE accounts SET balance = 90 WHERE id = 1 RETURNING * ;SELECT * FROM accounts WHERE id = 2 FOR UPDATE ;UPDATE accounts SET balance = 110 WHERE id = 2 RETURNING * ;ROLLBACK ;
5.postgres lock:帮助查询哪里有锁 The following query may be helpful to see what processes are blocking SQL statements (these only find row-level locks, not object-level locks).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 SELECT blocked_locks.pid AS blocked_pid, blocked_activity.usename AS blocked_user, blocking_locks.pid AS blocking_pid, blocking_activity.usename AS blocking_user, blocked_activity.query AS blocked_statement, blocking_activity.query AS current_statement_in_blocking_process FROM pg_catalog.pg_locks blocked_locks JOIN pg_catalog.pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid JOIN pg_catalog.pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype AND blocking_locks.database IS NOT DISTINCT FROM blocked_locks.database AND blocking_locks.relation IS NOT DISTINCT FROM blocked_locks.relation AND blocking_locks.page IS NOT DISTINCT FROM blocked_locks.page AND blocking_locks.tuple IS NOT DISTINCT FROM blocked_locks.tuple AND blocking_locks.virtualxid IS NOT DISTINCT FROM blocked_locks.virtualxid AND blocking_locks.transactionid IS NOT DISTINCT FROM blocked_locks.transactionid AND blocking_locks.classid IS NOT DISTINCT FROM blocked_locks.classid AND blocking_locks.objid IS NOT DISTINCT FROM blocked_locks.objid AND blocking_locks.objsubid IS NOT DISTINCT FROM blocked_locks.objsubid AND blocking_locks.pid != blocked_locks.pid JOIN pg_catalog.pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid WHERE NOT blocked_locks.granted;
1 SELECT * FROM accounts WHERE id= 1 FOR UPDATE ; 这条语句阻塞了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Here's an alternate view of that same data that includes an idea how old the state is # 列出所有锁 SELECT a.datname, l.relation::regclass, l.transactionid, //事务id l.mode, 锁的mod l.GRANTED, a.usename, who a.query, a.query_start, age(now(), a.query_start) AS "age", a.pid FROM pg_stat_activity a JOIN pg_locks l ON l.pid = a.pid ORDER BY a.query_start;
死锁是由外键约束引起的
1.删除约束
修改sql
1 2 3 4 SELECT * FROM accountsWHERE id = $1 LIMIT 1 FOR NO KEY UPDATE ;/ / 这步时解决死锁的关键
避免死锁是关键:微调事务中的查询
十三.隔离级别 数据库事务必须满足 ACID 原子性 一致性 隔离性 持久性
Read Phenomenaa 1.脏读 当一个事务读取了 其他并发事务写入的尚未提交的数据(导致 如果尚未提交的数据 最终回滚 可能导致用到错误的数据 )
2.不可重复读 当一个事务两次读取到同一记录并看到不同的值 因为第一次读取后提交的其他事务修改
3.幻读 影响多行
4.四种隔离级别 READ UNCOMMITMED: 可以看到其他未提交事务写入的数据
READ COMMITED:只能看到其他事务已经提交的数据
REPEATABLE READ:
SERIALIZABLE:
5.mysql选择隔离级别 1 2 3 set sexxion transaction isolation level read commited;select @@一种隔离级别
6.postgresql选择隔离级别 只有三个 1 2 3 4 5 在postgresql中 未提交和已提交是一个级别 show transaction isolation level set transaction isolation level read uncommited
十四.持续集成或CI 自动化构建和测试流程进行验证
1.Github Action 首先上传项目到github时如果出现了连接问题 就切换成ssh连接
1 2 3 4 git remote set-url origin git@github.com:Whuichenggong/projects.git PS E:\projects> git pull origin main --tags From github.com:Whuichenggong/projects * branch main -> FETCH_HEAD
创建文件
1 echo. > .github\workflows\ci.yml
安装工具
但是目前我看不到页面我的action
十五.RESTful HEEP API 1.创建api文件夹 account.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package apiimport ( "net/http" db "project/simplebank/db/sqlc" "github.com/gin-gonic/gin" ) type CreateAccountRequest struct { Owner string `json:"owner" binding:"required"` Currency string `json:"currency" binding:"required,oneof= USD EUR"` } func (server *Server) createAccount(ctx *gin.Context) { var req CreateAccountRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return } arg := db.CreateAccountParams{ Owner: req.Owner, Currency: req.Currency, Balance: 0 , } account, err := server.store.CreateAccount(ctx, arg) if err != nil { ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } ctx.JSON(http.StatusOK, account) }
server.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package apiimport ( db "project/simplebank/db/sqlc" "project/simplebank/util" "github.com/gin-gonic/gin" ) type Server struct { config util.Config store db.Store router *gin.Engine } func NewServer (config util.Config, store db.Store) (*Server, error ) { server := &Server{ config: config, store: store, } router := gin.Default() router.POST("/accounts" , server.createAccount) server.router = router return server, nil } func errorResponse (err error ) gin.H { return gin.H{"error" : err.Error()} } func (server *Server) Start(address string ) error { return server.router.Run(address) }
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 package mainimport ( "context" "log" "project/simplebank/api" "project/simplebank/util" db "project/simplebank/db/sqlc" "github.com/jackc/pgx/v5/pgxpool" ) func main () { config, err := util.LoadConfig("." ) if err != nil { log.Fatal("cannot load config:" , err) } connPool, err := pgxpool.New(context.Background(), config.DATABASE_URL) if err != nil { log.Fatal("cannot connect to db:" , err) } store := db.NewStore(connPool) runGinServer(config, store) if err != nil { log.Fatal("cannot start server:" , err) } } func runGinServer (config util.Config, store db.Store) { server, err := api.NewServer(config, store) if err != nil { log.Fatalf("cannot create server: %v" , err) } err = server.Start(config.HTTPServerAddress) if err != nil { log.Fatalf("cannot start server: %v" , err) } }
数据库重置
1 2 3 4 5 6 7 8 9 10 11 MySQL 数据库: 使用 TRUNCATE TABLE 语句: TRUNCATE TABLE table_name; PostgreSQL 数据库: 使用 TRUNCATE TABLE 语句: TRUNCATE TABLE table_name RESTART IDENTITY ; TRUNCATE TABLE accounts, entries RESTART IDENTITY ; 同时截断两个表
listaccount.go
用postman请求时://查询参数
page_id 1
page_size 5
在使用多组查找的时候没有找到用户???
目前为止还是无法解决
找了喜春学哥帮我找到了问题的所在在ListAccounts中 传进去的arg.Owner是个空值导致了出现了问题 把arg.Owner改成一个数据库中具体的值 就能找到问题的所在
1 2 func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error ) { rows, err := q.db.Query(ctx, listAccounts, arg.Owner, arg.Limit, arg.Offset)
2.模拟数据库测试 1 2 3 4 使用mock go get github.com/golang/mock/mockgen@v1.6.0 PS E:\projects\simplebank\db\mock> mockgen -destination db/mock/store.go project/simplebank/db/sqlc Store
3.account_test.go 出现的问题
你提到的问题是由于 mock_sqlc.MockStore
未完全实现 db.Store
接口,特别是缺少 createTransfer
方法。为了解决这个问题,您可以采取以下步骤:
解决步骤:
确认 db.Store
接口的定义:
首先,确保 db.Store
接口定义了所有需要的方法。特别是,确认接口中是否包含 createTransfer
方法。
也就是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 在你当前的测试代码中,store := mockdb.NewMockStore(ctrl) 返回的确实是 *mockdb.MockStore 类型,而 NewServer 需要的参数是 db.Store 接口类型。那么为什么没有类型错误呢?这是因为在 Go 中,接口是基于方法集实现的,而 *mockdb.MockStore 实现了 db.Store 接口中的所有方法。 具体原因分析: 接口实现方式:在 Go 语言中,接口并不关心你传递的具体类型(如 *mockdb.MockStore),它只关心该类型是否实现了接口中定义的所有方法。如果 *mockdb.MockStore 实现了 db.Store 接口的所有方法,那么它就可以被赋值给 db.Store 类型的变量。 gomock 的自动生成:你使用 gomock 生成了 *mockdb.MockStore,这个 mock 类型会模拟 db.Store 接口的所有方法。因为它是通过 gomock 自动生成的,并且已经包含了 db.Store 中的所有方法,所以它实际上是符合 db.Store 接口的实现。 类型匹配:在 Go 中,赋值 *mockdb.MockStore 给 db.Store 类型是可以的,因为 *mockdb.MockStore 实现了 db.Store 接口。即便 *mockdb.MockStore 是一个具体类型,只要它的方法集与 db.Store 接口的方法集匹配,Go 会认为它是一个合法的接口实现。 为什么没有错误? 由于 *mockdb.MockStore 实现了 db.Store 接口的所有方法,Go 编译器允许将 *mockdb.MockStore 传递给 NewServer 这个需要 db.Store 类型的函数参数。具体的原因是: NewMockStore 生成的 mock 类型实现了 db.Store 的所有方法,因此符合 db.Store 接口。 在 Go 语言中,接口实现是隐式的,不需要显式声明实现接口,只要结构体的方法集与接口匹配即可。
方法 1:使用类型断言验证
在 Go 中,你可以通过静态类型检查 来验证一个类型是否实现了某个接口。具体方法是使用以下代码:
1 var _ db.Store = (*mockdb.MockStore)(nil)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 package apiimport ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" db "project/simplebank/db/sqlc" "project/simplebank/util" "testing" mockdb "project/simplebank/db/mock" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) func TestGetAccountAPI (t *testing.T) { config, err := util.LoadConfig("." ) if err != nil { fmt.Println("配置文件出错" ) } account := randomAccount() testCases := []struct { name string accountID int64 buildStubs func (store *mockdb.MockStore) checkResponse func (t *testing.T, recoder *httptest.ResponseRecorder) }{ { name: "OK" , accountID: account.ID, buildStubs: func (store *mockdb.MockStore) { store.EXPECT(). GetAccount(gomock.Any(), gomock.Eq(account.ID)). Times(1 ). Return(account, nil ) }, checkResponse: func (t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusOK, recorder.Code) requireBodyMatchAccount(t, recorder.Body, account) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func (t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() store := mockdb.NewMockStore(ctrl) tc.buildStubs(store) server, _ := NewServer(config, store) recorder := httptest.NewRecorder() url := fmt.Sprintf("/accounts/%d" , tc.accountID) request, err := http.NewRequest(http.MethodGet, url, nil ) require.NoError(t, err) server.router.ServeHTTP(recorder, request) tc.checkResponse(t, recorder) }) } } func randomAccount () db.Account { return db.Account{ ID: util.RandomInt(1 , 1000 ), Balance: util.RandomMoney(), Currency: util.RandomCurrency(), } } func requireBodyMatchAccount (t *testing.T, body *bytes.Buffer, account db.Account) { data, err := io.ReadAll(body) require.NoError(t, err) var gotAccount db.Account err = json.Unmarshal(data, &gotAccount) require.NoError(t, err) require.Equal(t, account, gotAccount) }
在这段代码中有不懂的地方
1 2 3 4 5 6 7 8 9 store := mockdb.NewMockStore(ctrl) tc.buildStubs(store) server, _ := NewServer(config, store) store是 *mockdb.MockStore类型 而func NewServer (config util.Config, store db.Store) (*Server, error ) 需要的是db.store类型 server, _ := NewServer(config, store)
切片
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 testCases := []struct { name string accountID int64 buildStubs func (store *mockdb.MockStore) checkResponse func (t *testing.T, recoder *httptest.ResponseRecorder) }{ { name: "OK" , accountID: account.ID, buildStubs: func (store *mockdb.MockStore) { store.EXPECT(). GetAccount(gomock.Any(), gomock.Eq(account.ID)). Times(1 ). Return(account, nil ) }, checkResponse: func (t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusOK, recorder.Code) requireBodyMatchAccount(t, recorder.Body, account) }, }, { name: "NotFound" , accountID: account.ID, buildStubs: func (store *mockdb.MockStore) { store.EXPECT(). GetAccount(gomock.Any(), gomock.Eq(account.ID)). Times(1 ). Return(db.Account{}, sql.ErrNoRows) }, checkResponse: func (t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusNotFound, recorder.Code) }, }, }
目前的问题是notfound处理不符合预期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { name: "NotFound" , accountID: account.ID, setupAuth: func (t *testing.T, request *http.Request, tokenMaker token.Maker) { addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func (store *mockdb.MockStore) { store.EXPECT(). GetAccount(gomock.Any(), gomock.Eq(account.ID)). Times(1 ). Return(db.Account{}, db.ErrRecordNotFound) }, checkResponse: func (t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusNotFound, recorder.Code) }, },
4.transfer.go Currency string json:"currency" binding:"required,currency"
添加了currency验证器 因为正常json不能识别USD等货币
实现思路 在go run mian.go后使用gin框架请求路由前 使用自己添加的数字验证器
在server.go中添加如下内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func validCurrency (fl validator.FieldLevel) bool { currency := fl.Field().String() return currency == "USD" } func (server *Server) setupValidator() { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("currency" , validCurrency) } } func NewServer (config util.Config, store db.Store) (*Server, error ) { server := &Server{ config: config, store: store, } server.setupValidator() router := gin.Default() router.POST("/accounts" , server.createAccount) router.GET("/accounts/:id" , server.getAccount) router.GET("/accounts" , server.listAccount) router.POST("transfers" , server.createTransfer) server.router = router return server, nil } func errorResponse (err error ) gin.H { return gin.H{"error" : err.Error()} } func (server *Server) Start(address string ) error { return server.router.Run(address) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package apiimport ( "errors" "fmt" "net/http" db "project/simplebank/db/sqlc" "github.com/gin-gonic/gin" ) type transferRequest struct { FromAccountID int64 `json:"from_account" binding:"required,min=1"` ToAccountID int64 `json:"to_account" binding:"required,min=1"` Amount int64 `json:"amount" binding:"required,gt=0"` Currency string `json:"currency" binding:"required,currency"` } func (server *Server) createTransfer(ctx *gin.Context) { var req transferRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return } fromAccount, valid := server.validAccount(ctx, req.FromAccountID, req.Currency) if !valid { return } toAccount, valid := server.validAccount(ctx, req.ToAccountID, req.Currency) if !valid { return } arg := db.TransferTxParams{ FromAccountID: fromAccount.ID, ToAccountID: toAccount.ID, Amount: req.Amount, } result, err := server.store.TransferTx(ctx, arg) if err != nil { ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } ctx.JSON(http.StatusOK, result) } func (server *Server) validAccount(ctx *gin.Context, accountID int64 , currency string ) (db.Account, bool ) { account, err := server.store.GetAccount(ctx, accountID) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { ctx.JSON(http.StatusNotFound, errorResponse(err)) return account, false } ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return account, false } if account.Currency != currency { err := fmt.Errorf("account [%d] currency mismatch: %s vs %s" , account.ID, account.Currency, currency) ctx.JSON(http.StatusBadRequest, errorResponse(err)) return account, false } return account, true }
用postman测试得到的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 { "transfer" : { "id" : 35 , "from_account_id" : 3 , "to_account_id" : 5 , "amount" : 12 , "created_at" : "2024-10-20T04:53:01.433988Z" } , "from_account" : { "id" : 3 , "owner" : "afmxtl" , "balance" : 103 , "currency" : "USD" , "created_at" : "2024-10-13T13:33:43.423875Z" } , "to_account" : { "id" : 5 , "owner" : "bdupue" , "balance" : 119 , "currency" : "USD" , "created_at" : "2024-10-13T13:37:04.113466Z" } , "from_entry" : { "id" : 45 , "account_id" : 3 , "amount" : -12 , "created_at" : "2024-10-20T04:53:01.433988Z" } , "to_entry" : { "id" : 46 , "account_id" : 5 , "amount" : 12 , "created_at" : "2024-10-20T04:53:01.433988Z" } }
十六.用户身份验证和授权 1.建user数据库表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 / / Use DBML to define your database structure/ / Docs: https:/ / dbml.dbdiagram.io/ docsTable user as U{ username carchar [pk] hashed_paassword varchar [not null ] full_name varchar [not null ] email varchar [unique , not null ] password_changed_at timestamp [not null , default : `0001 -01 -01 00 :00 :00 Z`] create_at timestamptz [not null ,default : `now()`] } Table accounts as A { id bigser [pk] owner varchar [ref :> U.username,not null ] balance bigint [not null ] currency varchar [not null ] created_at timestamp [not null ,default : `now()`] Indexes { (owner, currency) [unique ] } } Table entries { id bigint [pk] account_id bigint [ref : > A.id,not null ] amount bigint [not null ] created_at timestamp [not null ,default : `now()`] Indexes { account_id } } Table transfers { id bigint [pk] from_account_id bigint [ref : > A.id,not null ] to_account_id bigint [ref : > A.id,not null ] amount bigint [not null ] created_at timestamp [not null ,default : `now()`] Indexes { from_account_id to_account_id (from_account_id,to_account_id) } }
新建数据库迁移:
1 migrate create -ext sql -dir db/migration -seq add_users
出现了错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 make migrateup migrate - path simplebank/ db/ migration - database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" - verbose up 2024 / 10 / 20 15 :11 :16 Start buffering 2 / u add_users2024 / 10 / 20 15 :11 :16 Read and execute 2 / u add_users2024 / 10 / 20 15 :11 :16 error: migration failed: syntax error at or near "00" (column 69 ) in line 6 : CREATE TABLE "user" ( "username" carchar PRIMARY KEY, "hashed_paassword" varchar NOT NULL , "full_name" varchar NOT NULL , "email" varchar UNIQUE NOT NULL , "password_changed_at" timestamp NOT NULL DEFAULT (0001 -01 -01 00 :00 :00 Z), "create_at" timestamptz NOT NULL DEFAULT (now()) ); ALTER TABLE "accounts" ADD FOREIGN KEY ("owner") REFERENCES "user" ("username");ALTER TABLE "acounts" ADD CONSTRAINT "owner_currency-unique" UNIQUE ("owner", "currency") (details: pq: syntax error at or near "00")make: * * * [migrateup] 错误 1
原因:违反了外键约束
十七.迁移失败原因 sql语句写错了 IF写成ID
**理解去除外键等 **
2.问题: 在执行数据库迁移时,出现的错误是因为在 accounts
表上有外键依赖 (transfers
表中的 transfers_from_account_id_fkey
和 transfers_to_account_id_fkey
约束依赖于 accounts
表)。当你尝试删除 accounts
表时,PostgreSQL 不允许删除这个表,因为还有其他表(如 transfers
)依赖它。
但是执行了migrateup指令就会出现脏读现象 使得数据库版本变为2 所以我们要先回退到1版本
1 2 3 4 5 6 7 make migratedown migrate - path simplebank/ db/ migration - database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" - verbose down 2024 / 10 / 20 15 :15 :34 Are you sure you want to apply all down migrations? [y/ N]y 2024 / 10 / 20 15 :15 :36 Applying all down migrations2024 / 10 / 20 15 :15 :36 error: Dirty database version 2. Fix and force version.make: * * * [migratedown] 错误 1
修改迁移表的值为 FALSE:没管用
1 2 3 4 5 6 7 8 9 10 11 执行migratedown操作时失败,并出现错误信息 “cannot drop table accounts because other objects depend on it”,这表明accounts表有其他数据库对象依赖于它。 原因包括: transfers表中的外键约束引用了accounts表。 直接删除含外键的表会引发错误。 建议: 修改迁移脚本,先删除依赖的对象,如约束、触发器、视图等。 使用CASCADE选项强制删除所有依赖的对象。 在 makefile 中为migrate命令添加条件检查。 可能的迁移修正示例: DROP TABLE IF EXISTS transfers CASCADE;DROP TABLE IF EXISTS accounts;
3.解除外键约束 1 2 3 4 5 6 7 8 9 10 11 解决方案: 你可以按以下步骤修改你的迁移文件,确保先删除外键约束,再删除相关的表。 删除外键约束: 在迁移文件中,先删除 transfers 表中的外键约束: ALTER TABLE transfers DROP CONSTRAINT IF EXISTS transfers_from_account_id_fkey;ALTER TABLE transfers DROP CONSTRAINT IF EXISTS transfers_to_account_id_fkey;删除表: 然后,按顺序删除表: DROP TABLE IF EXISTS transfers;DROP TABLE IF EXISTS entries;DROP TABLE IF EXISTS accounts;
migrate 出现的错误可能就是 把外键依赖删除然后在执行数据库迁移语句
有没有可能你在写数据库迁移的时候就没有在migratedown中加入 删除外键约束的语句呢从而导致这么麻烦???
编写migratedown时操作是和migrateup相反的
4.问题: 1 2 3 4 make migratedown1 migrate - path simplebank/ db/ migration - database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" - verbose down 1 2024 / 10 / 20 17 :22 :21 error: Dirty database version 1. Fix and force version.make: * * * [migratedown1] 错误 1
这个错误信息表明数据库处于 “dirty” 状态,通常意味着上一次的迁移未成功完成,导致数据库的迁移版本与实际执行情况不一致。要解决这个问题,你可以尝试以下步骤:
5.解决: 检查数据库的迁移状态
使用 migrate
查看当前的迁移状态,以确认哪个版本是 dirty:
1 migrate - path simplebank/ db/ migration - database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" version
这将显示当前数据库的版本号以及 dirty 状态。
强制迁移版本
使用 migrate force
命令将数据库状态恢复为干净的版本。你可以将其设置为版本 1,这样可以清除 “dirty” 标志,同时保留当前的版本号。
执行以下命令:
1 migrate - path simplebank/ db/ migration - database "postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable" force 1
这个命令不会运行任何迁移,它只是将数据库的迁移版本重置为 1,同时将脏状态清除。
6.检查当前数据库中的约束名 如果不确定数据库中的约束名,可以通过以下 SQL 查询当前表中的约束名称:
1 SELECT conname FROM pg_constraint WHERE conrelid = 'accounts' ::regclass;
每条语句后面要写分号啊啊啊啊!!!!!
十八. user_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package dbimport ( "context" util "project/simplebank/util" "testing" "time" "github.com/stretchr/testify/require" ) func createRandomUser (t *testing.T) User { arg := CreateUserParams{ Username: util.RandomOwner(), HashedPassword: "secret" , FullName: util.RandomOwner(), Email: util.RandomEmail(), } user, err := testStore.CreateUser(context.Background(), arg) require.NoError(t, err) require.NotEmpty(t, user) require.Equal(t, arg.Username, user.Username) require.Equal(t, arg.HashedPassword, user.HashedPassword) require.Equal(t, arg.FullName, user.FullName) require.Equal(t, arg.Email, user.Email) require.NotZero(t, user.CreateAt) return user } func TestCreateUser (t *testing.T) { createRandomUser(t) } func TestGetUser (t *testing.T) { user1 := createRandomUser(t) user2, err := testStore.GetUser(context.Background(), user1.Username) require.NoError(t, err) require.NotEmpty(t, user2) require.Equal(t, user1.Username, user2.Username) require.Equal(t, user1.HashedPassword, user2.HashedPassword) require.Equal(t, user1.FullName, user2.FullName) require.Equal(t, user1.Email, user2.Email) require.WithinDuration(t, user1.PasswordChangedAt.Time, user2.PasswordChangedAt.Time, time.Second) require.WithinDuration(t, user1.CreateAt.Time, user2.CreateAt.Time, time.Second) }
在第29行代码有一个断言语句判断 :
1 require.True(t, user.PasswordChangedAt.Time.IsZero())
这个语句目前不能通过测试 往后看吧看看是么时候找到问题
1. 10.23外键约束问题 运行真个包测试出现的问题
1 这个外键错误提示 "ERROR: insert or update on table"accounts"violates foreign key constraint"accounts_owner_fkey"(SQLSTATE 23503)" 意味着在尝试往 "accounts" 表中插入或更新数据时违反了名为 "accounts_owner_fkey" 的外键约束。
应该是 一个用户链接到账户 这就是主表与副表的关系 设置外键 将两个表链接到一起
2.数据库表出现错误 数据库语句就写错了 正常每个表的 id序列都应该是自增的 如果不是这样将会出现以下错误
1 ERROR: null value in column "id" violates not-null constraint (SQLSTATE 23502)
我们要重新修改数据库迁移语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 CREATE TABLE "accounts" ( "id" bigserial PRIMARY KEY, "owner" varchar NOT NULL, "balance" bigint NOT NULL, "currency" varchar NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()) ); CREATE TABLE "entries" ( "id" bigserial PRIMARY KEY, "account_id" bigint NOT NULL, "amount" bigint NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()) ); CREATE TABLE "transfers" ( "id" bigserial PRIMARY KEY, "from_account_id" bigint NOT NULL, "to_account_id" bigint NOT NULL, "amount" bigint NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()) ); ALTER TABLE "entries" ADD FOREIGN KEY ("account_id" ) REFERENCES "accounts" ("id" ); ALTER TABLE "transfers" ADD FOREIGN KEY ("from_account_id" ) REFERENCES "accounts" ("id" ); ALTER TABLE "transfers" ADD FOREIGN KEY ("to_account_id" ) REFERENCES "accounts" ("id" ); CREATE INDEX ON "accounts" ("owner" ); CREATE INDEX ON "entries" ("account_id" ); CREATE INDEX ON "transfers" ("from_account_id" ); CREATE INDEX ON "transfers" ("to_account_id" ); CREATE INDEX ON "transfers" ("from_account_id" , "to_account_id" ); COMMENT ON COLUMN "entries" ."amount" IS 'can be negative or positive' ; COMMENT ON COLUMN "transfers" ."amount" IS 'must be positive' ;
修改过后 正常运行account_test.go
3.修改状态码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 account, err := server.store.CreateAccount(ctx, arg) if err != nil { if pqErr, ok := err.(*pq.Error); ok { switch pqErr.Code.Name() { case "foreign_key_violation" , "unique_violation" : ctx.JSON(http.StatusForbidden, errorResponse(err)) return default : log.Println(pqErr.Code.Name()) } ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } ctx.JSON(http.StatusOK, account) }
出现错误了 等待明天修改
10.24
将上述代码语句修改为
1 2 3 4 5 6 if err != nil { errCode := db.ErrorCode(err) if errCode == db.ForeignKeyViolation || errCode == db.UniqueViolation { ctx.JSON(http.StatusForbidden, errorResponse(err)) return }
成功解决了问题 。 这是为什么呢??
应该是:
1 if errCode == db.ForeignKeyViolation || errCode == db.UniqueViolation
这段代码起到了主要i作用
在error.go中
1 2 3 4 const ( ForeignKeyViolation = "23503" UniqueViolation = "23505" )
这代表了:
1 2 3 ForeignKeyViolation 常量的值是 "23503",它代表 PostgreSQL 中的一个错误代码。当执行的数据库操作违反外键约束时,会触发这个错误。外键约束保证了不同表之间的关系,如果尝试插入、更新或删除的数据并不能被其他表中的相关记录引用,就会抛出这个错误。 UniqueViolation 常量的值是 "23505",这也是一个 PostgreSQL 错误代码。当向需要唯一值的字段插入了重复的值时,会引发这个错误。违反唯一性约束意味着这样的操作将导致两个记录含有相同的值,这在数据库规则中通常是不允许的,因为唯一约束保护了记录唯一识别数据的能力。
十九.在数据库中安全的存储密码 1.password.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package utilimport ( "fmt" "golang.org/x/crypto/bcrypt" ) func HashPassword (password string ) (string , error ) { hashedPassword, err := bcrypt.GenerateFromPassword([]byte (password), bcrypt.DefaultCost) if err != nil { return "" , fmt.Errorf("哈希加密失败:%w" , err) } return string (hashedPassword), nil } func CheckPassword (password string , hashedPassword string ) error { return bcrypt.CompareHashAndPassword([]byte (hashedPassword), []byte (password)) }
2.password_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package utilimport ( "testing" "github.com/stretchr/testify/require" "golang.org/x/crypto/bcrypt" ) func TestPassword (t *testing.T) { password := RandomString(6 ) hashPassword, err := HashPassword(password) require.NoError(t, err) err = CheckPassword(password, hashPassword) require.NoError(t, err) wrongPassword := RandomString(6 ) err = CheckPassword(wrongPassword, hashPassword) require.EqualError(t, err, bcrypt.ErrMismatchedHashAndPassword.Error()) }
3.user.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 package apiimport ( "net/http" db "project/simplebank/db/sqlc" util "project/simplebank/util" "github.com/gin-gonic/gin" ) type CreateUserRequest struct { Username string `json:"username" binding:"required,alphanum"` FullName string `json:"fullname" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } func (server *Server) createUser(ctx *gin.Context) { var req CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return } hashedPassword, err := util.HashedPassword(req.Password) if err != nil { ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } arg := db.CreateUserParams{ Username: req.Username, FullName: req.FullName, Email: req.Email, HashedPassword: hashedPassword, } account, err := server.store.CreateUser(ctx, arg) if err != nil { errCode := db.ErrorCode(err) if errCode == db.UniqueViolation { ctx.JSON(http.StatusForbidden, errorResponse(err)) return } ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } ctx.JSON(http.StatusOK, account) }
返回结果
1 2 3 4 5 6 7 8 { "username" : "ZhongHe" , "hashed_password" : "$2a$10$RRGhHuYmPf9tRVPDckNI5.q6VJ1TzG9aFJ12edZglg7kp97vGwtKO" , "full_name" : "ZhongHe Zhao" , "email" : "zhaozhonghe40@gmail.com" , "password_changed_at" : "2024-10-24T07:14:46.169687Z" , "create_at" : "2024-10-24T07:14:46.169687Z" }
想让返回结果没有 这个字段
1 "hashed_password" : "$2a$10$RRGhHuYmPf9tRVPDckNI5.q6VJ1TzG9aFJ12edZglg7kp97vGwtKO" ,
添加
1 2 3 4 5 6 7 8 type CreateUserResponse struct { Username string `json:"username"` FullName string `json:"full_name"` Email string `json:"email"` PasswordChangedAt time.Time `json:"password_changed_at"` CreateAt time.Time `json:"create_at"` }
1 2 3 4 5 6 7 8 9 rsp := CreateUserResponse{ Username: user.Username, FullName: user.FullName, Email: user.Email, PasswordChangedAt: user.PasswordChangedAt.Time, CreateAt: user.CreateAt.Time, } ctx.JSON(http.StatusOK, rsp)
二十.user_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 package apiimport ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "reflect" "testing" mockdb "project/simplebank/db/mock" db "project/simplebank/db/sqlc" "project/simplebank/util" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" ) type eqCreateUserParamsMatcher struct { arg db.CreateUserParams password string } func (e eqCreateUserParamsMatcher) Matches(x interface {}) bool { arg, ok := x.(db.CreateUserParams) if !ok { return false } err := util.CheckPassword(e.password, arg.HashedPassword) if err != nil { return false } e.arg.HashedPassword = arg.HashedPassword return reflect.DeepEqual(e.arg, arg) } func (e eqCreateUserParamsMatcher) String() string { return fmt.Sprintf("matches arg %v and password %v" , e.arg, e.password) } func EqCreateUserParams (arg db.CreateUserParams, password string ) gomock.Matcher { return eqCreateUserParamsMatcher{arg, password} } func TestCreateUserAPI (t *testing.T) { user, password := randomUser(t) testCases := []struct { name string body gin.H buildStubs func (store *mockdb.MockStore) checkResponse func (recoder *httptest.ResponseRecorder) }{ { name: "OK" , body: gin.H{ "username" : user.Username, "password" : password, "full_name" : user.FullName, "email" : user.Email, }, buildStubs: func (store *mockdb.MockStore) { arg := db.CreateUserParams{ Username: user.Username, FullName: user.FullName, Email: user.Email, HashedPassword: user.HashedPassword, } store.EXPECT(). CreateUser(gomock.Any(), EqCreateUserParams(arg, password)). Times(1 ). Return(user, nil ) }, checkResponse: func (recorder *httptest.ResponseRecorder) { fmt.Printf("Response code: %d\n" , recorder.Code) require.Equal(t, http.StatusOK, recorder.Code) requireBodyMatchUser(t, recorder.Body, user) }, }, } for i := range testCases { tc := testCases[i] t.Run(tc.name, func (t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() store := mockdb.NewMockStore(ctrl) tc.buildStubs(store) server := newTestServer(t, store) recorder := httptest.NewRecorder() data, err := json.Marshal(tc.body) require.NoError(t, err) fmt.Printf("Request body: %s\n" , string (data)) url := "/users" request, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data)) require.NoError(t, err) server.router.ServeHTTP(recorder, request) tc.checkResponse(recorder) fmt.Printf("Request body: %v\n" , tc.body) }) } } func randomUser (t *testing.T) (user db.User, password string ) { password = util.RandomString(6 ) hashedPassword, err := util.HashedPassword(password) require.NoError(t, err) user = db.User{ Username: util.RandomOwner(), HashedPassword: hashedPassword, FullName: util.RandomOwner(), Email: util.RandomEmail(), } return } func requireBodyMatchUser (t *testing.T, body *bytes.Buffer, user db.User) { data, err := io.ReadAll(body) require.NoError(t, err) var gotUser db.User err = json.Unmarshal(data, &gotUser) require.NoError(t, err) require.Equal(t, user.Username, gotUser.Username) require.Equal(t, user.FullName, gotUser.FullName) require.Equal(t, user.Email, gotUser.Email) require.Empty(t, gotUser.HashedPassword) }
gomock.Any()这个验证的 准确度太低 任何测试基本都能通过
解决方法 使用 新的自定义匹配器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 type eqCreateUserParamsMatcher struct { arg db.CreateUserParams password string } func (e eqCreateUserParamsMatcher) Matches(x interface {}) bool { arg, ok := x.(db.CreateUserParams) if !ok { return false } err := util.CheckPassword(e.password, arg.HashedPassword) if err != nil { return false } e.arg.HashedPassword = arg.HashedPassword return reflect.DeepEqual(e.arg, arg) } func (e eqCreateUserParamsMatcher) String() string { return fmt.Sprintf("matches arg %v and password %v" , e.arg, e.password) } func EqCreateUserParams (arg db.CreateUserParams, password string ) gomock.Matcher { return eqCreateUserParamsMatcher{arg, password} }
1.问题 长记性 json的字段名错误 我测试了一下午
1 2 3 4 5 6 type CreateUserRequest struct { Username string `json:"username" binding:"required,alphanum"` FullName string `json:"full_name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` }
FullName string json:"full_name" binding:"required"
这里的json标签我把full_name 写成了fullname
二十一.JWT 1.JSON Web令牌 密钥算法
服务器一般使用RSA 和 RS256来验证令牌
对称算法
非对称算法
必须在服务器代码中 检查令牌的算法标头
JWT令牌的很多问题:
RASETO作为替代JWT的安全方案
2.基于令牌的身份验证的工作原理是什么? 基于令牌的身份验证从用户登录至系统、设备或应用程序开始,通常使用密码或安全问题。授权服务器验证初始身份验证,然后发放访问令牌,访问令牌是一小段数据,允许客户端应用程序向 API 服务器发出安全调用或信号。
基于令牌的身份验证的工作原理是为服务器提供第二种高度可靠的方式来验证用户的身份和请求的真实性。
完成该基于令牌的初始身份验证协议后,令牌就像盖了章的票据一样:用户可以在令牌生命周期内连续无缝访问相关资源,而无需重新进行身份验证。 该生命周期在用户注销或退出应用程序时结束,也可由设定的超时协议触发。
3.基于令牌的身份验证有何益处? 基于令牌的身份验证能为多个利益相关者提供许多便利:
即时的用户体验 :用户无需在每次返回系统、应用程序或网页时重新输入凭据并重新进行身份验证,只要令牌仍然有效(通常会持续到会话因注销或退出而结束),用户就可以保持即时访问。
增加了数字安全性 :基于令牌的身份验证在传统的基于密码或基于服务器的身份验证之上又增加了一道安全保护。通常,令牌比密码更难被窃取、被黑客入侵或以其他方式泄露。
管理员控制 :基于令牌的身份验证为管理员提供了对每个用户操作和事项的更精细的控制和可见性。
减轻技术负担 :由于令牌生成可以与令牌验证完全分离,因此验证可以由辅助服务(如 Entrust 身份和访问管理解决方案提供的服务)来处理。这将显著减少内部服务器和设备上的负载。
二十二.编写令牌 make.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package tokenimport ( "time" ) type Maker interface { CreateToken(username string , role string , duration time.Duration) (string , *Payload, error ) VerifyToken(token string ) (*Payload, error ) }
payload.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package tokenimport ( "errors" "time" "github.com/google/uuid" ) var ( ErrInvalidToken = errors.New("token is invalid" ) ErrExpiredToken = errors.New("token has expired" ) ) type Payload struct { ID uuid.UUID `json:"id"` Username string `json:"username"` Role string `json:"role"` IssuedAt time.Time `json:"issued_at"` ExpiredAt time.Time `json:"expired_at"` } func NewPayload (username string , role string , duration time.Duration) (*Payload, error ) { tokenID, err := uuid.NewRandom() if err != nil { return nil , err } payload := &Payload{ ID: tokenID, Username: username, Role: role, IssuedAt: time.Now(), ExpiredAt: time.Now().Add(duration), } return payload, nil } func (payload *Payload) Valid() error { if time.Now().After(payload.ExpiredAt) { return ErrExpiredToken } return nil }
jwt_maker.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package tokenimport ( "errors" "fmt" "time" "github.com/dgrijalva/jwt-go" ) const minSecretKeySize = 32 type JWTMaker struct { secretKey string } func NewJWTMaker (secretKey string ) (Maker, error ) { if len (secretKey) < minSecretKeySize { return nil , fmt.Errorf("invalid key size: must be at least %d characters" , minSecretKeySize) } return &JWTMaker{secretKey}, nil } func (maker *JWTMaker) CreateToken(username string , role string , duration time.Duration) (string , *Payload, error ) { payload, err := NewPayload(username, role, duration) if err != nil { return "" , payload, err } jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, payload) token, err := jwtToken.SignedString([]byte (maker.secretKey)) return token, payload, err } func (maker *JWTMaker) VerifyToken(token string ) (*Payload, error ) { keyFunc := func (token *jwt.Token) (interface {}, error ) { _, ok := token.Method.(*jwt.SigningMethodHMAC) if !ok { return nil , ErrInvalidToken } return []byte (maker.secretKey), nil } jwtToken, err := jwt.ParseWithClaims(token, &Payload{}, keyFunc) if err != nil { verr, ok := err.(*jwt.ValidationError) if ok && errors.Is(verr.Inner, ErrExpiredToken) { return nil , ErrExpiredToken } return nil , ErrInvalidToken } payload, ok := jwtToken.Claims.(*Payload) if !ok { return nil , ErrInvalidToken } return payload, nil }
jwt_test.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 package tokenimport ( "testing" "time" "project/simplebank/util" "github.com/dgrijalva/jwt-go" "github.com/stretchr/testify/require" ) func TestJWTMaker (t *testing.T) { maker, err := NewJWTMaker(util.RandomString(32 )) require.NoError(t, err) username := util.RandomOwner() role := util.DepositorRole duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) token, err := maker.CreateToken(username, duration) require.NoError(t, err) require.NotEmpty(t, token) payload, err := maker.VerifyToken(token) require.NoError(t, err) require.NotEmpty(t, payload) require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) require.Equal(t, role, payload.Role) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } func TestExpiredJWTToken (t *testing.T) { maker, err := NewJWTMaker(util.RandomString(32 )) require.NoError(t, err) token, err := maker.CreateToken(util.RandomOwner(), -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) payload, err := maker.VerifyToken(token) require.Error(t, err) require.EqualError(t, err, ErrExpiredToken.Error()) require.Nil(t, payload) } func TestInvalidJWTTokenAlgNone (t *testing.T) { payload, err := NewPayload(util.RandomOwner(), time.Minute) require.NoError(t, err) jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload) token, err := jwtToken.SignedString(jwt.UnsafeAllowNoneSignatureType) require.NoError(t, err) maker, err := NewJWTMaker(util.RandomString(32 )) require.NoError(t, err) payload, err = maker.VerifyToken(token) require.Error(t, err) require.EqualError(t, err, ErrInvalidToken.Error()) require.Nil(t, payload) }
作者说 passeto是比JWT更简洁更好用
passeto_maker.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 package tokenimport ( "fmt" "time" "github.com/aead/chacha20poly1305" "github.com/o1egl/paseto" ) type PasetoMaker struct { paseto *paseto.V2 symmetricKey []byte } func NewPasetoMaker (symmetricKey string ) (Maker, error ) { if len (symmetricKey) != chacha20poly1305.KeySize { return nil , fmt.Errorf("invalid key size: must be exactly %d characters" , chacha20poly1305.KeySize) } maker := &PasetoMaker{ paseto: paseto.NewV2(), symmetricKey: []byte (symmetricKey), } return maker, nil } func (maker *PasetoMaker) CreateToken(username string , duration time.Duration) (string , error ) { payload, err := NewPayload(username, duration) if err != nil { return "" , err } return maker.paseto.Encrypt(maker.symmetricKey, payload, nil ) } func (maker *PasetoMaker) VerifyToken(token string ) (*Payload, error ) { payload := &Payload{} err := maker.paseto.Decrypt(token, maker.symmetricKey, payload, nil ) if err != nil { return nil , ErrInvalidToken } err = payload.Valid() if err != nil { return nil , err } return payload, nil }
paseto_make_test.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package tokenimport ( "testing" "time" "project/simplebank/util" "github.com/stretchr/testify/require" ) func TestPasetoMaker (t *testing.T) { maker, err := NewJWTMaker(util.RandomString(32 )) require.NoError(t, err) username := util.RandomOwner() duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) token, err := maker.CreateToken(username, duration) require.NoError(t, err) require.NotEmpty(t, token) payload, err := maker.VerifyToken(token) require.NoError(t, err) require.NotEmpty(t, payload) require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } func TestExpiredPasetoToken (t *testing.T) { maker, err := NewPasetoMaker(util.RandomString(32 )) require.NoError(t, err) token, err := maker.CreateToken(util.RandomOwner(), -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) payload, err := maker.VerifyToken(token) require.Error(t, err) require.EqualError(t, err, ErrExpiredToken.Error()) require.Nil(t, payload) }
10.28学习如何用令牌登录api 1.server.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package apiimport ( "fmt" db "project/simplebank/db/sqlc" "project/simplebank/token" "project/simplebank/util" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" "github.com/go-playground/validator/v10" ) type Server struct { config util.Config store db.Store router *gin.Engine tokenMaker token.Maker } func validCurrency (fl validator.FieldLevel) bool { currency := fl.Field().String() return currency == "RMB" } func (server *Server) setupValidator() { if v, ok := binding.Validator.Engine().(*validator.Validate); ok { v.RegisterValidation("currency" , validCurrency) } } func NewServer (config util.Config, store db.Store) (*Server, error ) { tokenMaker, err := token.NewPasetoMaker(config.TokenSymmetricKey) if err != nil { fmt.Printf("Key length in bytes: %d\n" , len ([]byte (config.TokenSymmetricKey))) return nil , fmt.Errorf("cannot create token maker: %w" , err) } server := &Server{ config: config, store: store, tokenMaker: tokenMaker, } server.setupValidator() server.setupRouter() return server, nil } func (server *Server) setupRouter() { router := gin.Default() router.POST("/users/login" , server.loginUser) router.POST("transfers" , server.createTransfer) router.POST("/accounts" , server.createAccount) router.GET("/accounts/:id" , server.getAccount) router.POST("/users" , server.createUser) router.GET("/accounts" , server.listAccounts) server.router = router } func errorResponse (err error ) gin.H { return gin.H{"error" : err.Error()} } func (server *Server) Start(address string ) error { return server.router.Run(address) }
2.user.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 package apiimport ( "errors" "fmt" "net/http" "time" db "project/simplebank/db/sqlc" util "project/simplebank/util" "github.com/gin-gonic/gin" ) type CreateUserRequest struct { Username string `json:"username" binding:"required,alphanum"` FullName string `json:"full_name" binding:"required"` Email string `json:"email" binding:"required,email"` Password string `json:"password" binding:"required,min=6"` } type UserResponse struct { Username string `json:"username"` FullName string `json:"full_name" binding:"required"` Email string `json:"email"` PasswordChangedAt time.Time `json:"password_changed_at"` CreateAt time.Time `json:"create_at"` } func newUserResponse (user db.User) UserResponse { return UserResponse{ Username: user.Username, FullName: user.FullName, Email: user.Email, PasswordChangedAt: user.PasswordChangedAt.Time, CreateAt: user.CreateAt.Time, } } func (server *Server) createUser(ctx *gin.Context) { var req CreateUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return } fmt.Printf("Received request: %+v\n" , req) hashedPassword, err := util.HashedPassword(req.Password) if err != nil { ctx.JSON(http.StatusInternalServerError, errorResponse(fmt.Errorf("failed to hash password: %v" , err))) return } arg := db.CreateUserParams{ Username: req.Username, FullName: req.FullName, Email: req.Email, HashedPassword: hashedPassword, } user, err := server.store.CreateUser(ctx, arg) if err != nil { fmt.Printf("Error creating user: %v\n" , err) errCode := db.ErrorCode(err) if errCode == db.UniqueViolation { return } ctx.JSON(http.StatusForbidden, errorResponse(err)) return } rsp := newUserResponse(user) ctx.JSON(http.StatusOK, rsp) } type loginUserRequest struct { Username string `json:"username" binding:"required,alphanum"` Password string `json:"password" binding:"required,min=6"` } type loginUserResponse struct { AccessToken string `json:"access_token"` User UserResponse `json:"user"` } func (server *Server) loginUser(ctx *gin.Context) { var req loginUserRequest if err := ctx.ShouldBindJSON(&req); err != nil { ctx.JSON(http.StatusBadRequest, errorResponse(err)) return } user, err := server.store.GetUser(ctx, req.Username) if err != nil { if errors.Is(err, db.ErrRecordNotFound) { ctx.JSON(http.StatusNotFound, errorResponse(err)) return } ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } err = util.CheckPassword(req.Password, user.HashedPassword) if err != nil { ctx.JSON(http.StatusUnauthorized, errorResponse(err)) return } accessToken, err := server.tokenMaker.CreateToken( user.Username, server.config.AccessTokenDuration, ) if err != nil { ctx.JSON(http.StatusInternalServerError, errorResponse(err)) return } rsp := loginUserResponse{ AccessToken: accessToken, User: newUserResponse(user), } ctx.JSON(http.StatusOK, rsp) }
3.问题1 为什么运行transfer_text.go出现了很多错误:
4.解决1 在学习的时候图方便把作者的代码全部拉了下来 在transfer_test.go中 有很多情况 在transfer中并没有实现 导致无法对应这些情况
正常时作者留给你的任务 让你去课后实现这些功能
重新回顾第13集:
模拟数据库进行测试:
确保模拟数据库实现与真是数据库相同的接口
出问题的两段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 { name: "UnauthorizedUser" , body: gin.H{ "from_account_id" : account1.ID, "to_account_id" : account2.ID, "amount" : amount, "currency" : util.RandomCurrency(), }, buildStubs: func (store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1 ).Return(account1, nil ) store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account2.ID)).Times(0 ) store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0 ) }, checkResponse: func (recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) }, }, { name: "NoAuthorization" , body: gin.H{ "from_account_id" : account1.ID, "to_account_id" : account2.ID, "amount" : amount, "currency" : util.USD, }, buildStubs: func (store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0 ) store.EXPECT().TransferTx(gomock.Any(), gomock.Any()).Times(0 ) }, checkResponse: func (recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) }, },
5.问题2 为什么得到分组用户出错
6.解决2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (q *Queries) ListAccounts(ctx context.Context, arg ListAccountsParams) ([]Account, error ) { rows, err := q.db.Query(ctx, listAccounts, arg.Owner, arg.Limit, arg.Offset) if err != nil { return nil , err } defer rows.Close() items := []Account{} for rows.Next() { var i Account if err := rows.Scan( &i.ID, &i.Owner, &i.Balance, &i.Currency, &i.CreatedAt, ); err != nil { return nil , err } items = append (items, i) } if err := rows.Err(); err != nil { return nil , err } return items, nil }
rows, err := q.db.Query(ctx, listAccounts, arg.Owner, arg.Limit, arg.Offset) 这里查询的条件有arg.owner 但是我们在测试的时候并没有设置owner 可以显示尝试把owner去掉
11.6日 二十二.身份验证中间件 授权API请求 使用make sqlc 和 make mock 重新为listAccount增加 Owner字段
搞了半天 app.env配置错了 应该是
ACCESS_TOKEN_DURATION=15m
我写成别的了
二十三.部署目前的程序 1.对程序进行docker化 运用git部署
注意 :永远不要将更改直接推送到主分支
1.创建新分支-》推送分支-》产生以下结果-》复制url-》创建标题-》创建拉取请求-》从而可以看到 Files changed 文件的更改
1 2 3 4 5 6 7 remote: Resolving deltas: 100% (2/2), completed with 2 local objects. remote: remote: Create a pull request for 'ft/docker' on GitHub by visiting: remote: https://github.com/Whuichenggong/projects/pull/new/ft/docker remote: To github.com:Whuichenggong/projects.git * [new branch] ft/docker -> ft/docker
重新回看第10集 配置工作流 最近这两天了解到了工作流有了更深的理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go name: Go on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: go-version: '1.20' - name: Build run: go build -v ./... - name: Test run: go test -v ./...
go语言的工作流模板
giuthub action 相当于将一些列配置放到了github上的一个服务器上 也就是相当于将东西放进了github的服务器
11.10日 还是github action问题 终于把 Install golang-migrate解决了
因为: 在最开始推送项目到github的时候 就是因为把项目结构推送错了 ,导致推送到github上的项目根目录没有go.mod文件这造成了很大的错误 导致一直失败
今天又解决了 install golang-migrate问题 因为sudo mv migrate /usr/bin/migrate 把之前的 名称换成 migrate 就好用了
问题2:
make migratedown migrate -path /db/migration -database “postgresql://root:secret@localhost:5432/simple_bank?sslmode=disable” -verbose down 2024/11/10 13:30:17 error: open /db/migration.: The system cannot find the path specified. make: *** [migratedown] 错误 1
在Makefile中的指令的 路径又弄错了 必须让指令能找到位置所在
卧槽:成功了 绿了 妈的
牛逼
Dockerfile 官方镜像
Dockerfile
1 2 3 4 5 6 7 8 9 FROM golang:1.16 -alpine3.13 WORKDIR /app COPY . . RUN go build -o main main.go EXPOSE 8080 CMD [ "/app/main" ]
docker build -t simplebank:latest
使用这个指令构建镜像
images的大小很大
1 2 3 docker images REPOSITORY TAG IMAGE ID CREATED SIZE simplebank latest 48621dad3f4d 5 minutes ago 656MB
分阶段构建可以减少体积
也就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 FROM golang:1.23 -alpine3.20 AS buildWORKDIR /app COPY . . RUN go build -o main main.go FROM alpine:3.20 WORKDIR /app COPY --from=build /app/main . EXPOSE 8080 CMD [ "/app/main" ]
最终体积
1 2 3 docker images REPOSITORY TAG IMAGE ID CREATED SIZE simplebank latest f64691fae70e 7 seconds ago 27.1MB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 docker ps -a列出容器状态 docker rmi f64691fae70e Untagged: simplebank:latest Deleted: sha256:f64691fae70e516b799ed846bbeef10045388dae1932ecafc8b93fb208b403f0 //运行这条指令便启动了容器 监听8080端口 docker run --name simplebank -p 8080:8080 simplebank:latest [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] POST /users --> project/simplebank/api.(*Server).createUser-fm (3 handlers) [GIN-debug] POST /users/login --> project/simplebank/api.(*Server).loginUser-fm (3 handlers) [GIN-debug] GET /accounts/:id --> project/simplebank/api.(*Server).getAccount-fm (4 handlers) [GIN-debug] POST /accounts --> project/simplebank/api.(*Server).createAccount-fm (4 handlers) [GIN-debug] GET /accounts --> project/simplebank/api.(*Server).listAccounts-fm (4 handlers) [GIN-debug] POST /transfers --> project/simplebank/api.(*Server).createTransfer-fm (4 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Listening and serving HTTP on 127.0.0.1:1124 //重新启动镜像 PS E:\projects\simplebank> docker rm simplebank simplebank PS E:\projects\simplebank> docker run --name simplebank -p 8080:8080 -e GIN_MODE=release simplebank:latest 这样启动就不会有上面的输出了
docker container inspect postgres12
检查网络设置
11.13日 问题: 解决用docker启动后 无法用postman测试接口的问题
1 docker run --name simplebank -p 8083:8083 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@172.17.0.2:5432/simplebank?sslmode=disable" simplebank:latest
每次修改完dockerfiles或者什么 要记住重新构建镜像
1 docker build --no-cache -t simplebank:latest .
关键: 先使用调试功能 查看是否正确监听端口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 docker run --name simplebank -p 8080:8080 simplebank:latest [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] POST /users --> project/simplebank/api.(*Server).createUser-fm (3 handlers) [GIN-debug] POST /users/login --> project/simplebank/api.(*Server).loginUser-fm (3 handlers) [GIN-debug] GET /accounts/:id --> project/simplebank/api.(*Server).getAccount-fm (4 handlers) [GIN-debug] POST /accounts --> project/simplebank/api.(*Server).createAccount-fm (4 handlers) [GIN-debug] GET /accounts --> project/simplebank/api.(*Server).listAccounts-fm (4 handlers) [GIN-debug] POST /transfers --> project/simplebank/api.(*Server).createTransfer-fm (4 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Listening and serving HTTP on 0.0.0.0:8080
这次在测试的时候 有了反应
1 2 PS E:\projects\simplebank> docker run --name simplebank -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@172.17.0.2:5432/simplebank?sslmode=disable" simplebank:latest [GIN] 2024/11/13 - 01:17:23 | 401 | 39.567µs | 172.17.0.1 | GET "/accounts/1"
此更改导致了postgres连接出错
#####不使用ip地址使用用户定义的网络 连接到postrges
docker network ls
1 2 3 4 5 6 7 NETWORK ID NAME DRIVER SCOPE ca0046b2c82c bank-network bridge local cf35f34026f7 bridge bridge local 1500c05159ef host host local 074a556122c6 none null local fafb76e1721e start_gvb-network bridge local
桥接网络
查看更详细的网络信息 docker network inspect bridge
删除网络:
docker network rm 0fd871187ef1
创建自己的网络 ``docker network create bank_network`
1 `0fd871187ef1e3b3bee37ac898e895cf54615e267bd6af9d7b2c045fc5178a14
连接创建的网络 docker network connect bank-network
将postrges12 连接到我们创建的网络
docker network connect bank-network postgres12
docker network inspect bank-network
得先启动 postrges12 验证 postgres12
容器是否正在运行 : 检查 named 的容器是否正在运行:postgres12
查找具有名称的容器并检查其状态。如果容器未运行,请启动容器:postgres12
再次将 postgres12
连接到网络 : 现在,尝试将容器连接到 :postgres12``bank-network
1 docker network connect bank-network postgres12
现在已经成功添加了postrges12
1 2 3 4 5 6 7 8 "ConfigOnly": false, "Containers": { "7ba14f6dd2f7a81db9264c0814e9686e921b0d86c01b2df325dad4a1cca35c40": { "Name": "postgres12", "EndpointID": "b3dc1614431f2f11f2b0d6c8bb7f33b529baacefa39521bf522c84a7f526a882", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": ""
此时查看
docker container inspect postgres12
这个容器将会有两段网络
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 "NetworkSettings": { "Bridge": "", "SandboxID": "2ea1e674576863a5e20fe6dda2a3ea265dd11b0223dc4a94bbfa23c57adc66d9", "SandboxKey": "/var/run/docker/netns/2ea1e6745768", "Ports": { "5432/tcp": [ { "HostIp": "0.0.0.0", "HostPort": "5432" } ] }, "HairpinMode": false, "LinkLocalIPv6Address": "", "LinkLocalIPv6PrefixLen": 0, "SecondaryIPAddresses": null, "SecondaryIPv6Addresses": null, "EndpointID": "d85289ea4f7ca088375523781a14955e1b1fc58e5af731fe7f4c48fecba470e6", "Gateway": "172.17.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "MacAddress": "02:42:ac:11:00:02", "Networks": { "bank-network": { "IPAMConfig": {}, "Links": null, "Aliases": [ "7ba14f6dd2f7" ], "MacAddress": "02:42:ac:12:00:02", "NetworkID": "ca0046b2c82ccb1fe4c996950a815d9c374c58514921c9b919899d8169cb9881", "EndpointID": "b3dc1614431f2f11f2b0d6c8bb7f33b529baacefa39521bf522c84a7f526a882", "Gateway": "172.18.0.1", "IPAddress": "172.18.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": {}, "DNSNames": [ "postgres12", "7ba14f6dd2f7" ] }, "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "MacAddress": "02:42:ac:11:00:02", "NetworkID": "cf35f34026f787fe91864d7e7a2ab23d482b6a6b956a10d596ae0d9818aa7e16", "EndpointID": "d85289ea4f7ca088375523781a14955e1b1fc58e5af731fe7f4c48fecba470e6", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": null, "DNSNames": null } } }
重新使用指令
docker run --name simplebank --network bank-network -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@172.17.0.2:5432/simplebank?sslmode=disable" simplebank:latest
此时 simplebank容器将与postgres12运行在同一个网络上
将172.17.0.2替换成postgres12 因为可以通过名称访问网络
启动容器指令:
1 2 docker run --name simplebank --network bank-network -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@postgres12:5432/simplebank?sslmode=disable" simplebank:latest [GIN] 2024/11/13 - 02:06:27 | 400 | 105.754µs | 172.18.0.1 | POST "/users/login"
docker network inspect bank-network
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 [ { "Name": "bank-network", "Id": "ca0046b2c82ccb1fe4c996950a815d9c374c58514921c9b919899d8169cb9881", "Created": "2024-05-10T13:32:42.557489581Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": {}, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "7ba14f6dd2f7a81db9264c0814e9686e921b0d86c01b2df325dad4a1cca35c40": { "Name": "postgres12", "EndpointID": "b3dc1614431f2f11f2b0d6c8bb7f33b529baacefa39521bf522c84a7f526a882", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" }, "a76e19ef1c210d1cc4f458ed9b2238db810872417e0a1072e8467dda82663a2a": { "Name": "simplebank", "EndpointID": "487f3ec81ada3bf84e44af700d0ae930075ce8c683755d789c27cadc7f95ed06", "MacAddress": "02:42:ac:12:00:03", "IPv4Address": "172.18.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": {} }
目前有两个容器在自定义的网络中运行
之后的postrges就可以正常使用了
更改Makefile文件
1 2 postgres: docker run --name postgres12 --network bank-network -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=secret -d postgres:12-alpine
在github中 pullrequest中可以查看更改 并且 merge 分支到主分支-》然后确认合并—》Delete branch
二十四.docker-compose https://docs.docker.com
创建docker-compose.yaml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 version: "3.9" services: postgres: image: postgres:12-alpine environment: - POSTGRES_USER=root - POSTGRES_PASSWORD=secret - POSTGRES_DB=simple_bank ports: - "5432:5432" api: build: context: . dockerfile: Dockerfile ports: - "8080:8080" environment: - DB_SOURCE=postgresql://root:secret@postgres:5432/simple_bank?sslmode=disable
docker compose up
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 docker compose up [+] Running 1/0 ✔ Container simplebank-api-1 Created 0.0s Attaching to api-1, postgres-1 api-1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. api-1 | api-1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. api-1 | - using env: export GIN_MODE=release api-1 | - using code: gin.SetMode(gin.ReleaseMode) api-1 | api-1 | [GIN-debug] POST /users --> project/simplebank/api.(*Server).createUser-fm (3 handlers) api-1 | [GIN-debug] POST /users/login --> project/simplebank/api.(*Server).loginUser-fm (3 handlers) api-1 | [GIN-debug] GET /accounts/:id --> project/simplebank/api.(*Server).getAccount-fm (4 handlers) api-1 | [GIN-debug] POST /accounts --> project/simplebank/api.(*Server).createAccount-fm (4 handlers) api-1 | [GIN-debug] GET /accounts --> project/simplebank/api.(*Server).listAccounts-fm (4 handlers) api-1 | [GIN-debug] POST /transfers --> project/simplebank/api.(*Server).createTransfer-fm (4 handlers) api-1 | [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. api-1 | Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. api-1 | [GIN-debug] Listening and serving HTTP on 0.0.0.0:8080 postgres-1 | The files belonging to this database system will be owned by user "postgres". postgres-1 | This user must also own the server process. postgres-1 | postgres-1 | The database cluster will be initialized with locale "en_US.utf8". postgres-1 | The default database encoding has accordingly been set to "UTF8". postgres-1 | The default text search configuration will be set to "english". postgres-1 | postgres-1 | Data page checksums are disabled. postgres-1 | postgres-1 | fixing permissions on existing directory /var/lib/postgresql/data ... ok postgres-1 | creating subdirectories ... ok postgres-1 | selecting dynamic shared memory implementation ... posix postgres-1 | selecting default max_connections ... 100 postgres-1 | selecting default shared_buffers ... 128MB postgres-1 | selecting default time zone ... UTC postgres-1 | creating configuration files ... ok postgres-1 | running bootstrap script ... ok postgres-1 | sh: locale: not found postgres-1 | 2024-11-13 06:50:42.795 UTC [30] WARNING: no usable system locales were found postgres-1 | performing post-bootstrap initialization ... ok postgres-1 | syncing data to disk ... ok postgres-1 | postgres-1 | postgres-1 | Success. You can now start the database server using: postgres-1 | postgres-1 | pg_ctl -D /var/lib/postgresql/data -l logfile start postgres-1 | postgres-1 | initdb: warning: enabling "trust" authentication for local connections postgres-1 | You can change this by editing pg_hba.conf or using the option -A, or postgres-1 | --auth-local and --auth-host, the next time you run initdb. postgres-1 | waiting for server to start....2024-11-13 06:50:43.144 UTC [36] LOG: starting PostgreSQL 12.18 on x86_64-pc-linux-musl, compiled by gcc (Alpine 13.2.1_git20231014) 13.2.1 20231014, 64-bit postgres-1 | 2024-11-13 06:50:43.146 UTC [36] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2024-11-13 06:50:43.160 UTC [37] LOG: database system was shut down at 2024-11-13 06:50:43 UTC postgres-1 | 2024-11-13 06:50:43.164 UTC [36] LOG: database system is ready to accept connections postgres-1 | done postgres-1 | server started postgres-1 | CREATE DATABASE postgres-1 | postgres-1 | postgres-1 | /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/* postgres-1 | postgres-1 | waiting for server to shut down....2024-11-13 06:50:43.315 UTC [36] LOG: received fast shutdown request postgres-1 | 2024-11-13 06:50:43.316 UTC [36] LOG: aborting any active transactions postgres-1 | 2024-11-13 06:50:43.318 UTC [36] LOG: background worker "logical replication launcher" (PID 43) exited with exit code 1 postgres-1 | 2024-11-13 06:50:43.318 UTC [38] LOG: shutting down postgres-1 | 2024-11-13 06:50:43.330 UTC [36] LOG: database system is shut down postgres-1 | done postgres-1 | server stopped postgres-1 | postgres-1 | PostgreSQL init process complete; ready for start up. postgres-1 | postgres-1 | 2024-11-13 06:50:43.447 UTC [1] LOG: starting PostgreSQL 12.18 on x86_64-pc-linux-musl, compiled by gcc (Alpine 13.2.1_git20231014) 13.2.1 20231014, 64-bit postgres-1 | 2024-11-13 06:50:43.447 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 postgres-1 | 2024-11-13 06:50:43.447 UTC [1] LOG: listening on IPv6 address "::", port 5432 postgres-1 | 2024-11-13 06:50:43.450 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" postgres-1 | 2024-11-13 06:50:43.461 UTC [51] LOG: database system was shut down at 2024-11-13 06:50:43 UTC postgres-1 | 2024-11-13 06:50:43.465 UTC [1] LOG: database system is ready to accept connections
构建镜像完成后
1 2 3 4 docker images REPOSITORY TAG IMAGE ID CREATED SIZE simplebank-api latest eb772c9e932f 6 hours ago 27.1MB simplebank latest 9f145f0ce89f 6 hours ago 27.1MB
查看占用端口的进程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 `netstat -ano | findstr :5432` TCP 0.0.0.0:5432 0.0.0.0:0 LISTENING 30352 TCP [::]:5432 [::]:0 LISTENING 30352 TCP [::1]:5432 [::]:0 LISTENING 35464 PS E:\projects\simplebank> `tasklist /FI "PID eq 30352"`` 映像名称 PID 会话名 会话# 内存使用 ========================= ======== ================ =========== ============ com.docker.backend.exe 30352 Console 2 117,104 K PS E:\projects\simplebank> `tasklist /FI "PID eq 35464"`` 映像名称 PID 会话名 会话# 内存使用 ========================= ======== ================ =========== ============ wslrelay.exe 35464 Console 2 8,328 K
在 Windows 上(终止进程):
1 2 taskkill /PID 30352 /F taskkill /PID 35464 /F
docker ps
1 2 3 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES dab18d564f9c postgres:12-alpine "docker-entrypoint.s…" About an hour ago Up 7 minutes 0.0.0.0:5432->5432/tcp simplebank-postgres-1 c4c37a8a870a simplebank-api "/app/main" About an hour ago Up 7 minutes 0.0.0.0:8080->8080/tcp simplebank-api-1
docker network inspect simplebank_default
两个服务容器实际在同一个网络上运行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 docker network inspect simplebank_default [ { "Name": "simplebank_default", "Id": "fab69439b1a55525d81fa70d9e789c3b6d51ba8d7899924deb8413fb724ca951", "Created": "2024-11-13T05:28:49.358856507Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.20.0.0/16", "Gateway": "172.20.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "c4c37a8a870a75e9fa626c7034dd935c8f3afdb86c5e2c37b012503bff9c7ab7": { "Name": "simplebank-api-1", "EndpointID": "8a247db55db70983d6b2d619caef09bf2593964daa02be5773448fbd74f9d791", "MacAddress": "02:42:ac:14:00:02", "IPv4Address": "172.20.0.2/16", "IPv6Address": "" }, "dab18d564f9c4554ef255e50205be2f4dd9c1fada3391dde698d7717d0e642ff": { "Name": "simplebank-postgres-1", "EndpointID": "2b9220ab1bcc031b29631a2ecb462a48a475a722b10592c478124c03d95e29df", "MacAddress": "02:42:ac:14:00:03", "IPv4Address": "172.20.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": { "com.docker.compose.network": "default", "com.docker.compose.project": "simplebank", "com.docker.compose.version": "2.24.6" } } ]
docker compose down
删除现在所有网络
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # Build stage 构建二进制文件 FROM golang:1.23-alpine3.20 AS build WORKDIR /app COPY . . RUN go build -o main main.go # Run stage FROM alpine:3.20 WORKDIR /app COPY --from=build /app/main . COPY app.env . # 这一步可以解决2024/11/13 08:08:06 cannot load config:Config File "app" Not Found in "[/app]" EXPOSE 8080 CMD [ "/app/main" ]
操 最后一刻验证成功了
用终端输入指令 带入参数 172.17.0.2 这样 viper可以自动读取配置
1 2 3 4 docker run --name simplebank -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@172.17.0.2:5432/simple_bank?sslmode=disable" simplebank:latest Received request: {Username:Zhonghe FullName:zhaohzonghe Email:3041322213@qq.com Password:zzh123456} [GIN] 2024/11/13 - 12:54:07 | 200 | 57.106456ms | 172.17.0.1 | POST "/users"
app.env中的配置
1 2 3 4 5 DATABASE_URL=postgres://root:secret@localhost:5432/simple_bank?sslmode=disable MIGRATION_URL=project/simplebank/db/migration HTTPServerAddress=0.0.0.0:8080 TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012 ACCESS_TOKEN_DURATION=15m
11.18日 docker ps
docker network inspect simplebank_default
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 81aa7c463a58 postgres:12-alpine "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp simplebank-postgres-1 047f0bb9fbc8 simplebank-api "/app/main" About a minute ago Up About a minute 0.0.0.0:8080->8080/tcp simplebank-api-1 PS E:\projects\simplebank> [ { "Name": "simplebank_default", "Id": "9afc6c5d5e9252f2161f204008596b067fceecd49ac5a9171910c58f4717e205", "Created": "2024-11-18T11:05:59.526768414Z", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.18.0.0/16", "Gateway": "172.18.0.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": { "047f0bb9fbc8fdbe07cc311b134c00ff27cb0a2cbcb4322746a6b30cbbb404bf": { "Name": "simplebank-api-1", "EndpointID": "b607176500386abe6ac7ad27f31d9c453a3f2087dacade426d50a72b1e30b585", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" }, "81aa7c463a58ad777dd3d99f9ba3c442c024c02d0f91be924903ffa423f99426": { "Name": "simplebank-postgres-1", "EndpointID": "ce4ec4fda5631c27ba1e8c96503ef86f1bfea3bc8f563ecf7528c75ca91f1bb6", "MacAddress": "02:42:ac:12:00:03", "IPv4Address": "172.18.0.3/16", "IPv6Address": "" } }, "Options": {}, "Labels": { "com.docker.compose.network": "default", "com.docker.compose.project": "simplebank", "com.docker.compose.version": "2.24.6" } } ]
两个服务器运行在同一个网络 通过名字彼此发现自己
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 "Containers": { "047f0bb9fbc8fdbe07cc311b134c00ff27cb0a2cbcb4322746a6b30cbbb404bf": { "Name": "simplebank-api-1", "EndpointID": "b607176500386abe6ac7ad27f31d9c453a3f2087dacade426d50a72b1e30b585", "MacAddress": "02:42:ac:12:00:02", "IPv4Address": "172.18.0.2/16", "IPv6Address": "" }, "81aa7c463a58ad777dd3d99f9ba3c442c024c02d0f91be924903ffa423f99426": { "Name": "simplebank-postgres-1", "EndpointID": "ce4ec4fda5631c27ba1e8c96503ef86f1bfea3bc8f563ecf7528c75ca91f1bb6", "MacAddress": "02:42:ac:12:00:03", "IPv4Address": "172.18.0.3/16", "IPv6Address": "" }
目前是链接不上数据库的因为没有执行数据库迁移
重新构建docker-compose docker files文件
docker compose down
[+] Running 3/3 ✔ Container simplebank-postgres-1 Removed 0.7s ✔ Container simplebank-api-1 Removed 0.6s ✔ Network simplebank_default Removed
删除目前所有容器和网络
使用docker rmi ….
删除simplebank_api镜像
出错 ERROR [api internal] load metadata for docker.io/library/builder:latest
错误的核心在于 tar
解压的文件名与你 mv
命令中期望的文件名不匹配。具体表现为 tar
解压生成的文件名并不是 migrate.linux-amd64
,而是 migrate
。
解决方法 1. 修改 RUN
命令中的文件名引用
根据错误日志,tar
解压后生成的文件名是 migrate
,而非 migrate.linux-amd64
。因此,mv
命令应改为直接操作 migrate
:
1 2 dockerfile复制代码RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz \ | tar -xz && mv migrate /app/migrate
这将确保正确地将解压出的 migrate
文件移动到 /app/migrate
。
2. 验证文件解压和路径
为了确保过程正确,可以在 RUN
指令中加入调试信息以打印文件列表:
1 2 dockerfile复制代码RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz \ | tar -xz && ls -l && mv migrate /app/migrate
原因:要保持 builer同意 我写成了一个build 另一个builder 因该换成build
1 2 COPY --from=build /app/main . COPY --from=build /app/migrate /usr/bin/migrate
欧克解决了
完整的 dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 FROM golang:1.23 -alpine3.20 AS buildWORKDIR /app COPY . . RUN go build -o main main.go RUN apk add curl RUN curl -L https://github.com/golang-migrate/migrate/releases/download/v4.17.0/migrate.linux-amd64.tar.gz | tar xvz && mv migrate /app/migrate FROM alpine:3.20 WORKDIR /app COPY --from=build /app/main . COPY --from=build /app/migrate /usr/bin/migrate COPY app.env . COPY start.sh . COPY db/migration ./migration EXPOSE 8080 CMD [ "/app/main" ] ENTRYPOINT [ "/app/start.sh" ]
完整的 docker-compose.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 version: "3.9" services: postgres: image: postgres:12 -alpine environment: - POSTGRES_USER=root - POSTGRES_PASSWORD=secret - POSTGRES_DB=simple_bank ports: - "5432:5432" api: build: context: . dockerfile: Dockerfile ports: - "8080:8080" environment: - DB_SOURCE=postgresql://root:secret@postgres:5432 /simple_bank?sslmode=disable depends_on: - postgres
下载wait-for工具
mv "C:\Users\30413\Downloads\wait-for" ./wait-for.sh
目前测试api问题:
1 2 3 { "error": "failed to connect to `user=root database=simple_bank`:\n\t127.0.0.1:5432 (localhost): dial error: dial tcp 127.0.0.1:5432: connect: connection refused\n\t[::1]:5432 (localhost): dial error: dial tcp [::1]:5432: connect: cannot assign requested address" }
二.11.22日 解决上次的问题 无论怎么样构建无法用postman接口调试
这是因为 你在
star.sh中
1 2 3 4 5 6 7 8 9 10 #!/bin/sh set -e echo "run db migrations" /app/migrate -path /app/migration -database "$DB_SOURCE" -verbose up echo "start the app" exec "$@"
解决
使用的连接数据库的 参数是 $DB_SOURCE” 但是你在app.env中配置的名字不是DB_SOURCE 是DATABASE_URL 这种错误造成的原因可能是目前你并不了解一些列的工具是如何真正使用的没有真正了解
之前的配置 都是用DATABASE_URL来配置的
1 2 3 4 5 DATABASE_URL=postgres://root:secret@localhost:5432/simple_bank?sslmode=disable MIGRATION_URL=project/simplebank/db/migration HTTPServerAddress=0.0.0.0:8080 TOKEN_SYMMETRIC_KEY=12345678901234567890123456789012 ACCESS_TOKEN_DURATION=15m
更改为 DB_SOURCE后api测试成功
三.11.23日 部署应用程序
创建AWS(最大的云提供商)账户部署应用程序 地址 https://aws.amazon.com/free/
emmm不知道银行卡的cvv
自动构建docker镜像并推送到AWS ECR 1.创建一个存储库存储docker镜像
将docker 镜像推送到CLI 每当新代码合并到主分支时 我们将使用 Github Actions自动构建标记和推送镜像
deploy.yml 关键
目前没有招商卡无法使用AWS 先使用快过期的aliyun试一试
添加go到linux环境
1 2 3 4 5 6 7 8 echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin [root@iZt4nbaeq7uzlvq978l1xqZ simplebank]# ^C [root@iZt4nbaeq7uzlvq978l1xqZ simplebank]# export PATH=$PATH:/usr/local/go/bin [root@iZt4nbaeq7uzlvq978l1xqZ simplebank]# go run main.go go: downloading github.com/jackc/pgx/v5 v5.7.1 go: downloading github.com/gin-gonic/gin v1.10.0
四.11.29日 尝试 在仅剩5个月的服务器中 把这个简单的项目部署到服务器上
配置服务器的docker的yum源 否则下载东西很费劲
设置国内镜像【不设置可能会导致拉取镜像失败】 进入/etc/docker文件夹下,修改daemon.json。如果文件不存在则,创建该文件。
daemon.json文件内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "registry-mirrors" : [ "https://jkfdsf2u.mirror.aliyuncs.com", "https://registry.docker-cn.com" ], "insecure-registries" : [ "docker-registry.zjq.com" ], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "10" }, "data-root": "/data/docker" }
拉取docker pull镜像
1 2 3 docker pull postgres:12-alpine
。。。。 配置不够cpu直接干到100% 看看有没有 简化的方法
11.30日
还是执着一点 弄了一台2核2gb的服务器 用docker部署
首先是源的配置 安装docker 安装docker-compose
然后是构建项目中出现的问题 反复构建
赋予权限等
权限问题在ubuntu中也是一个很重要的问题 哪个用户使用ubuntu也会导致不同的结果
1 2 3 4 5 6 7 8 9 从 ls -ld 命令的输出可以看到,/home/ubuntu/projects/simplebank 目录的所有者和所属组都是 ubuntu,权限也允许当前用户进行访问。这意味着该目录的所有权和权限没有问题。 但根据 Git 提示的错误信息,Git 依然检测到目录的所有权问题,因此需要添加该目录到 安全目录 列表中。 解决方案: 运行以下命令,将该目录添加到 Git 的安全目录列表中: git config --global --add safe.directory /home/ubuntu/projects/simplebank
看到希望了
1 2 3 4 5 root@VM-12-4-ubuntu:/home/ubuntu/projects/simplebank# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES root@VM-12-4-ubuntu:/home/ubuntu/projects/simplebank# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 758e9432d178 e054039bb12c "/app/start.sh /app/…" 27 minutes ago Created 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp simplebank
还需要配置数据库吗??我有点蒙了
docker run与docker start的区别
五.2024年 11.30日 21:27分 成了把项目成功部署到了云服务器上太不容易了
1 2 3 4 5 6 7 8 root@VM-12-4-ubuntu:/home/ubuntu/projects/simplebank# docker run --name simplebank -p 80:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:secret@172.17.0.2:5432/simple_bank?sslmode=disable" simplebank:latest run db migrations 2024/11/30 13:25:56 no change 2024/11/30 13:25:56 Finished after 977.24µs 2024/11/30 13:25:56 Closing source and database start the app Received request: {Username:Zhonghe FullName:zhaohzonghe Email:3041322213@qq.com Password:zzh123456} [GIN] 2024/11/30 - 13:26:07 | 200 | 75.179039ms | 202.97.179.126 | POST "/users"
为什么把端口8080:8080改成 80:8080就好用了 啊啊啊啊好兴奋 感谢老哥们
从把项目移动到 ubuntu 配置dockers环境
使用docker build构建项目
然后就是用postman测试
这期间 多次使用的
Docker 指令 docker run
docker images
docker ps -a
docekr pull
docker build -t simplebank:latest .
docker network create bank_network
docker network rm 0fd871187ef1
docker rm simplebank
docker rmi
docker network connect bank-network postgres12
docker network ls
docker container inspect postgres12
目前服务器中的 postgres12大体网络模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 root@VM-12-4-ubuntu:/home/ubuntu/projects/simplebank# docker container inspect postgres12 [ { "Id": "100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47", "Created": "2024-11-30T09:24:02.785101065Z", "Path": "docker-entrypoint.sh", "Args": [ "postgres" ], "State": { "Status": "running", "Running": true, "Paused": false, "Restarting": false, "OOMKilled": false, "Dead": false, "Pid": 315509, "ExitCode": 0, "Error": "", "StartedAt": "2024-11-30T11:54:14.985494404Z", "FinishedAt": "2024-11-30T11:35:40.792853655Z" }, "Image": "sha256:486566ce0ca8f59e321b2b5999de4b50237b2c60bcc3414d8a602fb96cb12c6f", "ResolvConfPath": "/data/docker/containers/100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47/resolv.conf", "HostnamePath": "/data/docker/containers/100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47/hostname", "HostsPath": "/data/docker/containers/100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47/hosts", "LogPath": "/data/docker/containers/100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47/100ff1a5f0bf6e1f0447fff800aaa00ba54edc2cf19826eef512a442c2ec3a47-json.log", "Name": "/postgres12", "RestartCount": 0, "Driver": "overlay2", "Platform": "linux", "MountLabel": "", "ProcessLabel": "", "AppArmorProfile": "docker-default", "ExecIDs": null, "HostConfig": { "Binds": null, "ContainerIDFile": "", "LogConfig": { "Type": "json-file", "Config": { "max-file": "10", "max-size": "10m" "Networks": { "bank_network": { "IPAMConfig": {}, "Links": null, "Aliases": [], "MacAddress": "02:42:ac:12:00:02", "DriverOpts": {}, "NetworkID": "c2a3ada685148d5607a5a6fc39e1690e5fbd161f0607df5a3a189f74ced100fa", "EndpointID": "07a57c58250657bf968d33d1f93cea6e9225d0cae314648d1b1c639c3811c9c1", "Gateway": "172.18.0.1", "IPAddress": "172.18.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DNSNames": [ "postgres12", "100ff1a5f0bf" ] }, "bridge": { "IPAMConfig": null, "Links": null, "Aliases": null, "MacAddress": "02:42:ac:11:00:02", "DriverOpts": null, "NetworkID": "83e7fddfe207131e6199fb11fb5daa38bf044b67817fba2de02bd7f1639d4bb8", "EndpointID": "d9449d910f4e7be735031acb301f0e418999b091bb8c75450fecf983eca2aa24", "Gateway": "172.17.0.1", "IPAddress": "172.17.0.2", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DNSNames": null } } } } ]
还是看跟着课程走一走吧
AWS的EKS
将工作节点 添加到EKS集群 使用 kubectl 连接到集群
如何创建新的EKS集群并向其中添加工作节点 大多都是用AWS目前没有卡还是先不要弄了
学习一下其他的知识
进阶后端 master haha
管理用户会话
用PASETO JWT作为基于令牌的身份验证
因为这些是无状态设计 这些令牌不会存储到数据库中 寿命应该很短
他们的过期时间通常为10~15分钟 如果token每次都在这么短时间过期重新输入用户名和密码一定不是一个好的体验
刷新令牌
在服务器上维护有状态的会话
它将存储在数据库中 生命周期长
创建一个新的字段添加到app.env中
REFRESH_TOKEN_DURATION=24h
同时config中添加新字段
RefreshTokenDuration time.Duration mapstructure:"REFRESH_TOKEN_DURATION"
使用指令 migrate create -ext sql -dir db/migration -seq
1 ,用于创建一个新的迁移文件。该指令参数的意义如下:<migration_name>表示迁移文件的名称;-ext sql指定迁移文件的扩展名;-dir db/migration定义了迁移文件的存储路径;-seq代表创建顺序迁移文件,并在文件名前加上序号。这个命令会在指定目录下生成两个文件,一个用于执行迁移(.up.sql),另一个用于回滚迁移(.down.sql),以实现数据库的版本控制和变更管理。
add_sessions.up.sql 1 2 3 4 5 6 7 8 9 10 11 12 13 CREATE TABLE "sessions" ( "id" uuid PRIMARY KEY, "username" varchar NOT NULL, "refresh_token" varchar NOT NULL, "user_agent" varchar NOT NULL, "client_ip" varchar NOT NULL, "is_blocked" boolean NOT NULL DEFAULT false, "expires_at" timestamptz NOT NULL, "created_at" timestamptz NOT NULL DEFAULT (now()) ); ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
“is_blocked” boolean NOT NULL DEFAULT false, 添加bool列来阻止会话 以防止刷新令牌被泄露
“expires_at” timestamptz NOT NULL, 刷新令牌的过期时间
ALTER TABLE “sessions” ADD FOREIGN KEY (“username”) REFERENCES “users” (“username”); 外键约束
11.30日 理清楚sqlc generate
到底是什么意思
依赖于sqlc.yml文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 version: "2" sql: - schema: "./db/migration" queries: "./db/query" engine: "postgresql" gen: go: package: "db" out: "./db/sqlc" sql_package: "pgx/v5" emit_json_tags: true emit_interface: true emit_empty_slices: true overrides: - db_type: "timestamptz" go_type: "time.Time" - db_type: "uuid" go_type: "github.com/google/uuid.UUID"
指定一些列路径 自动生成代码到哪个位置
依赖的是.sql文件自动生成 相关的代码
12.2日 加入更多的响应