Como eu nunca havia visto um benchmark para mostrar as diferenças entre, abrir uma conexão no início do programa e utilizá-la como dependência e, abrir uma nova conexão a cada chamada, resolvi fazê-la.
Embora a maioria das pessoas que converso saberem a resposta correta, não sei se, assim como eu, elas têm a ideia de quão grande é a diferença entre essas abordagens.
Mas antes de ver o resultado, mostrarei como construí o benchmark.
Conexão & Dados
Bem simples, para abrir a conexão sqlite3, criei uma função chamada Open
.
package connections import ( "database/sql" _ "github.com/mattn/go-sqlite3" ) // Open a connection with a sqlite3 database func Open() (*sql.DB, error) { return sql.Open("sqlite3", "test.db") }
Depois, defini uma struct bem simples e o SQL para criar uma tabela no banco de dados.
package connections // User definitiontype User struct { ID int `json:"id"` Username string `json:"username"` Password string `json:"password"` } // CreateUserTable creates table on sqlite3 database func CreateUserTable() error { db, err := Open() if err != nil { return err } defer db.Close() // sql to create user table sql := `CREATE TABLE IF NOT EXISTS users( id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR NOT NULL, password VARCHAR NOT NULL);` // execute sql _, err = db.Exec(sql) return err }
Por fim, criei um arquivo cmd/main.go
para adicionar 3 registros no banco de dados.
package main import ( "log" "github.com/aprendagolang/connections") func main() { db, err := connections.Open() if err != nil { log.Fatal(err) } defer db.Close() // create table err = connections.CreateUserTable() if err != nil { log.Fatal(err) } // insert 3 users users := []connections.User{ {Username: "user1", Password: "pass1"}, {Username: "user2", Password: "pass2"}, {Username: "user3", Password: "pass3"}, } // sql for insert user sql := `INSERT INTO users (username, password) VALUES (?, ?)` for _, user := range users { _, err = db.Exec(sql, user.Username, user.Password) if err != nil { log.Fatal(err) } } }
Uma conexão por chamada
Após executar o código acima, implementei uma função bem simples para selecionar todos os registros da tabela users
, abrindo uma nova conexão a cada chamada.
package connections // SelectAllUsers select all records from users table func SelectAllUsers() ([]User, error) { db, err := Open() if err != nil { return nil, err } defer db.Close() // select all users rows, err := db.Query("SELECT * FROM users") var users []User for rows.Next() { var user User err = rows.Scan(&user.ID, &user.Username, &user.Password) if err != nil { return nil, err } users = append(users, user) } return users, err }
Com a função implementada, criei a seguinte função para benchmark.
package connections import "testing" func BenchmarkSelectAllUsers(b *testing.B) { for n := 0; n < b.N; n++ { _, err := SelectAllUsers() if err != nil { b.Errorf("Error: %s", err) } } }
Conexão sempre aberta
Nessa abordagem, criei uma struct de repository bem simples, somente com um atributo do tipo *sql.DB
.
Depois, implementei uma função para iniciá-la e um método com basicamente o mesmo código da função SelectAllUsers
, exceto pela parte do defer db.Close()
.
package connections import "database/sql" // UserRepository defines the structure of a user repository type UserRepository struct { DB *sql.DB } // NewUserRepository creates a new instance of UserRepository func NewUserRepository(db *sql.DB) *UserRepository { return &UserRepository{DB: db} } // SelectAllUsers select all records from users table func (r *UserRepository) SelectAllUsers() ([]User, error) { // select all users rows, err := r.DB.Query("SELECT * FROM users") var users []User for rows.Next() { var user User err = rows.Scan(&user.ID, &user.Username, &user.Password) if err != nil { return nil, err } users = append(users, user) } return users, err }
Por fim, implementei a função de benchmark para o método UserRepository.SelectAlUsers()
.
package connections import "testing" func BenchmarkUserRepository_SelectAllUsers(b *testing.B) { db, err := Open() if err != nil { b.Errorf("Error: %s", err) } repo := NewUserRepository(db) defer repo.DB.Close() for n := 0; n < b.N; n++ { _, err := repo.SelectAllUsers() if err != nil { b.Errorf("Error: %s", err) } } }
Repare que ambas implementações são realmente muito parecidas, só mudando a questão da conexão com o banco de dados.
Benchmark

Na imagem, da esquerda para a direita, temos:
- Nome da função executada
- Quantidade de loopings executados
- Nanosegundos por operação
- Total de bytes alocados por operação
- Total de alocações em memória por operação
Podemos observar na imagem que, abrir uma conexão por chamada conseguiu executar uma média de 4.101 chamadas, enquanto ter somente uma conexão aberta executou, espantosamente, uma média de 130.810 chamadas.
Além da quantidade de chamadas a mais, ter somente uma chamada aberta ainda “custa” quase a metade de alocações de memória e consome somente 1/3 da quantidade de bytes por operação.
Conclusão
Como dito no início do post, a maioria das pessoas que eu converso sabem qual é a melhor abordagem, mas acredito que elas, assim como eu, não sabiam dessa diferença de mais de 31x na quantidade de chamadas.
Sem dúvida, utilizar a abordagem de abrir a conexão no início do programa traz grandes benefícios em escalabilidade e consumo recursos.
Mas é claro que não podemos esquecer de fechar essas conexões em caso de crash da aplicação. Para entender um pouco melhor sobre, recomendo a leitura do post Como fazer graceful shutdown correto em um server HTTP
Se inscreva na nossa newsletter!
Até a próxima!
Algumas libs suportam também pool de conexões, que é uma abordagem ainda melhor, pois evita enfileiramento de chamadas na mesma conexão. Parabéns pelo post!