A small framework for Go designed to separate logic from data access.
It achieves this by connecting the logic layer and the data access layer via interfaces, similar to traditional Dependency Injection (DI). This reduces the dependency between the two, allowing them to be implemented and tested independently.
However, traditional DI often presented inconvenience in how methods were grouped. Typically, methods were grouped by external data service like a database or by database table. This meant the logic layer had to depend on units defined by the data access layer's concerns. Furthermore, such interfaces often contained more methods than a specific piece of logic needed, making it difficult to tell which methods were actually used in the logic without tracing the code.
This framework addresses that inconvenience. The data access interface used by a logic function is unique to that specific logic, passed as an argument to the logic function. This interface declares all the data access methods that specific logic will use.
On the data access layer side, implementations can be provided by concrete types that fulfill multiple DataAcc
derived structs.
This allows for implementation in any arbitrary unit — whether by external data service, by table,
or by functional concern.
This is achieved through the following mechanism:
- A
DataHub
struct aggregates all data access methods.DataAcc
derived structs are attached toDataHub
, givingDataHub
the implementations of the data access methods. - Logic functions accept specific, narrowly defined data access interfaces as arguments. These interfaces declare only the methods relevant to that particular piece of logic.
- The
DataHub
type implements all of these specific data access interfaces. When aDataHub
instance is passed to a logic function, the logic function interacts with it via the narrower interface, ensuring it only sees and uses the methods it needs. Using Go's embedding mechanism, a type implements an interface by methods of other structs. TheDataHub
simply needs to have methods that match the signatures of all the methods declared across the various logic-facing data access interfaces.
This approach provides strong compile-time guarantees that logic only uses what it declares, while allowing flexible organization of data access implementations.
go get github.com/sttk/sabi
First, you'll define DataSrc
which manages connections to external data services and creates
DataConn
.
Then, you'll define DataConn
which represents a session-specific connection and implements
transactional operations.
import (
"github.com/sttk/errs"
"github.com/sttk/sabi"
)
type FooDataSrc struct {}
func (ds *FooDataSrc) Setup(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (ds *FooDataSrc) Close() {}
func (ds *FooDataSrc) CreateDataConn() (sabi.DataConn, errs.Err) { return &FooDataConn{}, errs.Ok() }
type FooDataConn struct {}
func (conn *FooDataConn) Commit(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (conn *FooDataConn) PreCommit(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (conn *FooDataConn) PostCommit(ag *sabi.AsyncGroup) {}
func (conn *FooDataConn) ShouldForceBack() bool { return false }
func (conn *FooDataConn) Rollback(ag *sabi.AsyncGroup) {}
func (conn *FooDataConn) ForceBack(ag *sabi.AsyncGroup) {}
func (conn *FooDataConn) Close() {}
type BarDataSrc struct {}
func (ds *BarDataSrc) Setup(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (ds *BarDataSrc) Close() {}
func (ds *BarDataSrc) CreateDataConn() (sabi.DataConn, errs.Err) { return &BarDataConn{}, errs.Ok() }
type BarDataConn struct {}
func (conn *BarDataConn) Commit(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (conn *BarDataConn) PreCommit(ag *sabi.AsyncGroup) errs.Err { return errs.Ok() }
func (conn *BarDataConn) PostCommit(ag *sabi.AsyncGroup) {}
func (conn *BarDataConn) ShouldForceBack() bool { return false }
func (conn *BarDataConn) Rollback(ag *sabi.AsyncGroup) {}
func (conn *BarDataConn) ForceBack(ag *sabi.AsyncGroup) {}
func (conn *BarDataConn) Close() {}
Define interfaces and functions that express your application logic. These interfaces are independent of specific data source implementations, improving testability.
type MyData interface {
GetText() (string, errs.Err)
SetText(text string) errs.Err
}
func MyLogic(data MyData) errs.Err {
text, err := data.GetText()
if err != nil {
return err
}
return data.SetText(text)
}
The DataAcc
interface abstracts access to data connections.
The methods defined here will be used to obtain data connections via DataHub
and perform
actual data operations.
type GettingDataAcc struct { sabi.DataAcc }
func (data *GettingDataAcc) GetText() (string, errs.Err) {
conn, err := sabi.GetDataConn[*FooDataConn](data, "foo")
if err != nil { return "", err }
return "output text", errs.Ok()
}
type SettingDataAcc struct { sabi.DataAcc }
func (data *SettingDataAcc) SetText(text string) errs.Err {
conn, err := sabi.GetDataConn[*BarDataConn](data, "bar")
if err != nil { return err }
return errs.Ok()
}
The DataHub
is the central component that manages all DataSrc
and DataConn
,
providing access to them for your application logic.
By implementing the data interface (MyData
) from step 2 and the DataAcc
structs
from step 3 on DataHub
, you integrate them.
type MyDataHub struct {
sabi.DataHub
*GettingDataAcc
*SettingDataAcc
}
func NewMyDataHub() sabi.DataHub {
hub := sabi.NewDataHub()
return MyDataHub {
DataHub: hub,
GettingDataAcc: &GettingDataAcc{DataAcc: hub},
SettingDataAcc: &SettingDataAcc{DataAcc: hub},
}
}
Inside your init
function, register your global DataSrc
.
Next, main
function calls run
function, and inside run
function, setup the sabi
framework.
Then, create an instance of DataHub
and register the necessary local DataSrc
using
the Uses
method.
Finally, use the txn
method of DataHub
to execute your defined application logic
function (MyLogic
) within a transaction.
This automatically handles transaction commits and rollbacks.
func init() {
// Register global DataSrc.
sabi.Uses("foo", &FooDataSrc{})
}
func main() {
if run().IsNotOk() {
os.Exit(1)
}
}
func run() errs.Err {
// Set up the sabi framework.
if err := sabi.Setup(); err != nil { return err }
defer sabi.Shutdown()
// Creates a new instance of DataHub.
data := sabi.NewMyDataHub()
// Register session-local DataSrc with DataHub.
data.Uses("bar", &BarDataSrc{})
// Execute application logic within a transaction.
// MyLogic performs data operations via DataHub.
return sabi.Txn(data, MyLogic)
}
This framework supports Go 1.21 or later.
% gvm-fav
Now using version go1.21.13
go version go1.21.13 darwin/amd64
ok github.com/sttk/sabi 8.749s coverage: 96.8% of statements
Now using version go1.22.12
go version go1.22.12 darwin/amd64
ok github.com/sttk/sabi 8.747s coverage: 96.8% of statements
Now using version go1.23.10
go version go1.23.10 darwin/amd64
ok github.com/sttk/sabi 8.737s coverage: 96.8% of statements
Now using version go1.24.4
go version go1.24.4 darwin/amd64
ok github.com/sttk/sabi 8.753s coverage: 96.8% of statements
Back to go1.24.4
Now using version go1.24.4
Copyright (C) 2022-2025 Takayuki Sato
This program is free software under MIT License.
See the file LICENSE in this distribution for more details.