Go version: go1.25.0 darwin/arm64.
Bun version: v1.2.16
PostgreSQL version: 17
I worked with a nullable JSONB column in PostgreSQL and found a case where an uninitialized empty interface in the model definition causes the scanJSON (code) function to panic. In this case , theExec and Scan methods stay blocked and the database connection has idle in transaction state.
In the code snippet below, I want to insert two rows: one with JSONB data and one without (expecting NULL in that column). The nullzero annotation forces query executor to select data from the table (query will have RETURNING data statement). We can see that the Data field of the second slice element is uninitialized, so it is empty interface. Empty interface is a unaddressable value, so in scanJSON function we can not call dest.Addr().Interface() for it.
package main
import (
"context"
"database/sql"
"log"
"time"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
)
func main() {
dsn := "postgres://postgres:@localhost:55432/postgres?sslmode=disable"
sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
db := bun.NewDB(sqldb, pgdialect.New())
log.Println("Connected to database")
_, err := db.Exec("CREATE TABLE IF NOT EXISTS test (id BIGINT PRIMARY KEY, data JSONB); TRUNCATE test;")
if err != nil {
log.Fatalln(err)
}
log.Println("Created \"test\" table")
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
rows := []model{
{
ID: 1,
Data: map[string]any{
"foo": "bar",
},
},
{
ID: 2,
},
}
_, err = db.NewInsert().
Model(&rows).
Exec(ctx)
if err != nil {
log.Fatalln(err)
}
log.Println(rows)
}
type model struct {
bun.BaseModel `bun:"table:test"`
ID int64 `bun:"id,pk"`
Data any `bun:"data,nullzero,type:jsonb"`
}
2025/11/25 21:52:20 Connected to database
2025/11/25 21:52:20 Created "test" table
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [sync.RWMutex.Lock]:
sync.runtime_SemacquireRWMutex(0x10?, 0x60?, 0x14000002301?)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/sema.go:105 +0x28
sync.(*RWMutex).Lock(0x104b54a78?)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/rwmutex.go:155 +0xf4
database/sql.(*Rows).close(0x1400011a280, {0x0, 0x0})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3443 +0x80
database/sql.(*Rows).Close(0x1400011a280)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3439 +0x30
panic({0x1047ad6c0?, 0x10481aef0?})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/panic.go:783 +0x120
reflect.Value.Addr({0x1047ca980?, 0x1400011e1b0?, 0x104819df0?})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/reflect/value.go:268 +0x64
github.com/uptrace/bun/schema.scanJSON({0x1047ca980?, 0x1400011e1b0?, 0x104b54a78?}, {0x1047a9500?, 0x14000138198?})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/schema/scan.go:361 +0x78
github.com/uptrace/bun/schema.scanJSONIntoInterface({0x1047badc0?, 0x1400011e1e8?, 0x1047e6620?}, {0x1047a9500, 0x14000138198})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/schema/scan.go:535 +0x140
github.com/uptrace/bun/schema.(*Field).ScanWithCheck(0x1047e6620?, {0x1047badc0?, 0x1400011e1e8?, 0x104780f70?}, {0x1047a9500?, 0x14000138198?})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/schema/field.go:132 +0x88
github.com/uptrace/bun/schema.(*Field).ScanValue(0x14000142300, {0x1047e6620?, 0x1400011e1e0?, 0x0?}, {0x1047a9500, 0x14000138198})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/schema/field.go:125 +0xc4
github.com/uptrace/bun.(*structTableModel).scanColumn(0x14000150000, {0x14000154000, 0x4}, {0x1047a9500, 0x14000138198})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_struct.go:314 +0xc0
github.com/uptrace/bun.(*structTableModel).ScanColumn(0x14000150000, {0x14000154000, 0x4}, {0x1047a9500?, 0x14000138198?})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_struct.go:294 +0x30
github.com/uptrace/bun.(*structTableModel).Scan(0x104a13430?, {0x1047a9500?, 0x14000138198?})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_struct.go:290 +0x98
database/sql.convertAssignRows({0x10480b380, 0x14000150000}, {0x1047a9500, 0x14000138198}, 0x1400011a280)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/convert.go:394 +0x1a48
database/sql.(*Rows).scanLocked(0x1400011a280, {0x14000023bf8, 0x1, 0x14000023b28?})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3399 +0x1f0
database/sql.(*Rows).Scan(0x1400011a280, {0x14000023bf8, 0x1, 0x1400011a280?})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3374 +0x9c
github.com/uptrace/bun.(*structTableModel).scanRow(0x14000150000, {0x10481eeb0, 0x1400013c000}, 0x1400011a280, {0x14000023bf8, 0x1, 0x1})
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_struct.go:275 +0x58
github.com/uptrace/bun.(*sliceTableModel).ScanRows(0x14000150000, {0x10481eeb0, 0x1400013c000}, 0x1400011a280)
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/model_table_slice.go:76 +0x234
github.com/uptrace/bun.(*baseQuery)._scan(0x1400000c288?, {0x10481eeb0, 0x1400013c000}, {0x10481ef90?, 0x14000140000?}, {0x14000152000, 0x5c}, {0x10481e1e0, 0x14000150000}, 0x0)
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/query_base.go:622 +0xbc
github.com/uptrace/bun.(*baseQuery).scan(0x14000140000, {0x10481eeb0?, 0x1400013c000?}, {0x10481ef90, 0x14000140000}, {0x14000152000, 0x5c}, {0x10481e1e0, 0x14000150000}, 0x0)
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/query_base.go:604 +0xa0
github.com/uptrace/bun.(*InsertQuery).scanOrExec(0x14000140000, {0x10481eeb0, 0x1400013c000}, {0x0, 0x0, 0x0}, 0x0)
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/query_insert.go:614 +0x28c
github.com/uptrace/bun.(*InsertQuery).Exec(...)
/Users/gopher/go/pkg/mod/github.com/uptrace/bun@v1.2.16/query_insert.go:569
main.main()
/Users/gopher/Projects/sql-db-issue/bun/main.go:46 +0x318
goroutine 34 [sync.Mutex.Lock]:
internal/sync.runtime_SemacquireMutex(0x104a23700?, 0x50?, 0x14000049e18?)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/runtime/sema.go:95 +0x28
internal/sync.(*Mutex).lockSlow(0x1400011a2b8)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/internal/sync/mutex.go:149 +0x170
internal/sync.(*Mutex).Lock(...)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/internal/sync/mutex.go:70
sync.(*Mutex).Lock(...)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/mutex.go:46
sync.(*RWMutex).Lock(0x1400011a2b8)
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/sync/rwmutex.go:150 +0x6c
database/sql.(*Rows).close(0x1400011a280, {0x10481d000, 0x104a49940})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3443 +0x80
database/sql.(*Rows).awaitDone(0x1400011a280, {0x10481eeb0, 0x1400013c000}, {0x0, 0x0}, {0x10481ee40, 0x14000158050})
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:3020 +0x19c
created by database/sql.(*Rows).initContextClose in goroutine 1
/Users/gopher/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.darwin-arm64/src/database/sql/sql.go:2996 +0x12c
Go version: go1.25.0 darwin/arm64.
Bun version: v1.2.16
PostgreSQL version: 17
I worked with a nullable JSONB column in PostgreSQL and found a case where an uninitialized empty interface in the model definition causes the
scanJSON(code) function to panic. In this case , theExecandScanmethods stay blocked and the database connection hasidle in transactionstate.In the code snippet below, I want to insert two rows: one with JSONB data and one without (expecting NULL in that column). The
nullzeroannotation forces query executor to select data from the table (query will haveRETURNING datastatement). We can see that theDatafield of the second slice element is uninitialized, so it is empty interface. Empty interface is a unaddressable value, so inscanJSONfunction we can not calldest.Addr().Interface()for it.Stack trace:
As we can see, this code snippet causes panic in the
scanJSONfunction and blocks the goroutine and the database connection. This case is difficult to catch because it does not return any error.Related issue golang/go#76465.