scope

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 18, 2025 License: Apache-2.0 Imports: 5 Imported by: 0

README

scope

scope is a Go module that provides iterators that can be used to automatically close, unlock, or cancel resources like files when they are no longer needed.

Go Reference Go Report Card

The iterator returned by this package will execute the loop body exactly once.

The defer idiom is commonly used in Go to allocate and release resources.

Deferred actions are executed when the function returns, so the release of resources is delayed until the function returns.

Sometimes that's what you want, and sometimes it's not.

When resources need to be released immediately, function literals are often used to specify the scope.

func() {
    file, err := os.Open(name)
    if err != nil {
      return
    }
    defer file.Close()
    _ = file // use file here
}()

This package provides another solution for such situations. The following code snippet achieves the same result as the code above.

import "github.com/goaux/scope"

for file, err := range scope.Use2(os.Open(name)) {
    // If err==nil, the file will be closed at the end of the loop body regardless of break.
    if err != nil {
        break
    }
    _ = file // use file here
}
Example usage:
import "github.com/goaux/scope"

for file, err := range scope.Use2(os.Create("test.gz")) {
    // If err==nil, the file will be closed at the end of the loop body regardless of break.
    if err != nil {
        fmt.Println(err)
        break
    }
    for gz := range scope.Use(gzip.NewWriter(file)) {
        // gz will always be closed at the end of the loop body.
        if _, err := fmt.Fprintln(gz, "hello world"); err != nil {
            fmt.Println(err)
            break
        }
    }
}

Documentation

Overview

Package scope provides iterators that automatically closing, unlocking or canceling resources such as files when they are no longer needed.

The iterator returned by this package will execute the loop body exactly once.

The defer idiom is commonly used in Go to allocate and release resources.

file, err := os.Open(name)
if err != nil {
	panic(err)
}
defer file.Close()
_ = file // use file here

Deferred actions are executed when the function returns, so the release of resources is delayed until the function returns.

Sometimes that's what you want, and sometimes it's not.

If you need to release resources immediately, you can use a function literal to specify the scope:

func() {
	file, err := os.Open(name)
	if err != nil {
		return
	}
	defer file.Close()
	_ = file // use file here
}()

This package provides another solution for such situations. The following code snippet achieves the same result as the code above.

for file, err := range scope.Use2(os.Open(name)) {
	if err != nil {
		break
	}
	_ = file // use file here
}

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Context

func Context(
	ctx context.Context,
	cancel context.CancelFunc,
) iter.Seq[context.Context]

Context returns an iterator that yields ctx as is. The loop body will be executed exactly once. The cancel will be called after the loop body executed.

Example
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/goaux/scope"
)

func main() {
	var g sync.WaitGroup
	for ctx := range scope.Context(context.WithTimeout(context.TODO(), 100*time.Millisecond)) {
		fmt.Println("pass 0")
		g.Add(1)
		go func() {
			defer g.Done()
			<-ctx.Done()
			fmt.Println("pass 1", ctx.Err())
		}()
		// ctx will be canceled at the end of the loop body.
	}
	g.Wait()
	fmt.Println("ok")
}
Output:

pass 0
pass 1 context canceled
ok

func Lock

func Lock[Mu sync.Locker](mu Mu) iter.Seq[Mu]

Lock returns an iterator that yields a locked mu. The loop body executes exactly once. mu.Lock is called before the loop body executes. mu.Unlock is called after the loop body executes.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/goaux/scope"
)

func main() {
	var mu sync.Mutex
	for range scope.Lock(&mu) {
		// mu is locked.
		fmt.Println("pass 0")
	}
	fmt.Println("pass 1")
}
Output:

pass 0
pass 1
Example (Inspect)
package main

import (
	"fmt"
	"sync"

	"github.com/goaux/scope"
)

func main() {
	var mu TestLocker
	for range scope.Lock(&mu) {
		// mu is locked for writing.
		fmt.Println("pass 0")
		// mu will be unlocked at the end of the loop body.
	}
	fmt.Println("pass 1")
	for range scope.Lock(mu.RLocker()) {
		// mu is locked for reading.
		fmt.Println("pass 2")
		// mu will be unlocked at the end of the loop body.
	}
	fmt.Println("pass 3")
}

type TestLocker struct{}

func (*TestLocker) Lock()    { fmt.Println("Lock") }
func (*TestLocker) Unlock()  { fmt.Println("Unlock") }
func (*TestLocker) RLock()   { fmt.Println("RLock") }
func (*TestLocker) RUnlock() { fmt.Println("RUnlock") }

func (*TestLocker) RLocker() sync.Locker { return (*rlocker)(nil) }

type rlocker TestLocker

func (r *rlocker) Lock()   { (*TestLocker)(r).RLock() }
func (r *rlocker) Unlock() { (*TestLocker)(r).RUnlock() }
Output:

Lock
pass 0
Unlock
pass 1
RLock
pass 2
RUnlock
pass 3
Example (RWMutex)
package main

import (
	"fmt"
	"sync"

	"github.com/goaux/scope"
)

func main() {
	var mu sync.RWMutex
	for range scope.Lock(&mu) {
		// mu is locked for writing.
		fmt.Println("pass 0")
		// mu will be unlocked at the end of the loop body.
	}
	fmt.Println("pass 1")
	for range scope.Lock(mu.RLocker()) {
		// mu is locked for reading.
		fmt.Println("pass 2")
	}
	fmt.Println("pass 3")
}
Output:

pass 0
pass 1
pass 2
pass 3

func Tx

func Tx[Tx Rollbacker](tx Tx) iter.Seq[Tx]

Context returns an iterator that yields tx as is. The loop body will be executed exactly once. tx.Rollback will be called after the loop body executed.

Example
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"fmt"

	"github.com/goaux/scope"
)

func main() {
	for db, err := range scope.Use2(sql.Open("sql_test", "ExampleOpenDB")) {
		if err != nil {
			fmt.Println(err)
			break
		}
		tx, err := db.Begin()
		if err != nil {
			fmt.Println(err)
			break
		}
		for tx := range scope.Tx(tx) {
			// do something.
			_ = tx
			// Not committing implies a rollback
		}
		fmt.Println("end#1")
	}
	fmt.Println("end#2")
}

func init() {
	sql.Register("sql_test", Driver{})
}

type Driver struct{}

func (Driver) Open(name string) (driver.Conn, error) {
	fmt.Printf("Driver.Open(%q)\n", name)
	return Conn{}, nil
}

func (Driver) OpenConnector(name string) (driver.Connector, error) {
	fmt.Printf("Driver.OpenConnector(%q)\n", name)
	return Connector{}, nil
}

type Connector struct{}

func (Connector) Connect(context.Context) (driver.Conn, error) {
	fmt.Println("Connector.Connect(ctx)")
	return Conn{}, nil
}

func (Connector) Driver() driver.Driver {
	return Driver{}
}

type Conn struct{}

func (Conn) Prepare(query string) (driver.Stmt, error) {
	fmt.Printf("Conn.Prepare(%q)\n", query)
	return Stmt{}, nil
}

func (Conn) Close() error {
	fmt.Println("Conn.Close()")
	return nil
}

func (Conn) Begin() (driver.Tx, error) {
	fmt.Println("Conn.Begin()")
	return Tx{}, nil
}

func (Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
	p, _ := json.Marshal(opts)
	fmt.Printf("Conn.BeginTx(ctx, %s)\n", string(p))
	return Tx{}, nil
}

type Stmt struct{}

func (Stmt) Close() error {
	fmt.Println("Stmt.Close()")
	return nil
}

func (Stmt) NumInput() int {
	fmt.Println("Stmt.NumInput()")
	return 0
}

func (Stmt) Exec(args []driver.Value) (driver.Result, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Exec(%s)", string(p))
	return Result{}, nil
}

func (Stmt) Query(args []driver.Value) (driver.Rows, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Query(%s)", string(p))
	return Rows{}, nil
}

type Tx struct{}

func (Tx) Commit() error {
	fmt.Println("Tx.Commit()")
	return nil
}

func (Tx) Rollback() error {
	fmt.Println("Tx.Rollback()")
	return nil
}

type Result struct{}

func (Result) LastInsertId() (int64, error) {
	fmt.Println("Result.LastInsertId()")
	return 0, nil
}

func (Result) RowsAffected() (int64, error) {
	fmt.Println("Result.RowsAffected()")
	return 0, nil
}

type Rows struct{}

func (Rows) Columns() []string {
	fmt.Println("Rows.Columns()")
	return nil
}

// Close closes the rows iterator.
func (Rows) Close() error {
	fmt.Println("Rows.Close()")
	return nil
}

func (Rows) Next(dest []driver.Value) error {
	p, _ := json.Marshal(dest)
	fmt.Printf("Rows.Next(%s)\n", string(p))
	return nil
}
Output:

Driver.OpenConnector("ExampleOpenDB")
Connector.Connect(ctx)
Conn.BeginTx(ctx, {"Isolation":0,"ReadOnly":false})
Tx.Rollback()
end#1
Conn.Close()
end#2

func Tx2

func Tx2[Tx Rollbacker](tx Tx, err error) iter.Seq2[Tx, error]

Context returns an iterator that yields tx and err as is. The loop body will be executed exactly once. If err is nil, tx.Rollback will be called after the loop body executed.

Example (Begin)
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"fmt"

	"github.com/goaux/scope"
)

func main() {
	for db, err := range scope.Use2(sql.Open("sql_test", "ExampleOpenDB")) {
		if err != nil {
			fmt.Println(err)
			break
		}
		for tx, err := range scope.Tx2(db.Begin()) {
			if err != nil {
				fmt.Println(err)
				break
			}

			// do something.
			_ = tx

			// Under normal circumstances, Commit is called last.
			if err := tx.Commit(); err != nil {
				fmt.Println(err)
				break
			}
			fmt.Println("end#1")
		}

		for tx, err := range scope.Tx2(db.Begin()) {
			if err != nil {
				fmt.Println(err)
				break
			}

			// do something.
			_ = tx

			// Not committing implies a rollback
			fmt.Println("end#2")
		}
		fmt.Println("end#3")
	}
	fmt.Println("end#4")
}

func init() {
	sql.Register("sql_test", Driver{})
}

type Driver struct{}

func (Driver) Open(name string) (driver.Conn, error) {
	fmt.Printf("Driver.Open(%q)\n", name)
	return Conn{}, nil
}

func (Driver) OpenConnector(name string) (driver.Connector, error) {
	fmt.Printf("Driver.OpenConnector(%q)\n", name)
	return Connector{}, nil
}

type Connector struct{}

func (Connector) Connect(context.Context) (driver.Conn, error) {
	fmt.Println("Connector.Connect(ctx)")
	return Conn{}, nil
}

func (Connector) Driver() driver.Driver {
	return Driver{}
}

type Conn struct{}

func (Conn) Prepare(query string) (driver.Stmt, error) {
	fmt.Printf("Conn.Prepare(%q)\n", query)
	return Stmt{}, nil
}

func (Conn) Close() error {
	fmt.Println("Conn.Close()")
	return nil
}

func (Conn) Begin() (driver.Tx, error) {
	fmt.Println("Conn.Begin()")
	return Tx{}, nil
}

func (Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
	p, _ := json.Marshal(opts)
	fmt.Printf("Conn.BeginTx(ctx, %s)\n", string(p))
	return Tx{}, nil
}

type Stmt struct{}

func (Stmt) Close() error {
	fmt.Println("Stmt.Close()")
	return nil
}

func (Stmt) NumInput() int {
	fmt.Println("Stmt.NumInput()")
	return 0
}

func (Stmt) Exec(args []driver.Value) (driver.Result, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Exec(%s)", string(p))
	return Result{}, nil
}

func (Stmt) Query(args []driver.Value) (driver.Rows, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Query(%s)", string(p))
	return Rows{}, nil
}

type Tx struct{}

func (Tx) Commit() error {
	fmt.Println("Tx.Commit()")
	return nil
}

func (Tx) Rollback() error {
	fmt.Println("Tx.Rollback()")
	return nil
}

type Result struct{}

func (Result) LastInsertId() (int64, error) {
	fmt.Println("Result.LastInsertId()")
	return 0, nil
}

func (Result) RowsAffected() (int64, error) {
	fmt.Println("Result.RowsAffected()")
	return 0, nil
}

type Rows struct{}

func (Rows) Columns() []string {
	fmt.Println("Rows.Columns()")
	return nil
}

// Close closes the rows iterator.
func (Rows) Close() error {
	fmt.Println("Rows.Close()")
	return nil
}

func (Rows) Next(dest []driver.Value) error {
	p, _ := json.Marshal(dest)
	fmt.Printf("Rows.Next(%s)\n", string(p))
	return nil
}
Output:

Driver.OpenConnector("ExampleOpenDB")
Connector.Connect(ctx)
Conn.BeginTx(ctx, {"Isolation":0,"ReadOnly":false})
Tx.Commit()
end#1
Conn.BeginTx(ctx, {"Isolation":0,"ReadOnly":false})
end#2
Tx.Rollback()
end#3
Conn.Close()
end#4
Example (BeginTx)
package main

import (
	"context"
	"database/sql"
	"database/sql/driver"
	"encoding/json"
	"fmt"

	"github.com/goaux/scope"
)

func main() {
	for db, err := range scope.Use2(sql.Open("sql_test", "ExampleOpenDB")) {
		if err != nil {
			fmt.Println(err)
			break
		}
		ctx := context.TODO()
		for tx, err := range scope.Tx2(db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})) {
			if err != nil {
				fmt.Println(err)
				break
			}

			// do something.
			_ = tx

			if err := tx.Commit(); err != nil {
				fmt.Println(err)
				break
			}
			fmt.Println("end#1")
		}

		for tx, err := range scope.Tx2(db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})) {
			if err != nil {
				fmt.Println(err)
				break
			}

			// do something.
			_ = tx

			// Not committing implies a rollback
			fmt.Println("end#2")
		}
		fmt.Println("end#3")
	}
	fmt.Println("end#4")
}

func init() {
	sql.Register("sql_test", Driver{})
}

type Driver struct{}

func (Driver) Open(name string) (driver.Conn, error) {
	fmt.Printf("Driver.Open(%q)\n", name)
	return Conn{}, nil
}

func (Driver) OpenConnector(name string) (driver.Connector, error) {
	fmt.Printf("Driver.OpenConnector(%q)\n", name)
	return Connector{}, nil
}

type Connector struct{}

func (Connector) Connect(context.Context) (driver.Conn, error) {
	fmt.Println("Connector.Connect(ctx)")
	return Conn{}, nil
}

func (Connector) Driver() driver.Driver {
	return Driver{}
}

type Conn struct{}

func (Conn) Prepare(query string) (driver.Stmt, error) {
	fmt.Printf("Conn.Prepare(%q)\n", query)
	return Stmt{}, nil
}

func (Conn) Close() error {
	fmt.Println("Conn.Close()")
	return nil
}

func (Conn) Begin() (driver.Tx, error) {
	fmt.Println("Conn.Begin()")
	return Tx{}, nil
}

func (Conn) BeginTx(ctx context.Context, opts driver.TxOptions) (driver.Tx, error) {
	p, _ := json.Marshal(opts)
	fmt.Printf("Conn.BeginTx(ctx, %s)\n", string(p))
	return Tx{}, nil
}

type Stmt struct{}

func (Stmt) Close() error {
	fmt.Println("Stmt.Close()")
	return nil
}

func (Stmt) NumInput() int {
	fmt.Println("Stmt.NumInput()")
	return 0
}

func (Stmt) Exec(args []driver.Value) (driver.Result, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Exec(%s)", string(p))
	return Result{}, nil
}

func (Stmt) Query(args []driver.Value) (driver.Rows, error) {
	p, _ := json.Marshal(args)
	fmt.Printf("Stmt.Query(%s)", string(p))
	return Rows{}, nil
}

type Tx struct{}

func (Tx) Commit() error {
	fmt.Println("Tx.Commit()")
	return nil
}

func (Tx) Rollback() error {
	fmt.Println("Tx.Rollback()")
	return nil
}

type Result struct{}

func (Result) LastInsertId() (int64, error) {
	fmt.Println("Result.LastInsertId()")
	return 0, nil
}

func (Result) RowsAffected() (int64, error) {
	fmt.Println("Result.RowsAffected()")
	return 0, nil
}

type Rows struct{}

func (Rows) Columns() []string {
	fmt.Println("Rows.Columns()")
	return nil
}

// Close closes the rows iterator.
func (Rows) Close() error {
	fmt.Println("Rows.Close()")
	return nil
}

func (Rows) Next(dest []driver.Value) error {
	p, _ := json.Marshal(dest)
	fmt.Printf("Rows.Next(%s)\n", string(p))
	return nil
}
Output:

Driver.OpenConnector("ExampleOpenDB")
Connector.Connect(ctx)
Conn.BeginTx(ctx, {"Isolation":6,"ReadOnly":false})
Tx.Commit()
end#1
Conn.BeginTx(ctx, {"Isolation":6,"ReadOnly":false})
end#2
Tx.Rollback()
end#3
Conn.Close()
end#4

func Use

func Use[T io.Closer](resource T) iter.Seq[T]

Use returns an iterator that yields resource as is. The loop body will be executed exactly once. resource.Close will be called after the loop body executed.

Example
package main

import (
	"bytes"
	"compress/gzip"
	"encoding/hex"
	"fmt"

	"github.com/goaux/scope"
)

func main() {
	buf := &bytes.Buffer{}
	for gz := range scope.Use(gzip.NewWriter(buf)) {
		fmt.Fprintln(gz, "hello world")
	}
	fmt.Println(hex.EncodeToString(buf.Bytes()))
}
Output:

1f8b08000000000000ffca48cdc9c95728cf2fca49e102040000ffff2d3b08af0c000000

func Use2

func Use2[T io.Closer](resource T, err error) iter.Seq2[T, error]

Use2 returns an iterator that yields resource and err as is. The loop body will be executed exactly once. If err is nil, resource.Close will be called after the loop body executed.

Example
package main

import (
	"compress/gzip"
	"fmt"
	"io"
	"os"

	"github.com/goaux/scope"
)

func main() {
	defer os.Remove("test.gz")
	for file, err := range scope.Use2(os.Create("test.gz")) {
		if err != nil {
			fmt.Println(err)
			break
		}
		for gz := range scope.Use(gzip.NewWriter(file)) {
			if _, err := fmt.Fprintln(gz, "hello world"); err != nil {
				fmt.Println(err)
				break
			}
			// gz will be closed at the end of the loop body.
		}
		// file will be closed at the end of the loop body.
	}

	for file, err := range scope.Use2(os.Open("test.gz")) {
		if err != nil {
			fmt.Println(err)
			break
		}
		if gz, err := gzip.NewReader(file); err != nil {
			fmt.Println(err)
			break
		} else if b, err := io.ReadAll(gz); err != nil {
			fmt.Println(err)
		} else {
			fmt.Print(string(b))
		}
		// file will be closed at the end of the loop body.
	}
}
Output:

hello world

func WithCancel

func WithCancel(
	parent context.Context,
) iter.Seq[context.Context]

WithCancel returns Context(context.WithCancel(parent)).

See Context, context.WithCancel.

Example
package main

import (
	"context"
	"fmt"
	"sync"

	"github.com/goaux/scope"
)

func main() {
	var g sync.WaitGroup
	for ctx := range scope.WithCancel(context.TODO()) {
		fmt.Println("pass 0")
		g.Add(1)
		go func() {
			defer g.Done()
			<-ctx.Done()
			fmt.Println("pass 1")
		}()
		// ctx will be canceled at the end of the loop body.
	}
	g.Wait()
	fmt.Println("ok")
}
Output:

pass 0
pass 1
ok

func WithDeadline

func WithDeadline(
	parent context.Context,
	d time.Time,
) iter.Seq[context.Context]

WithDeadline returns Context(context.WithDeadline(parent, d)).

See Context, context.WithDeadline.

Example
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/goaux/scope"
)

func main() {
	var g sync.WaitGroup
	for ctx := range scope.WithDeadline(context.TODO(), time.Now().Add(100*time.Millisecond)) {
		fmt.Println("pass 0")
		g.Add(1)
		go func() {
			defer g.Done()
			<-ctx.Done()
			fmt.Println("pass 1", ctx.Err())
		}()
		// ctx will be canceled at the end of the loop body.
	}
	g.Wait()
	fmt.Println("ok")
}
Output:

pass 0
pass 1 context canceled
ok

func WithTimeout

func WithTimeout(
	parent context.Context,
	timeout time.Duration,
) iter.Seq[context.Context]

WithTimeout returns Context(context.WithTimeout(parent, timeout)).

See Context, context.WithTimeout.

Example
package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/goaux/scope"
)

func main() {
	var g sync.WaitGroup
	for ctx := range scope.WithTimeout(context.TODO(), 100*time.Millisecond) {
		fmt.Println("pass 0")
		g.Add(1)
		go func() {
			defer g.Done()
			<-ctx.Done()
			fmt.Println("pass 1", ctx.Err())
		}()
		// ctx will be canceled at the end of the loop body.
	}
	g.Wait()
	fmt.Println("ok")
}
Output:

pass 0
pass 1 context canceled
ok

func WithTimeoutCause

func WithTimeoutCause(
	parent context.Context,
	timeout time.Duration,
	cause error,
) iter.Seq[context.Context]

WithTimeoutCause returns Context(context.WithTimeoutCause(parent, timeout, cause)).

See Context, context.WithTimeoutCause.

Example
package main

import (
	"context"
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/goaux/scope"
)

func main() {
	var g sync.WaitGroup
	for ctx := range scope.WithTimeoutCause(context.TODO(), 100*time.Millisecond, errors.New("hello")) {
		fmt.Println("pass 0")
		g.Add(1)
		go func() {
			defer g.Done()
			<-ctx.Done()
			fmt.Println("pass 1", ctx.Err())
		}()
		// ctx will be canceled at the end of the loop body.
	}
	g.Wait()
	fmt.Println("ok")
}
Output:

pass 0
pass 1 context canceled
ok

Types

type Rollbacker

type Rollbacker interface {
	Rollback() error
}

Rollbacker is a base type that Tx and Tx2 accept.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL