How to do transaction Management - sql

I have found a lot of golang tutorials on how to handle transactions, clean architecture, but none has satisfied my needs.
I have a go web application with a series of rest API each of one served by a three layers structure:
API
Service
Dao
DB
This is the main :
func main() {
DB = db.New()
dao = dao.New(DB)
service := service.New(dao)
server := &http.Server(....)
server.ListenAndServe()
}
Now, what I would realize is a transaction manager in the service layer.
Something like this:
type Service interface {
TransactionalExecute(fn func() error)
}
type ServiceImpl struct {
dao DaoImpl
}
func (service *ServiceImpl) TransactionalExecute(fn func() error) {
service.dao.TxBegin()
err := fn()
service.dao.TxEnd(err)
}
Dao should be like this:
type Dao interface {
TxBegin()
TxEnd(err error)
}
type DaoImpl struct {
DB db
tx *sql.Tx
}
func (dao *DaoImpl) TxBegin() {
dao.tx = dao.db.Begin()
}
func (dao *DaoImpl) TxEnd(err error) {
if p:= recover(); p != nil {
dao.tx.Rollback()
panic(p)
} else if err != nil {
dao.tx.Rollback()
} else {
dao.tx.Commit()
}
}
This kind of POC has two problems :
fn func() error parameter passed in the transactionalExecute() method must use the dao.Tx variable of the dao instance
This approach is not thread-safe : I am currently using gorilla mux and each http request will start a goroutine using a single instance of service, dao and DB. These instances are shared between multiple threads and is not safe. Anyway I was thinking about the use of mutex to access to the dao.tx variable but I am concerned about performances.
Any suggestion? Or different approaches to the problem?

Transactions should not really be part of DAO layer, it's a completely separate concern. A transaction is simply a single connection that executes all statements atomically. DAO, on the other hand, is a collection of database functions which use connections/transactions.
You can implement transactions and DAO nicely as two separate interfaces. Transaction can be an interface that is implemented both by sql.DB (connection pool from the standard library) and sql.Tx (transaction).
// Transaction represents both database connection pool and a single transaction/connection
type Transaction interface {
// ExecContext from database/sql library
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
// PrepareContext from database/sql library
PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
// QueryContext from database/sql library
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
// QueryRowContext from database/sql library
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
// QueryxContext from sqlx library
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
// QueryRowxContext from sqlx library
QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row
// PreparexContext from sqlx library
PreparexContext(ctx context.Context, query string) (*sqlx.Stmt, error)
// GetContext from sqlx library
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
// SelectContext from sqlx library
SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}
We use both database/sql and sqlx extended library for easier data scanning, you can just drop sqlx functions if you do not need them. Then a helper function for easier work:
// A TxFn is a function that will be called with an initialized `Transaction` object
// that can be used for executing statements and queries against a database.
type TxFn func(Transaction) error
// WithTransaction creates a new transaction and handles rollback/commit based on the
// error object returned by the `TxFn` or when it panics.
func WithTransaction(ctx context.Context, db *sqlx.DB, fn TxFn) error {
logger := ctxval.GetLogger(ctx)
tx, err := db.BeginTxx(ctx, &sql.TxOptions{
Isolation: sql.LevelDefault,
ReadOnly: false,
})
if err != nil {
logger.Error().Err(err).Msg("Cannot begin database transaction")
return fmt.Errorf("cannot begin transaction: %w", err)
}
defer func() {
if p := recover(); p != nil {
logger.Trace().Msg("Rolling database transaction back")
err := tx.Rollback()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
panic(p)
} else if err != nil {
logger.Trace().Msg("Rolling database transaction back")
err := tx.Rollback()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
} else {
logger.Trace().Msg("Committing database transaction")
err = tx.Commit()
if err != nil {
logger.Error().Err(err).Msg("Cannot rollback database transaction")
return
}
}
}()
logger.Trace().Msg("Starting database transaction")
err = fn(tx)
return err
}
Then you need a DAO interface, we have an initializer as a function that is then initialized by individual implementations, it takes context and transaction and returns DAO implementation:
var GetAccountDao func(ctx context.Context, tx Transaction) (AccountDao, error)
type AccountDao interface {
GetById(ctx context.Context, id uint64) (*models.Account, error)
GetByAccountNumber(ctx context.Context, number string) (*models.Account, error)
GetByOrgId(ctx context.Context, orgId string) (*models.Account, error)
List(ctx context.Context, limit, offset uint64) ([]*models.Account, error)
}
Implementation looks something like this:
const (
getAccountById = `SELECT * FROM accounts WHERE id = $1 LIMIT 1`
)
type accountDaoSqlx struct {
name string
getById *sqlx.Stmt
}
func getAccountDao(ctx context.Context, tx dao.Transaction) (dao.AccountDao, error) {
var err error
daoImpl := accountDaoSqlx{}
daoImpl.name = "account"
daoImpl.getById, err = tx.PreparexContext(ctx, getAccountById)
if err != nil {
return nil, NewPrepareStatementError(ctx, &daoImpl, getAccountById, err)
}
return &daoImpl, nil
}
func init() {
dao.GetAccountDao = getAccountDao
}
func (dao *accountDaoSqlx) GetById(ctx context.Context, id uint64) (*models.Account, error) {
query := getAccountById
stmt := dao.getById
result := &models.Account{}
err := stmt.GetContext(ctx, result, id)
if err != nil {
return nil, NewGetError(ctx, dao, query, err)
}
return result, nil
}
// etc
And example use in a "service" package (without and with transaction):
// import sqlx implementation which calls init()
_ "internal/dao/sqlx"
"internal/dao"
// etc
func CreatePubkey(w http.ResponseWriter, r *http.Request) {
payload := &payloads.PubkeyRequest{}
if err := render.Bind(r, payload); err != nil {
renderError(w, r, payloads.NewInvalidRequestError(r.Context(), err))
return
}
// example with transaction
err := dao.WithTransaction(r.Context(), db.DB, func(tx dao.Transaction) error {
pkDao, err := dao.GetPubkeyDao(r.Context(), tx)
if err != nil {
return payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err)
}
err = pkDao.Create(r.Context(), payload.Pubkey)
if err != nil {
return payloads.NewDAOError(r.Context(), "create pubkey", err)
}
// ... cut ...
return nil
})
// ignore errors etc
}
func ListPubkeys(w http.ResponseWriter, r *http.Request) {
// example without transaction
pubkeyDao, err := dao.GetPubkeyDao(r.Context(), db.DB)
if err != nil {
renderError(w, r, payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err))
return
}
pubkeys, err := pubkeyDao.List(r.Context(), 100, 0)
if err != nil {
renderError(w, r, payloads.NewDAOError(r.Context(), "list pubkeys", err))
return
}
if err := render.RenderList(w, r, payloads.NewPubkeyListResponse(pubkeys)); err != nil {
renderError(w, r, payloads.NewRenderError(r.Context(), "list pubkeys", err))
return
}
}
This is incomplete code I did cut details. Note I have both these interfaces in our dao package, these might be either separate or the same package.
One important thing to know: you must not share DAO implementation just like that (as in your snippet). The standard library sql.DB interface is actually a connection pool and it is thread safe (as documented), you can call as many Query/Exec/Etc function as you want, the driver will use one or more connection as needed.
Therefore the correct approach is to create new DAO type everytime a new request comes in, the connection pool will automatically acquire existing or new connection. More about this in the Go documentation.

you can modify the package dao to look like the following, which will make it thread-safe as func NewDao will be called in handler. you make this work you need to pass db *sql.DB from package to handler.
type Dao interface {
TxEnd(err error)
}
type DaoImpl struct {
tx *sql.Tx
db *sql.DB
txStared bool
}
func NewDao(db *sql.DB, txStared bool) Dao {
if txStared {
tx, _ := db.Begin()
return &DaoImpl{tx: tx, db: db, txStared: txStared}
}
return &DaoImpl{tx: nil, db: db, txStared: txStared}
}
func (dao *DaoImpl) TxEnd(err error) {
if !dao.txStared {
return
}
if p:= recover(); p != nil {
dao.tx.Rollback()
panic(p)
} else if err != nil {
dao.tx.Rollback()
} else {
dao.tx.Commit()
}
}
}

Related

Golang - Sending API POST Request - Not enough arguments error

The following code attempts to send a POST API request with a payload that is in RequestDetails.FormData. When I run main.go function, then I get the following errors.
go run main.go
# command-line-arguments
./main.go:53:17: not enough arguments in call to http.HandleFunc
./main.go:53:33: not enough arguments in call to reqDetails.Send
have ()
want (http.ResponseWriter, *http.Request)
./main.go:53:33: reqDetails.Send() used as value
The code is available below. Anybody knows what I could do wrong here? Thanks a lot for your help.
//main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
)
// RequestDetails contains input data for Send
type RequestDetails struct {
EndPoint string
FormType string
FormData map[string]string
}
// Send sends API POST request to an endpoint
func (rd RequestDetails) Send(w http.ResponseWriter, r *http.Request) {
json_data, err := json.Marshal(rd.FormData)
if err != nil {
log.Fatal(err)
}
resp, err := http.Post(rd.EndPoint, rd.FormType, bytes.NewBuffer(json_data))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp)
}
func main() {
m := map[string]string{
"AuthParamOne": "AP0000001",
"AuthParamTwo": "AP0000002",
"AuthParamThree": "AP0000003",
}
reqDetails := RequestDetails{
EndPoint: "https://httpbin.org/post",
FormType: "application/json",
FormData: m,
}
http.HandleFunc(reqDetails.Send())
}
you have to use HandleFunc in following below:
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
for code above follow this:
http.HandleFunc("/test",reqDetails.Send) //-> add reference instead of calling 'reqDetails.Send()'
reference: https://pkg.go.dev/net/http#HandleFunc
please vote up :)
In your Send method, you don't make use of w http.ResponseWriter, r *http.Request, So it seems you don't need them:
func (rd RequestDetails) Send() {...
Also in your last line, HandleFunc requires different arguments which once again is not necessary in your case. Just try to run the Send method:
reqDetails.Send()
The whole main.go file:
//main.go
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
)
// RequestDetails contains input data for Send
type RequestDetails struct {
EndPoint string
FormType string
FormData map[string]string
}
// Send sends API POST request to an endpoint
func (rd RequestDetails) Send() {
json_data, err := json.Marshal(rd.FormData)
if err != nil {
log.Fatal(err)
}
resp, err := http.Post(rd.EndPoint, rd.FormType, bytes.NewBuffer(json_data))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp)
}
func main() {
m := map[string]string{
"AuthParamOne": "AP0000001",
"AuthParamTwo": "AP0000002",
"AuthParamThree": "AP0000003",
}
reqDetails := RequestDetails{
EndPoint: "https://httpbin.org/post",
FormType: "application/json",
FormData: m,
}
reqDetails.Send()
}
if your code like this
watcher := bufio.NewReader(os.Stdin)
input, _ := watcher.ReadString()
fmt.Println(input)
you needed this for reading line line
old -> input, _ := watcher.ReadString()
new -> input, _ := watcher.ReadString('\n')

Golang struct having io.Reader unable to serialize

I am trying to serialize below struct into byte[] to store it into DB and then while reading it from DB I am deserializing it.
type Response struct {
Headers map[string][]string
Body io.Reader
Status int
}
Below is the code how I am creating response object and setting up the value for it.
resp := new(Response)
resp.Body = bytes.NewReader(outBytes) //outBytes is byte[]
resp.Headers.SetKeyValue("Content-Type", "text/json") //SetKeyValue is the method created for adding headers
resp.Headers.SetKeyValue("Url-Type", "broker")
resp.Status = 200
I am using json.Marshal() to serialize the resp object as below.
b, _ := json.Marshal(resp)
Below is the code, I am using to deserialize.
var r Response
r.Body = &bytes.Buffer{}
json.Unmarshal(b,&r)
Problem is with deserialization, I am not able to get the resp.Body object. It is always nil or blank in spite of setting body object (see above). I am able to get Headers and Status field of the struct back from deserialize but not Body.
I know there is something to be handle with Body field which is an io.Reader.
Any help would be really great.
Short Answer : JSON marshaller will not use Read() function to read the string from io.Reader . Instead of using io.Reader you may use a type that implements Marshaler interface.
How Marshaller works :
Marshal traverses the value v recursively. If an encountered value implements the Marshaler interface and is not a nil pointer, Marshal calls its MarshalJSON method to produce JSON. If no MarshalJSON method is present but the value implements encoding.TextMarshaler instead, Marshal calls its MarshalText method. The nil pointer exception is not strictly necessary but mimics a similar, necessary exception in the behavior of UnmarshalJSON.
Otherwise, Marshal uses the following type-dependent default encodings:
Boolean values encode as JSON booleans.
Floating point, integer, and Number values encode as JSON numbers.
Implementaton
This is what you may do
type Response struct {
Headers map[string][]string
Body *JSONReader
Status int
}
type JSONReader struct {
*bytes.Reader
}
func NewJSONReader(outBytes []byte) *JSONReader {
jr := new(JSONReader)
jr.Reader = bytes.NewReader(outBytes)
return jr
}
func (js JSONReader) MarshalJSON() ([]byte, error) {
data, err := ioutil.ReadAll(js.Reader)
if err != nil {
return nil, err
}
data = []byte(`"` + string(data) + `"`)
return data, nil
}
// UnmarshalJSON sets *jr to a copy of data.
func (jr *JSONReader) UnmarshalJSON(data []byte) error {
if jr == nil {
return errors.New("json.JSONReader: UnmarshalJSON on nil pointer")
}
if data == nil {
return nil
}
data = []byte(strings.Trim(string(data), "\""))
jr.Reader = bytes.NewReader(data)
return nil
}
Here is a go playground link with the implementation and sample use : link
Overview
io.Reader is an interface so it can't be marshaled. Each marshaling struct attribute must implement Marshaler interface to be marshaled. You could declare your own marshaler wrapper struct to marshal data from bytes.Reader.
Why interface can't be marshaled?
Interfaces in Go provide a way to specify the behavior of an object: if something can do this, then it can be used here. In opposite Go’s structs are typed collections of fields. They’re useful for grouping data together to form records. Go supports methods defined on struct types not interface types.
Implementation
type Response struct {
Body *MarshalableReader
}
type MarshalableReader struct {
*bytes.Reader
}
func (r MarshalableReader) MarshalJSON() ([]byte, error) {
data, err := ioutil.ReadAll(r.Reader)
if err != nil {
return nil, err
}
return []byte(fmt.Sprintf("\"%s\"", data)), nil
}
func main() {
resp := Response{&MarshalableReader{bytes.NewReader([]byte("Blah Blah"))}}
marshaled, err := json.Marshal(resp)
if err != nil {
log.Fatal(err)
}
fmt.Printf("JSON: %s\n", marshaled)
}
I am using "encoding/json" package : https://golang.org/pkg/encoding/json/
As I can use the http.ResponseWriter to send JSON Response. Here is two functions you could use to send JSON and read JSON from the body :
// GetJSONContent returns the JSON content of a request
func GetJSONContent(v interface{}, r *http.Request) error {
defer r.Body.Close()
return json.NewDecoder(r.Body).Decode(v)
}
// JSONWithHTTPCode Json Output with an HTTP code
func JSONWithHTTPCode(w http.ResponseWriter, d interface{}, code int) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
w.WriteHeader(code)
if d != nil {
err := json.NewEncoder(w).Encode(d)
if err != nil {
panic(err)
}
}
}
And then in your handlers just use those func as follow :
// Handler handler
func Handler(w http.ResponseWriter, r *http.Request) {
s := YourStruct{}
err = GetJSONContent(s, r)
if err != nil {
panic(err)
}
return
}
JSONWithHTTPCode(w, s, http.StatusOK)
}
Hope it helped

Golang imap.DialTLS Config example

I used to be able to connect to port 143 of a mail server like this:
c, err := imap.Dial(mailServer)
The code above connects to port 143 of the mailServer. Now I have a new mail server only accepts port 993. Looking at the Golang imap source code, the function DialTLS will connect to port 993. The signature of DialTLS looks like this:
func DialTLS(addr string, config *tls.Config) (c *Client, err error)
Now I don't know how to construct the *tls.Config. I Googled around, but didn't not find anything really useful. Can someone show me some example how to construct the *tls.Config?
I tried to pass in nil as the second parameter, it compiles, and I didn't get any runtime error. But it seems no new mails were fetched, even I believe there should be.
My fetch mail code looks like this:
// testimap
package main
import (
"bytes"
"code.google.com/p/go-imap/go1/imap"
"fmt"
"net/mail"
"time"
)
type Mail struct {
Subject string
Body string
From string
Uid uint32
}
func FetchMail(lastUid uint32) []*Mail {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
//
// Note: most of error handling code is omitted for brevity
//
var (
c *imap.Client
cmd *imap.Command
rsp *imap.Response
)
// Connect to the server
c, err := imap.DialTLS(mailServer, nil)
if err != nil {
fmt.Println(err)
}
// Remember to log out and close the connection when finished
defer c.Logout(30 * time.Second)
// Print server greeting (first response in the unilateral server data queue)
//fmt.Println("Server says hello:", c.Data[0].Info)
c.Data = nil
// Enable encryption, if supported by the server
if c.Caps["STARTTLS"] {
c.StartTLS(nil)
}
// Authenticate
if c.State() == imap.Login {
c.Login(mailSupportUser, mailSupportPw)
}
//// List all top-level mailboxes, wait for the command to finish
cmd, err = imap.Wait(c.List("", "%"))
if err != nil {
fmt.Println(err)
}
// Print mailbox information
//fmt.Println("\nTop-level mailboxes:")
//for _, rsp = range cmd.Data {
// fmt.Println("|--", rsp.MailboxInfo())
//}
// Check for new unilateral server data responses
//for _, rsp = range c.Data {
// fmt.Println("Server data:", rsp)
//}
c.Data = nil
// Open a mailbox (synchronous command - no need for imap.Wait)
c.Select("INBOX", true)
//fmt.Print("\nMailbox status:\n", c.Mailbox)
// Fetch the headers of the 10 most recent messages
set, err := imap.NewSeqSet(fmt.Sprint(lastUid, ":*"))
if err != nil {
fmt.Println(err)
}
//if c.Mailbox.Messages >= 10 {
// set.AddRange(c.Mailbox.Messages-9, c.Mailbox.Messages)
//} else {
// set.Add("1:*")
//}
cmd, err = c.UIDFetch(set, "RFC822.HEADER", "RFC822.TEXT")
if err != nil {
fmt.Println(err)
}
// Process responses while the command is running
//fmt.Println("\nMost recent messages:")
mails := make([]*Mail, 0, 10)
for cmd.InProgress() {
// Wait for the next response (no timeout)
c.Recv(-1)
// Process command data
for _, rsp = range cmd.Data {
if err != nil {
fmt.Println(err)
}
header := imap.AsBytes(rsp.MessageInfo().Attrs["RFC822.HEADER"])
uid := imap.AsNumber((rsp.MessageInfo().Attrs["UID"]))
body := imap.AsBytes(rsp.MessageInfo().Attrs["RFC822.TEXT"])
if msg, err := mail.ReadMessage(bytes.NewReader(header)); msg != nil {
if err != nil {
fmt.Println(err)
}
mail := &Mail{
Subject: msg.Header.Get("Subject"),
From: msg.Header.Get("FROM"),
Body: string(body),
Uid: uid,
}
if mail.Uid < lastUid {
continue
}
mails = append(mails, mail)
}
}
cmd.Data = nil
c.Data = nil
}
// Check command completion status
if rsp, err := cmd.Result(imap.OK); err != nil {
if err == imap.ErrAborted {
fmt.Println("Fetch command aborted")
} else {
fmt.Println("Fetch error:", rsp.Info)
}
}
fmt.Println(mails)
return mails
}
First off, you should be using the project's GitHub repo as the Google Code project stated development was moving to GitHub due to Google Code shutting down. It's a few commits ahead of the Google Code repo too, so you won't be getting any updates if you don't migrate to the GitHub repo.
Secondly, looking at the package's demo, passing nil to DialTLS as the TLS client seems to be the proper way to connect with a default TLS client.
From the information you've given, it seems like it may be an issue with your server accepting connections over that port. I would look into if the port is open to the client you're trying to connect from or if your IMAP server is even accepting TLS connections.
If you're absolutely sure it's not a server issue after further debugging, I would file an issue on the project's GitHub issue tracker to get help from people who are more familiar with the package, seeing that it's a third party package.

SSH Connection Timeout

I am trying to make SSH connections using golang.org/x/crypto/ssh and I am kinda surprised that I can't seem to find out how to timeout the NewSession function (I actually don't seen any way to timeout anything). When I try to connect to a server that is having issues, this just hangs for a very long time. I have written something to use select with a time.After but it just feels like a hack. Something I haven't tried yet is to keep the underlying net.Conn in my struct and just keep doing Conn.SetDeadline() calls. Haven't tried this yet because I don't know if the crypto/ssh library overrides this or anything like that.
Anyone have a good way to timeout dead servers with this library? Or does anyone know of a better library?
One way to handle this transparently with the ssh package, is to create a connection with an idle timeout via a custom net.Conn which sets deadlines for you. However, this will cause the background Reads on a connection to timeout, so we need to use ssh keepalives to keep the connection open. Depending on your use case, simply using ssh keepalives as an alert for a dead connection may suffice.
// Conn wraps a net.Conn, and sets a deadline for every read
// and write operation.
type Conn struct {
net.Conn
ReadTimeout time.Duration
WriteTimeout time.Duration
}
func (c *Conn) Read(b []byte) (int, error) {
err := c.Conn.SetReadDeadline(time.Now().Add(c.ReadTimeout))
if err != nil {
return 0, err
}
return c.Conn.Read(b)
}
func (c *Conn) Write(b []byte) (int, error) {
err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout))
if err != nil {
return 0, err
}
return c.Conn.Write(b)
}
You can then use net.DialTimeout or a net.Dialer to get the connection, wrap it in your Conn with timeouts, and pass it into ssh.NewClientConn.
func SSHDialTimeout(network, addr string, config *ssh.ClientConfig, timeout time.Duration) (*ssh.Client, error) {
conn, err := net.DialTimeout(network, addr, timeout)
if err != nil {
return nil, err
}
timeoutConn := &Conn{conn, timeout, timeout}
c, chans, reqs, err := ssh.NewClientConn(timeoutConn, addr, config)
if err != nil {
return nil, err
}
client := ssh.NewClient(c, chans, reqs)
// this sends keepalive packets every 2 seconds
// there's no useful response from these, so we can just abort if there's an error
go func() {
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for range t.C {
_, _, err := client.Conn.SendRequest("keepalive#golang.org", true, nil)
if err != nil {
return
}
}
}()
return client, nil
}
Set the timeout on the ssh.ClientConfig.
cfg := ssh.ClientConfig{
User: "root",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
HostKeyCallback: ssh.FixedHostKey(hostKey),
Timeout: 15 * time.Second, // max time to establish connection
}
ssh.Dial("tcp", ip+":22", &cfg)
When you call ssh.Dial, the timeout will be passed to net.DialTimeout.

Idiomatic Go for abstracting resource allocation/deallocation

Is there an idiomatic Go way for abstracting resource allocation/deallocation? My initial guess is to abstract the allocation/deallocation in a higher-order function:
func withResource(f func(Resource)error) error {
// allocate resource
// defer free resource
return f(resource)
}
However, this line of thinking is borrowed directly from the functional paradigm and doesn't seem to align well with Go's largely imperative nature.
As a concrete example, running a daemon for the duration of a block of code is a recurring theme in my current project, so I've created a withDaemon function to abstract the commonality:
func withDaemon(
cmd *exec.Cmd,
f func(io.ReadCloser, io.ReadCloser, io.WriteCloser) error,
) error {
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("couldn't get stdout: %v", err)
}
stderr, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("couldn't get stderr: %v", err)
}
stdin, err := cmd.StdinPipe()
if err != nil {
return fmt.Errorf("couldn't get stdin: %v", err)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start: %v", err)
}
defer func() {
cmd.Process.Kill()
cmd.Wait()
}
return f(stdout, stderr, stdin)
}
I think that the idiomatic way would be to create a Daemon type, and to just use defer in the caller:
d := NewDaemon(...)
defer d.Stop()
doWhatever()