在Go中关闭/重新打开MySQL连接时出现意外的“无此主机错误”

huangapple go评论74阅读模式
英文:

Unexpected "no such host error" when closing/reopening MySQL connection in Go

问题

我正在学习Go的初级阶段,并且正在尝试熟悉Go与SQL数据库交互的方式。我之前在SQL数据库方面有很多经验,但几乎没有使用过ORM。

作为在数据库中插入/删除数据的简单练习,我编写了addCardTest()函数来向我的MySQL数据库中的表添加一行数据,以及listCardsTest()函数来打印该表上简单查询的结果。在main()函数中,我尝试按照这个顺序运行这两个函数。然而,当我运行第二个函数时,我收到了一个错误消息:

panic: dial tcp: lookup rpi.local on 172.24.32.1:53: no such host

goroutine 1 [running]:
main.listCardsTest()
        /home/tmiku/go/src/mtg/mtg.go:23 +0x325
main.main()
        /home/tmiku/go/src/mtg/mtg.go:12 +0x1c
exit status 2

我发现,如果我从第一个调用的函数中删除"defer db.Close()"这一行,这个错误就会消失。我对这个结果感到惊讶,并且对导致这个错误的原因有很多疑问。

  • 在底层,是否有一些并发工作正在进行(即listCardsTest()在addCardsTest()完成之前运行)?在Go中使用外部数据库时,这是否是可以预期的?
  • 一个单独的Go程序只能创建一个连接池与任何给定的数据库进行连接吗?当我尝试打开另一个连接时会发生什么?这作为一种限制是有道理的,但是"no such host"错误似乎是一种奇怪/不直观的实施方式。
  • 访问数据库的函数是否应该写成假设连接已经打开,并将数据库信息作为参数传递?将打开/关闭数据库连接包装到执行其他操作的函数中是否是一种不好的做法?虽然这个错误似乎在推动我朝这个方向发展,但我在明显的地方很难找到相关建议的文档。

以下是完整的代码。MySQL数据库的主机名是rpi.local,请原谅硬编码的凭据(我正在学习)。

package main

import (
	"database/sql"
	"fmt"

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

func main() {
	addCardTest()
	listCardsTest()
}

func listCardsTest() {
	db, err := sql.Open("mysql", "tmiku:pass@tcp(rpi.local:3306)/mtg")
	if err != nil {
		panic(err)
	}
	defer db.Close()

	rows, err := db.Query("select cardid, name, manacost, type from Cards")
	if err != nil {
		panic(err)
	}
	defer rows.Close()

	var id int
	var name, manacost, cardtype string // 需要先初始化这些变量...

	for rows.Next() {
		err := rows.Scan(&id, &name, &manacost, &cardtype) // ...这样Scan()函数才有地方存放它们。
		if err != nil {
			panic(err)
		}
		// 打印结果
		fmt.Println(id, name, manacost, cardtype)
	}
}

func addCardTest() {
	// 打开数据库,db是指向数据库对象的指针
	db, err := sql.Open("mysql", "tmiku:pass@tcp(rpi.local:3306)/mtg")
	if err != nil {
		panic(err)
	}
	defer db.Close() // 从这个函数中删除了这一行,修复了错误

	// 执行插入操作
	result, err := db.Exec("insert into mtg.Cards (name, manacost, ruletext, type, subtype, power, toughness) values ('Stormchaser Drake', '1U', 'Flying\nWhenever Stormchaser Drake becomes the target of a spell you control, draw a card.', 'Creature', 'Drake', 2, 1)")
	if err != nil {
		panic(err)
	}
	lastId, _ := result.LastInsertId()
	fmt.Println("Added id ", lastId)
}

希望对你有所帮助!如果你有任何其他问题,请随时问我。

英文:

I'm in the early stages of learning Go, and I'm trying to build familiarity with the ways that Go can interact with SQL databases. I've worked extensively with SQL databases, but almost never with ORMs.

As a simple first exercise in inserting/deleting from a database, I've written the function addCardTest() to add a row to a table in my MySQL database, and the function listCardsTest() to print the results of a simple query on that table. In the main() function, I try to run the two functions in that order. However, I receive an error on the second function I run:

panic: dial tcp: lookup rpi.local on 172.24.32.1:53: no such host

goroutine 1 [running]:
main.listCardsTest()
        /home/tmiku/go/src/mtg/mtg.go:23 +0x325
main.main()
        /home/tmiku/go/src/mtg/mtg.go:12 +0x1c
exit status 2

I found that this error goes away if I remove the "defer db.Close()" line from the first function I call. I'm surprised that this worked, and it leaves me with many questions about what throws this error in the first place.

  • Under the hood, is there some concurrent work happening (i.e. listCardsTest() running before addCardsTest() finishes)? Is this to be expected when working with external databases in Go?
  • Can a single Go program only create a single connection pool to any given database? What happens when I try to open another one? This makes sense as a limitation, but the "no such host" error seems like a bizarre/unintuitive way to enforce that.
  • Should functions that access databases be written with the assumption that the connection is already open, and accept the database info as a parameter? Is it bad form to wrap the opening/closing of a database connection into a function that does other things? It feels like the error is pushing me in this direction, but I had trouble finding that advice documented anywhere obvious.

Full code below. The hostname of the MySQL database is rpi.local, excuse the hardcoded credentials (I'm learning).

package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
func main() {
addCardTest()
listCardsTest()
}
func listCardsTest() {
db, err := sql.Open("mysql", "tmiku:pass@tcp(rpi.local:3306)/mtg")
if err != nil {
panic(err)
}
defer db.Close()
rows, err := db.Query("select cardid, name, manacost, type from Cards")
if err != nil {
panic(err)
}
defer rows.Close()
var id int
var name, manacost, cardtype string // need to initialize these first...
for rows.Next() {
err := rows.Scan(&id, &name, &manacost, &cardtype) // ...so Scan() has somewhere to put them.
if err != nil {
panic(err)
}
//print the results
fmt.Println(id, name, manacost, cardtype)
}
}
func addCardTest() {
//open the database, db is a pointer to the db object
db, err := sql.Open("mysql", "tmiku:pass@tcp(rpi.local:3306)/mtg")
if err != nil {
panic(err)
}
defer db.Close() //REMOVED THIS LINE, FIXED ERROR
//run our insert
result, err := db.Exec("insert into mtg.Cards (name, manacost, ruletext, type, subtype, power, toughness) values ('Stormchaser Drake', '1U', 'Flying\nWhenever Stormchaser Drake becomes the target of a spell you control, draw a card.', 'Creature', 'Drake', 2, 1)")
if err != nil {
panic(err)
}
lastId, _ := result.LastInsertId()
fmt.Println("Added id ", lastId)
}

答案1

得分: 0

我稍微重构了一下你的代码,以便让你更好地理解我通常如何编写这段代码。首先,让我介绍一下代码,然后我会逐个介绍所有相关部分。为了演示的目的,所有的代码都被写入了一个名为 main.go 的单个文件中:

package main

import (
	"database/sql"
	"fmt"

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

type card struct {
	Id   int
	Name string
}

func addCardTest(sql *sql.DB, card card) error {
	res, err := sql.Exec(`INSERT INTO cards (name) VALUES (?)`, card.Name)
	if err != nil {
		return fmt.Errorf("插入卡片时出错:%v", err)
	}
	lastId, err := res.LastInsertId()
	if err != nil {
		return fmt.Errorf("获取最后一个ID时出错:%v", err)
	}
	fmt.Println("插入的卡片ID为:", lastId)
	return nil
}

func listCardsTest(sql *sql.DB) ([]card, error) {
	rows, err := sql.Query("select id, name from cards")
	if err != nil {
		return nil, fmt.Errorf("获取卡片时出错:%v", err)
	}
	defer rows.Close()
	var currentCard card
	res := make([]card, 0)
	for rows.Next() {
		if err := rows.Scan(&currentCard.Id, &currentCard.Name); err != nil {
			return nil, fmt.Errorf("扫描行时出错:%v", err)
		}
		res = append(res, currentCard)
	}
	return res, nil
}

func main() {
	dsn := "root:root@tcp(127.0.0.1:3306)/card"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()
	if err := addCardTest(db, card{Name: "lorem ipsum"}); err != nil {
		panic(err)
	}
	if err := addCardTest(db, card{Name: "second card"}); err != nil {
		panic(err)
	}
	cards, err := listCardsTest(db)
	if err != nil {
		panic(err)
	}
	for _, v := range cards {
		fmt.Println(v)
	}

}

现在,让我们仔细看看每个部分。

请注意,为了通过Docker运行MySQL实例,我使用了以下命令:

docker run --name some-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql

addCardTest 函数

在这里,有几个改进点:

  • 函数参数。你应该期望一个指向SQL客户端的指针(稍后会详细介绍)和一个要插入的卡片。
  • 你应该使用 Exec 方法来执行命令(例如 INSERT 语句),而不是 Query
  • 即使是示例问题或POC,也不应该硬编码数据。
  • 总是检查错误。

现在,让我们切换到读取逻辑。

listCardsTest 函数

在这里,代码与之前大致相同。

main 函数

在这个函数中,你应该实例化一个指向MySQL数据库的句柄,从现在开始将使用该句柄。你只需要调用一次 sql.Open() 函数,并在 main 函数中使用 defer 调用一次 Close() 方法。所有这些都必须在 main 函数中完成。然后,你应该在与数据库相关的函数中(例如上面提到的 listCardsTestaddCardTest)期望这样的参数。

我强烈建议你查看这个链接,它解释了如何使用 sql.DB 结构体。

如果我的程序能帮助你解决问题,请告诉我,谢谢!

英文:

I've refactored a little bit your code to let you better understand how I usually write this piece of code. First, let me present the code, then, I'll walk you through all the relevant sections. For the sake of the demo, all the code has been written into a single file called main.go:

package main

import (
	"database/sql"
	"fmt"

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

type card struct {
	Id   int
	Name string
}

func addCardTest(sql *sql.DB, card card) error {
	res, err := sql.Exec(`INSERT INTO cards (name) VALUES (?)`, card.Name)
	if err != nil {
		return fmt.Errorf("err while inserting a card: %v", err)
	}
	lastId, err := res.LastInsertId()
	if err != nil {
		return fmt.Errorf("err while getting the last ID: %v", err)
	}
	fmt.Println("inserted card with id:", lastId)
	return nil
}

func listCardsTest(sql *sql.DB) ([]card, error) {
	rows, err := sql.Query("select id, name from cards")
	if err != nil {
		return nil, fmt.Errorf("err while getting cards: %v", err)
	}
	defer rows.Close()
	var currentCard card
	res := make([]card, 0)
	for rows.Next() {
		if err := rows.Scan(&currentCard.Id, &currentCard.Name); err != nil {
			return nil, fmt.Errorf("err while scanning a row: %v", err)
		}
		res = append(res, currentCard)
	}
	return res, nil
}

func main() {
	dsn := "root:root@tcp(127.0.0.1:3306)/card"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		panic(err)
	}
	defer db.Close()
	if err := addCardTest(db, card{Name: "lorem ipsum"}); err != nil {
		panic(err)
	}
	if err := addCardTest(db, card{Name: "second card"}); err != nil {
		panic(err)
	}
	cards, err := listCardsTest(db)
	if err != nil {
		panic(err)
	}
	for _, v := range cards {
		fmt.Println(v)
	}

}

Now, let's take a closer look at each section.
> Please note that to run the MySQL instance through Docker, I used this command:
docker run --name some-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql.

The addCardTest function

Here, there are a couple of improvements:

  • The function parameters. You should expect a pointer to an SQL client (more on that later) and a card to insert
  • You should use the method Exec meant to be used for commands (e.g. the INSERT statement) instead of the Query
  • You should not hard-code data even if it's a sample problem or a POC
  • Always check for errors

Now, let's switch to the reading logic.

The listCardsTest function

Here, the code is more or less the same as before.

The main function

Within this function, you should instantiate a handle towards the MySQL DB that has to be used from now on. You only need to invoke the function sql.Open() once and defer a single invocation to the method Close(). All of this has to be done within the main function. Then, you should expect a parameter of this kind in your DB-related functions (e.g. the listCardsTest and the addCardTest mentioned above).

I strongly suggest you check this link that explains how the sql.DB struct is supposed to be used.

Let me know if my program helps you to solve the issue, thanks!

huangapple
  • 本文由 发表于 2023年6月20日 12:22:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76511398.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定