Procedimentos armazenados com go-mssqldb

O go-mssqldb driver suporta chamar procedimentos armazenados com parâmetros de entrada, parâmetros de saída e valores de status de retorno. Este artigo aborda os padrões comuns.

A maioria dos trechos deste artigo pressupõe que a configuração usual database/sql já esteja configurada, com database/sql importado como sql, ctx disponível e db inicializado. Snippets incluem blocos de importação apenas quando introduzem pacotes adicionais como context, fmt, log, os, ou github.com/microsoft/go-mssqldb. Quando um bloco de código continua intencionalmente o mesmo exemplo, use = para valores que já foram declarados anteriormente nesse exemplo; use := para declarações novas em trechos independentes.

Chamar um procedimento armazenado

Use ExecContext ou QueryContext com o nome do procedimento diretamente:

_, err := db.ExecContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6))

Prefira o nome do procedimento seguido dos parâmetros para chamadas comuns de procedimento. Use uma string explícita EXECUTE ... apenas quando precisar incorporar a chamada em um lote maior de Transact-SQL (T-SQL) ou use sintaxe que não seja representada por sql.Named parâmetros.

Parâmetros de saída

Use sql.Named com sql.Out para receber valores dos parâmetros de saída:

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

ctx := context.Background()
var employeeCount int64
_, err := db.ExecContext(ctx, "dbo.GetEmployeeCount",
    sql.Named("count", sql.Out{Dest: &employeeCount}))
if err != nil {
    log.Fatal(err)
}
fmt.Println("Employee count:", employeeCount)

O procedimento correspondente do T-SQL:

CREATE PROCEDURE dbo.GetEmployeeCount
    @count INT OUTPUT
AS
BEGIN
    SELECT @count = COUNT(*) FROM HumanResources.Employee;
END;

Parâmetros de entrada/saída

Para parâmetros que são tanto entrada quanto saída, defina In: true na sql.Out struct:

var result int64 = 10
_, err := db.ExecContext(ctx, "dbo.DoubleValue",
    sql.Named("value", sql.Out{Dest: &result, In: true}))
if err != nil {
    log.Fatal(err)
}
fmt.Println("Doubled:", result)

Status de retorno

Use mssql.ReturnStatus para capturar o valor inteiro de retorno de um procedimento armazenado. Este exemplo usa o procedimento AdventureWorks dbo.uspGetEmployeeManagers :

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

    mssql "github.com/microsoft/go-mssqldb"
)

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

fmt.Println("Return status:", returnStatus)

ReturnStatus funciona com ExecContext e QueryContext, mas não com QueryRowContext.

Você pode combinar ReturnStatus com os parâmetros do procedimento. Este exemplo continua o exemplo anterior, então ele reutiliza a mssql importação mostrada anteriormente e usa o procedimento AdventureWorks dbo.uspGetEmployeeManagers :

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

Conjuntos de resultados a partir de procedimentos armazenados

Se o procedimento armazenado retorna conjuntos de resultados, use QueryContext:

rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers", sql.Named("BusinessEntityID", 6))
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var reportingLevel int
    var employeeID int
    var employeeFirstName string
    var employeeLastName string
    var organizationNode string
    var managerFirstName string
    var managerLastName string
    if err := rows.Scan(&reportingLevel, &employeeID, &employeeFirstName, &employeeLastName, &organizationNode, &managerFirstName, &managerLastName); err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Level %d: %d %s %s (manager: %s %s)\n",
        reportingLevel, employeeID, employeeFirstName, employeeLastName, managerFirstName, managerLastName)
    _ = organizationNode
}

Para procedimentos que retornam múltiplos conjuntos de resultados, use rows.NextResultSet(). Para mais informações, veja Consultas e declarações.

Leia todas as linhas antes de usar parâmetros de saída ou status de retorno

O SQL Server envia parâmetros de saída e valores de status de retorno após o término dos conjuntos de resultados. Se você ler uma variável de saída antes de consumir todas as linhas, o valor ainda pode estar incompleto.

Este exemplo continua o anterior e reutiliza a mssql importação mostrada anteriormente.

Ele não imprime os dados da linha. O loop consome apenas o conjunto de resultados, então returnStatus fica disponível após o término da consulta.

var returnStatus mssql.ReturnStatus
rows, err := db.QueryContext(ctx, "dbo.uspGetEmployeeManagers",
    sql.Named("BusinessEntityID", 6),
    &returnStatus)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var reportingLevel int
    var employeeID int
    var employeeFirstName string
    var employeeLastName string
    var organizationNode string
    var managerFirstName string
    var managerLastName string
    if err := rows.Scan(&reportingLevel, &employeeID, &employeeFirstName, &employeeLastName, &organizationNode, &managerFirstName, &managerLastName); err != nil {
        log.Fatal(err)
    }
    _ = organizationNode
    _ = managerFirstName
    _ = managerLastName
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

fmt.Println("Return status:", returnStatus)

ExecContext Use quando o procedimento não retorna linhas. Se o procedimento retornar linhas, espere até que todas as linhas e os conjuntos de resultados sejam consumidos antes de ler os parâmetros de saída ou o status de retorno.

Tabelas temporárias e procedimentos armazenados

Quando você cria uma tabela temporária e depois faz consultas nela em chamadas separadas, as operações podem ser executadas em conexões diferentes do pool. Como as tabelas temporárias são restritas a uma única conexão, a segunda chamada pode não ver a tabela.

Para trabalhar com tabelas temporárias, use uma abordagem de conexão única:

conn, err := db.Conn(ctx)
if err != nil {
    log.Fatal(err)
}
defer conn.Close()

_, err = conn.ExecContext(ctx, "CREATE TABLE #TempItems (Id INT, Name NVARCHAR(50))")
if err != nil {
    log.Fatal(err)
}

_, err = conn.ExecContext(ctx, "INSERT INTO #TempItems VALUES (1, N'Item A')")
if err != nil {
    log.Fatal(err)
}

rows, err := conn.QueryContext(ctx, "SELECT * FROM #TempItems")
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

Alternativamente, encapsule as operações em uma transação, que as fixa automaticamente na mesma conexão.

Capturar mensagens PRINT e RAISERROR

As instruções do SQL Server PRINT eRAISERROR, com gravidade de 0 a 10, geram mensagens informativas que não são retornadas como erros de Go. Para capturar essas mensagens, ative o log parâmetro de conexão com a flag 2 (mensagens):

sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&log=2

Com o logging ativado, o driver grava a saída PRINT e mensagens RAISERROR de baixa gravidade no pacote log padrão do Go. Para capturá-los programaticamente, defina um logger personalizado antes de abrir a conexão:

import (
    "database/sql"
    "log"
    "os"

    mssql "github.com/microsoft/go-mssqldb"
)

// Direct driver messages to a custom logger.
mssql.SetLogger(log.New(os.Stdout, "mssql: ", log.LstdFlags))

db, err := sql.Open("sqlserver",
    "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&log=2")

Então, qualquer procedimento armazenado que use PRINT ou RAISERROR(..., 0, 1) envia suas mensagens para o seu registrador.

Note

RAISERROR de gravidade 11 ou superior gera um erro em Go que você pode tratar com a verificação normal de erros. Apenas mensagens de gravidade 0-10 exigem o log parâmetro para serem capturadas.

Para mais informações sobre sinalizadores de registro e captura programática de logs, consulte Registro em log e diagnósticos.