英文:
Go - Variable initialized and non nil, but nil for other functions
问题
有点难以写一个与我的当前问题相匹配的标题...
我有一个main()函数,它使用另一个包(database_sql)中的一个函数。这个函数初始化了一个全局变量sql.DB*。初始化后,这个变量不是nil,但对于其他函数来说,这个变量仍然是nil...让我们看一下下面的代码!
main.go
package main
import (
"net/http"
db "./database_sql"
router "./router"
)
func main() {
db.Init_SQL();
router.Init_routes()
http.ListenAndServe(":8080", router.GetRouter())
}
db.go
package database_sql
import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB // 全局变量,用于在main和HTTP处理程序之间共享
//var dbi *databaseinfos
func Init_SQL() {
dbi := databaseinfos{
user: "something",
password: "something",
name: "something",
address: "something",
port: "2323",
url: ""}
dbi.url = (dbi.user + ":" + dbi.password + "@tcp(" + dbi.address + ":" + dbi.port + ")/" + dbi.name)
db_init_var, err := sql.Open("mysql", dbi.url)
if err != nil {
fmt.Println("初始化数据库连接时出错:%s", err.Error())
}
err = db_init_var.Ping()
if err != nil {
fmt.Println("打开数据库连接时出错:%s", err.Error())
}
// 在这里,你可以测试一下,我的变量是否被初始化了
if err == nil {
DB = db_init_var
if DB == nil {
fmt.Println("NIL DB !!")
}
if db_init_var == nil {
fmt.Println("NIL db_init_var !!")
}
fmt.Println(dbi.url)
}
}
现在,一切都正常!现在,我将使用上面声明的DB变量测试http://xxxxxxx/login,使用用户名/密码。一切都正常工作,现在是时候使用我的DB变量在数据库中进行请求了。
request_user.go
package database_sql
import (
"encoding/json"
"fmt"
"net/http"
m "models"
)
func CanLogin(user m.User) (bool, m.User, m.StatusBack) {
// 准备读取数据的语句
stmtOut, err := DB.Prepare("SELECT username, password, token FROM users WHERE username = ? AND password = ?")
if err != nil {
return false, user, m.StatusBack{ToString: err.Error(), IdStatus: http.StatusInternalServerError}
}
defer stmtOut.Close()
// 查询用户
err = stmtOut.QueryRow(user.Username, user.Password).Scan(&user.Username, &user.Password, &user.UUID) // WHERE user is registered
if err != nil {
return false, user, m.StatusBack{ToString: "未找到用户。", IdStatus: http.StatusNotFound}
} else {
j, err := json.Marshal(user)
if err == nil {
return true, user, m.StatusBack{ToString: string(j), IdStatus: http.StatusOK}
} else {
return false, user, m.StatusBack{ToString: err.Error(), IdStatus: http.StatusInternalServerError}
}
}
}
然而,当我进行SQL查询时,下面这行代码显示DB是nil。
stmtOut, err := DB.Prepare("SELECT username, password, token FROM users WHERE username = ? AND password = ?")
我进行了很多测试,比如尝试使用'='而不是':='进行初始化。我尝试了'if DB == nil'语句,DB始终为nil...为什么?我在初始化路由之前初始化了我的数据库变量,所以从技术上讲,当我的路由器将重定向到一个函数时,这个函数不会使用一个数据库变量(DB)为nil,对吗?
谢谢你的回答!
编辑2
我阅读了你的回答,然后按照你的要求对代码进行了一些修改。在Init_SQL之后,DB不再是nil!
main.go
package main
import (
"fmt"
"net/http"
db "./database_sql"
router "./router"
)
func main() {
if db.Init_SQL() == true {
if db.DB == nil {
fmt.Println("db.DB是nil!")
}
if router.Init_routes() == true {
http.ListenAndServe(":8080", router.GetRouter())
}
}
}
所以,现在我的两个初始化函数在成功时返回true,在失败时返回false。我认为这可能是一个异步问题,我这样做是为了等待每个函数结束后才继续我的“逐步”操作。
英文:
It's a bit hard to write a title that match with my current problem..
I have a main() function which uses a function in another package (database_sql). This function initializes a global variable sql.DB*. After the initialization, the variable isn't nil, but for the others functions, this variable is still nil.. Let see the code below !
main.go
package main
import (
"net/http"
db "./database_sql"
router "./router"
)
func main() {
db.Init_SQL();
router.Init_routes()
http.ListenAndServe(":8080", router.GetRouter())
}
db.go
package database_sql
import (
"fmt"
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
var DB *sql.DB // global variable to share it between main and the HTTP handler
//var dbi *databaseinfos
func Init_SQL() {
dbi := databaseinfos{
user: "something",
password: "something",
name: "something",
address: "something",
port: "2323",
url: ""}
dbi.url = (dbi.user + ":" + dbi.password + "@tcp(" + dbi.address + ":" + dbi.port + ")/" + dbi.name)
db_init_var, err := sql.Open("mysql", dbi.url)
if err != nil {
fmt.Println("Error on initializing database connection: %s", err.Error())
}
err = db_init_var.Ping()
if err != nil {
fmt.Println("Error on opening database connection: %s", err.Error())
}
// Here, as you can test, my variable is initialized
if err == nil {
DB = db_init_var
if DB == nil {
fmt.Println("NIL DB !!")
}
if db_init_var == nil {
fmt.Println("NIL db_init_var !!")
}
fmt.Println(dbi.url)
}
}
Now, everything is ok ! I will now test http://xxxxxxx/login with an username/password. Everything's working fine and now, it's the time to make a request in the database with my DB variable declared above.
request_user.go
package database_sql
import (
"encoding/json"
"fmt"
"net/http"
m "models"
)
func CanLogin(user m.User) (bool, m.User, m.StatusBack) {
// Prepare statement for reading data
stmtOut, err := DB.Prepare("SELECT username, password, token FROM users WHERE username = ? AND password = ?")
if err != nil {
return false, user, m.StatusBack{ToString: err.Error(), IdStatus: http.StatusInternalServerError}
}
defer stmtOut.Close()
// Query the user
err = stmtOut.QueryRow(user.Username, user.Password).Scan(&user.Username, &user.Password, &user.UUID) // WHERE user is registered
if err != nil {
return false, user, m.StatusBack{ToString: "User not found.", IdStatus: http.StatusNotFound}
} else {
j, err := json.Marshal(user)
if err == nil {
return true, user, m.StatusBack{ToString: string(j), IdStatus: http.StatusOK}
} else {
return false, user, m.StatusBack{ToString: err.Error(), IdStatus: http.StatusInternalServerError}
}
}
}
However, when I'm making my sql request, the following line says that DB is nil.
stmtOut, err := DB.Prepare("SELECT username, password, token FROM users WHERE username = ? AND password = ?")
I made lot of test such as trying to initialize it with '=' and no ':='. I tried 'if DB == nil' statement and DB is always nil.. Why? I initialize my database var before initialize my router, so technically, when my router give a redirection to a function, this function wouldn't use a database var (DB) nil, isn't?
Thank for your answers !
EDIT 2
I red your answers and then, I edited my code like you asked with some adds. After init_SQL, DB isn't nil !
main.go
package main
import (
"fmt"
"net/http"
db "./database_sql"
router "./router"
)
func main() {
if db.Init_SQL() == true {
if db.DB == nil {
fmt.Println("db.DB is nil !")
}
if router.Init_routes() == true {
http.ListenAndServe(":8080", router.GetRouter())
}
}
}
So, now my 2 init functions return true on success and false on failure. I thought it could be an asynchronous problem, I made it like that to wait the end of each functions to continue my 'step by step'
答案1
得分: 1
首先,像那样导出一个全局变量是不符合惯用法的。复杂类型应该由运行时初始化(你在main()
中已经做了),但是要从运行时全局保留以控制处理。
你还漏掉了一个db.Close()
。
我相信几年前在首次使用MySQL时遇到了与你的模式相同的问题。在创建一个局部作用域指针,然后将其分配给全局变量时有一种奇怪的方式。通常最好直接将其分配给全局变量。但我认为核心问题是你需要将指针分配给指针。
我使用的模式是将可测试的database/sql *DB
保留在全局状态中,在初始化时进行初始化。为什么要重新发明轮子,当轮子已经运转良好:
package main
import (
...
"database/sql"
"mypackage-that-inits-db/database"
)
var db *sql.DB
func main() {
var err error
db, err = database.Init_SQL(...params)
if err != nil {
...
}
defer db.Close()
InitUsers(db)
InitStats(db)
...
http.ListenAndServe(...)
// 现在在其他代码中注入全局依赖的db
//
globalStats := NewStatsEngine(db)
globalStats.RecordUpdateToSomething(...)
users := getUsers(db)
... 等等
}
这通常是在其他代码中看到的模式。
注意在调用者中对defer和Close的控制。
无法轻松测试
还要注意,通过创建一个模拟的sql.DB
对象并将其注入到NewStatsEngine、getUsers等中,你现在可以轻松地测试其他包中的代码,而不必模拟一个全局变量,进行测试、拆除和再次设置以进行测试、测试、拆除等。自从Go 1.5以来,测试可以并发运行,所以如果你保留包中的全局DB变量,甚至需要在其周围放置一个mutex.Lock。切换到像我在上面的代码中演示的IoC模式,可以更轻松地测试(和调试)你的代码。
英文:
First, it is not idomatic to export a global var like that. Complex types should be initialized by the runtime (which you are doing with main()
), but retained globally from the runtime to control the disposal.
You are also missing a db.Close()
.
I believe I ran I to the same issue with your pattern a few years ago when first using MySQL. There is an oddness with the way one creates a local scoped pointer, and then assign it to a global var. It is usually better to assign it directly to global var. But I think the core issue is you need to assign the pointer to the pointer.
The pattern I use is to keep the testable database/sql *DB
in the global state where I initialize it. Why reinvent the wheel when the wheel works:
package main
import (
...
"database/sql"
"mypackage-that-inits-db/database"
)
var db *sql.DB
find main() {
var err error
db, err = database.Init_SQL(...params)
if err != nil {
...
}
defer db.Close()
InitUsers(db)
InitStats(db)
...
http.ListenAndServe(...)
// now inject db global dependency in other code
//
globalStats := NewStatsEngine(db)
globalStats.RecordUpdateToSomething(...)
users := getUsers(db)
... etc
}
That's typically the pattern you see in other code.
Note the control over defer and Close, where it belongs here in caller.
Can't Test that easily
Also note that your code in other packages now becomes easily testable by creating a mock sql.DB
object and injecting it into NewStatsEngine, getUsers, etc instead of having to mock a global var, test, tear down and setup again for test test, test, teardown, etc. Also since Go 1.5, tests could be run concurrently so you'll even need to put a mutex.Lock around your global DB var if you leave your package like that. Switching to a IoC pattern, like Dependency Injection which I demo'd in the code above, makes so much easier to test (and debug) your code.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论