tessl install tessl/golang-cloud-google-com--go--spanner@1.87.2Official Google Cloud Spanner client library for Go providing comprehensive database operations, transactions, and admin functionality
This document covers all read operations including single reads, batch reads, queries, and read options.
Spanner supports several types of read operations:
Use Single() for one-shot read operations:
func (c *Client) Single() *ReadOnlyTransactionfunc (t *ReadOnlyTransaction) ReadRow(ctx context.Context, table string, key Key, columns []string) (*Row, error)Example:
row, err := client.Single().ReadRow(ctx, "Users",
spanner.Key{"alice"}, []string{"email", "name"})
if err != nil {
return err
}
var email, name string
if err := row.Columns(&email, &name); err != nil {
return err
}func (t *ReadOnlyTransaction) Read(ctx context.Context, table string, keys KeySet, columns []string) *RowIteratorExample:
iter := client.Single().Read(ctx, "Users",
spanner.AllKeys(), []string{"id", "name"})
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
// Process row
}func (t *ReadOnlyTransaction) ReadUsingIndex(ctx context.Context, table, index string, keys KeySet, columns []string) *RowIteratorExample:
iter := client.Single().ReadUsingIndex(ctx, "Users", "UsersByEmail",
spanner.Key{"alice@example.com"}, []string{"id", "name"})func (t *ReadOnlyTransaction) ReadWithOptions(ctx context.Context, table string, keys KeySet, columns []string, opts *ReadOptions) *RowIterator
type ReadOptions struct {
Index string
Limit int
Priority sppb.RequestOptions_Priority
RequestTag string
DataBoostEnabled bool
DirectedReadOptions *sppb.DirectedReadOptions
OrderBy sppb.ReadRequest_OrderBy
LockHint sppb.ReadRequest_LockHint
}Example:
opts := &spanner.ReadOptions{
Limit: 100,
Priority: sppb.RequestOptions_PRIORITY_HIGH,
RequestTag: "user-query-001",
}
iter := client.Single().ReadWithOptions(ctx, "Users",
spanner.AllKeys(), []string{"id", "name"}, opts)func (t *ReadOnlyTransaction) Query(ctx context.Context, statement Statement) *RowIterator
type Statement struct {
SQL string
Params map[string]interface{}
}
func NewStatement(sql string) StatementExample:
stmt := spanner.NewStatement("SELECT id, name FROM Users WHERE age > @minAge")
stmt.Params["minAge"] = 18
iter := client.Single().Query(ctx, stmt)
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
var id int64
var name string
if err := row.Columns(&id, &name); err != nil {
return err
}
}func (t *ReadOnlyTransaction) QueryWithOptions(ctx context.Context, statement Statement, opts QueryOptions) *RowIterator
type QueryOptions struct {
Mode *sppb.ExecuteSqlRequest_QueryMode
Options *sppb.ExecuteSqlRequest_QueryOptions
Priority sppb.RequestOptions_Priority
RequestTag string
DataBoostEnabled bool
DirectedReadOptions *sppb.DirectedReadOptions
ExcludeTxnFromChangeStreams bool
LastStatement bool
}Example:
stmt := spanner.NewStatement("SELECT * FROM Users WHERE region = @region")
stmt.Params["region"] = "us-west"
opts := spanner.QueryOptions{
Priority: sppb.RequestOptions_PRIORITY_MEDIUM,
RequestTag: "regional-query",
}
iter := client.Single().QueryWithOptions(ctx, stmt, opts)func (t *ReadOnlyTransaction) QueryWithStats(ctx context.Context, statement Statement) *RowIteratorExample:
stmt := spanner.NewStatement("SELECT * FROM Users WHERE active = true")
iter := client.Single().QueryWithStats(ctx, stmt)
defer iter.Stop()
// Process rows
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
// Process row
}
// Access statistics after iteration
fmt.Printf("Rows returned: %d\n", iter.RowCount)
fmt.Printf("Query stats: %v\n", iter.QueryStats)func (t *ReadOnlyTransaction) AnalyzeQuery(ctx context.Context, statement Statement) (*sppb.QueryPlan, error)Example:
stmt := spanner.NewStatement("SELECT * FROM Users WHERE region = @region")
stmt.Params["region"] = "us-east"
plan, err := client.Single().AnalyzeQuery(ctx, stmt)
if err != nil {
return err
}
// Inspect query plan
fmt.Printf("Query plan: %v\n", plan)Iterator for reading multiple rows:
type RowIterator struct {
QueryPlan *sppb.QueryPlan // Available after QueryWithStats completes
QueryStats map[string]interface{} // Available after QueryWithStats completes
RowCount int64 // Rows affected (for DML)
Metadata *sppb.ResultSetMetadata // Result metadata
}
func (r *RowIterator) Next() (*Row, error)
func (r *RowIterator) Do(f func(r *Row) error) error
func (r *RowIterator) Stop()iter := client.Single().Read(ctx, "Users", spanner.AllKeys(), []string{"id", "name"})
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
return err
}
// Process row
}iter := client.Single().Read(ctx, "Users", spanner.AllKeys(), []string{"id", "name"})
err := iter.Do(func(row *spanner.Row) error {
var id int64
var name string
if err := row.Columns(&id, &name); err != nil {
return err
}
fmt.Printf("%d: %s\n", id, name)
return nil
})
if err != nil {
return err
}type Row struct {
// Has unexported fields
}
func NewRow(columnNames []string, columnValues []interface{}) (*Row, error)func (r *Row) Column(i int, ptr interface{}) error
func (r *Row) ColumnByName(name string, ptr interface{}) error
func (r *Row) Columns(ptrs ...interface{}) error
func (r *Row) ToStruct(p interface{}) error
func (r *Row) ToStructLenient(p interface{}) error
func (r *Row) Size() int
func (r *Row) ColumnNames() []string
func (r *Row) ColumnName(i int) string
func (r *Row) ColumnIndex(name string) (int, error)
func (r *Row) ColumnType(i int) *sppb.Type
func (r *Row) ColumnValue(i int) *proto3.ValueBy Position:
var id int64
var name string
if err := row.Column(0, &id); err != nil {
return err
}
if err := row.Column(1, &name); err != nil {
return err
}By Name:
var email string
if err := row.ColumnByName("email", &email); err != nil {
return err
}All Columns:
var id int64
var name, email string
if err := row.Columns(&id, &name, &email); err != nil {
return err
}To Struct:
type User struct {
ID int64 `spanner:"id"`
Name string `spanner:"name"`
Email string `spanner:"email"`
}
var user User
if err := row.ToStruct(&user); err != nil {
return err
}For multiple consistent reads at the same timestamp:
func (c *Client) ReadOnlyTransaction() *ReadOnlyTransaction
type ReadOnlyTransaction struct {
// Has unexported fields
}All read methods from Single() plus:
func (t *ReadOnlyTransaction) Close()
func (t *ReadOnlyTransaction) Timestamp() (time.Time, error)
func (t *ReadOnlyTransaction) WithTimestampBound(tb TimestampBound) *ReadOnlyTransaction
func (t *ReadOnlyTransaction) WithBeginTransactionOption(option BeginTransactionOption) *ReadOnlyTransactionExample:
txn := client.ReadOnlyTransaction()
defer txn.Close()
// Multiple reads at same timestamp
row1, err := txn.ReadRow(ctx, "Users", spanner.Key{"alice"}, []string{"balance"})
if err != nil {
return err
}
row2, err := txn.ReadRow(ctx, "Accounts", spanner.Key{"123"}, []string{"total"})
if err != nil {
return err
}
// Get read timestamp
ts, err := txn.Timestamp()
if err != nil {
return err
}
fmt.Printf("Read at timestamp: %v\n", ts)Control when data is read using timestamp bounds:
type TimestampBound struct {
// Has unexported fields
}
func StrongRead() TimestampBound
func ReadTimestamp(t time.Time) TimestampBound
func ExactStaleness(d time.Duration) TimestampBound
func MinReadTimestamp(t time.Time) TimestampBound
func MaxStaleness(d time.Duration) TimestampBoundRead latest committed data:
row, err := client.Single().ReadRow(ctx, "Users", key, columns)
// Equivalent to:
row, err := client.Single().WithTimestampBound(spanner.StrongRead()).ReadRow(ctx, "Users", key, columns)Read data at a specific staleness:
// Read data as it was 15 seconds ago
txn := client.Single().WithTimestampBound(spanner.ExactStaleness(15 * time.Second))
iter := txn.Query(ctx, stmt)Read at specific timestamp:
ts := time.Now().Add(-1 * time.Hour)
txn := client.Single().WithTimestampBound(spanner.ReadTimestamp(ts))
row, err := txn.ReadRow(ctx, "Users", key, columns)Read data at most N seconds stale:
// Read data at most 10 seconds stale
txn := client.Single().WithTimestampBound(spanner.MaxStaleness(10 * time.Second))
iter := txn.Read(ctx, "Users", keys, columns)Read at timestamp at least as recent as specified:
minTs := time.Now().Add(-5 * time.Minute)
txn := client.Single().WithTimestampBound(spanner.MinReadTimestamp(minTs))
iter := txn.Query(ctx, stmt)For partitioned reads across multiple machines:
func (c *Client) BatchReadOnlyTransaction(ctx context.Context, tb TimestampBound) (*BatchReadOnlyTransaction, error)
func (c *Client) BatchReadOnlyTransactionFromID(tid BatchReadOnlyTransactionID) *BatchReadOnlyTransaction
type BatchReadOnlyTransaction struct {
ReadOnlyTransaction
ID BatchReadOnlyTransactionID
}func (t *BatchReadOnlyTransaction) PartitionRead(ctx context.Context, table string, keys KeySet, columns []string, opt PartitionOptions) ([]*Partition, error)
func (t *BatchReadOnlyTransaction) PartitionReadUsingIndex(ctx context.Context, table, index string, keys KeySet, columns []string, opt PartitionOptions) ([]*Partition, error)
func (t *BatchReadOnlyTransaction) PartitionReadWithOptions(ctx context.Context, table string, keys KeySet, columns []string, opt PartitionOptions, readOptions ReadOptions) ([]*Partition, error)
type PartitionOptions struct {
PartitionBytes int64 // Desired data size per partition
MaxPartitions int64 // Desired maximum partitions
}func (t *BatchReadOnlyTransaction) PartitionQuery(ctx context.Context, statement Statement, opt PartitionOptions) ([]*Partition, error)
func (t *BatchReadOnlyTransaction) PartitionQueryWithOptions(ctx context.Context, statement Statement, opt PartitionOptions, qOpts QueryOptions) ([]*Partition, error)func (t *BatchReadOnlyTransaction) Execute(ctx context.Context, p *Partition) *RowIterator// Create batch transaction
txn, err := client.BatchReadOnlyTransaction(ctx, spanner.StrongRead())
if err != nil {
return err
}
defer txn.Close()
// Create partitions
stmt := spanner.NewStatement("SELECT * FROM Users WHERE active = true")
partitions, err := txn.PartitionQuery(ctx, stmt, spanner.PartitionOptions{
PartitionBytes: 100000000, // 100MB per partition
})
if err != nil {
return err
}
// Execute partitions in parallel
var wg sync.WaitGroup
for _, partition := range partitions {
wg.Add(1)
go func(p *spanner.Partition) {
defer wg.Done()
iter := txn.Execute(ctx, p)
defer iter.Stop()
for {
row, err := iter.Next()
if err == iterator.Done {
break
}
if err != nil {
log.Printf("Error: %v", err)
return
}
// Process row
}
}(partition)
}
wg.Wait()Share batch transaction across processes:
// Process 1: Create and serialize
txn, err := client.BatchReadOnlyTransaction(ctx, spanner.StrongRead())
if err != nil {
return err
}
txnID := txn.ID
data, err := txnID.MarshalBinary()
if err != nil {
return err
}
// Share 'data' with other processes
// Process 2: Reconstruct
var txnID spanner.BatchReadOnlyTransactionID
if err := txnID.UnmarshalBinary(data); err != nil {
return err
}
txn := client.BatchReadOnlyTransactionFromID(txnID)
// Use txn for readsUtility for iterating all rows into a slice:
func SelectAll(rows rowIterator, destination interface{}, options ...DecodeOptions) error
func WithLenient() DecodeOptionsExample:
type User struct {
ID int64
Name string
Email string
}
stmt := spanner.NewStatement("SELECT id, name, email FROM Users")
iter := client.Single().Query(ctx, stmt)
var users []*User
if err := spanner.SelectAll(iter, &users); err != nil {
return err
}
// Or with lenient mode (ignore extra columns)
if err := spanner.SelectAll(iter, &users, spanner.WithLenient()); err != nil {
return err
}