diff options
author | Ian Lance Taylor <ian@gcc.gnu.org> | 2012-03-30 21:27:11 +0000 |
---|---|---|
committer | Ian Lance Taylor <ian@gcc.gnu.org> | 2012-03-30 21:27:11 +0000 |
commit | 456fba2651cfb0cb67e44b8354668a0b3f5f5182 (patch) | |
tree | 9a0dfa827abe382ac0f44768e5365b87f00ac0a9 /libgo/go/database | |
parent | e0be8a5c203451b47fd3da59b0e0f56cc3d42f22 (diff) | |
download | gcc-456fba2651cfb0cb67e44b8354668a0b3f5f5182.zip gcc-456fba2651cfb0cb67e44b8354668a0b3f5f5182.tar.gz gcc-456fba2651cfb0cb67e44b8354668a0b3f5f5182.tar.bz2 |
libgo: Update to weekly.2012-03-13.
From-SVN: r186023
Diffstat (limited to 'libgo/go/database')
-rw-r--r-- | libgo/go/database/sql/driver/driver.go | 11 | ||||
-rw-r--r-- | libgo/go/database/sql/fakedb_test.go | 21 | ||||
-rw-r--r-- | libgo/go/database/sql/sql.go | 145 | ||||
-rw-r--r-- | libgo/go/database/sql/sql_test.go | 115 |
4 files changed, 221 insertions, 71 deletions
diff --git a/libgo/go/database/sql/driver/driver.go b/libgo/go/database/sql/driver/driver.go index 7f986b8..2f5280d 100644 --- a/libgo/go/database/sql/driver/driver.go +++ b/libgo/go/database/sql/driver/driver.go @@ -43,6 +43,17 @@ type Driver interface { // documented. var ErrSkip = errors.New("driver: skip fast-path; continue as if unimplemented") +// ErrBadConn should be returned by a driver to signal to the sql +// package that a driver.Conn is in a bad state (such as the server +// having earlier closed the connection) and the sql package should +// retry on a new connection. +// +// To prevent duplicate operations, ErrBadConn should NOT be returned +// if there's a possibility that the database server might have +// performed the operation. Even if the server sends back an error, +// you shouldn't return ErrBadConn. +var ErrBadConn = errors.New("driver: bad connection") + // Execer is an optional interface that may be implemented by a Conn. // // If a Conn does not implement Execer, the db package's DB.Exec will diff --git a/libgo/go/database/sql/fakedb_test.go b/libgo/go/database/sql/fakedb_test.go index fc63f03..184e775 100644 --- a/libgo/go/database/sql/fakedb_test.go +++ b/libgo/go/database/sql/fakedb_test.go @@ -82,6 +82,7 @@ type fakeConn struct { mu sync.Mutex stmtsMade int stmtsClosed int + numPrepare int } func (c *fakeConn) incrStat(v *int) { @@ -208,10 +209,13 @@ func (c *fakeConn) Begin() (driver.Tx, error) { func (c *fakeConn) Close() error { if c.currTx != nil { - return errors.New("can't close; in a Transaction") + return errors.New("can't close fakeConn; in a Transaction") } if c.db == nil { - return errors.New("can't close; already closed") + return errors.New("can't close fakeConn; already closed") + } + if c.stmtsMade > c.stmtsClosed { + return errors.New("can't close; dangling statement(s)") } c.db = nil return nil @@ -249,6 +253,7 @@ func errf(msg string, args ...interface{}) error { // just a limitation for fakedb) func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 3 { + stmt.Close() return nil, errf("invalid SELECT syntax with %d parts; want 3", len(parts)) } stmt.table = parts[0] @@ -259,14 +264,17 @@ func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, e } nameVal := strings.Split(colspec, "=") if len(nameVal) != 2 { + stmt.Close() return nil, errf("SELECT on table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } column, value := nameVal[0], nameVal[1] _, ok := c.db.columnType(stmt.table, column) if !ok { + stmt.Close() return nil, errf("SELECT on table %q references non-existent column %q", stmt.table, column) } if value != "?" { + stmt.Close() return nil, errf("SELECT on table %q has pre-bound value for where column %q; need a question mark", stmt.table, column) } @@ -279,12 +287,14 @@ func (c *fakeConn) prepareSelect(stmt *fakeStmt, parts []string) (driver.Stmt, e // parts are table|col=type,col2=type2 func (c *fakeConn) prepareCreate(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 2 { + stmt.Close() return nil, errf("invalid CREATE syntax with %d parts; want 2", len(parts)) } stmt.table = parts[0] for n, colspec := range strings.Split(parts[1], ",") { nameType := strings.Split(colspec, "=") if len(nameType) != 2 { + stmt.Close() return nil, errf("CREATE table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } stmt.colName = append(stmt.colName, nameType[0]) @@ -296,17 +306,20 @@ func (c *fakeConn) prepareCreate(stmt *fakeStmt, parts []string) (driver.Stmt, e // parts are table|col=?,col2=val func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, error) { if len(parts) != 2 { + stmt.Close() return nil, errf("invalid INSERT syntax with %d parts; want 2", len(parts)) } stmt.table = parts[0] for n, colspec := range strings.Split(parts[1], ",") { nameVal := strings.Split(colspec, "=") if len(nameVal) != 2 { + stmt.Close() return nil, errf("INSERT table %q has invalid column spec of %q (index %d)", stmt.table, colspec, n) } column, value := nameVal[0], nameVal[1] ctype, ok := c.db.columnType(stmt.table, column) if !ok { + stmt.Close() return nil, errf("INSERT table %q references non-existent column %q", stmt.table, column) } stmt.colName = append(stmt.colName, column) @@ -322,10 +335,12 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e case "int32": i, err := strconv.Atoi(value) if err != nil { + stmt.Close() return nil, errf("invalid conversion to int32 from %q", value) } subsetVal = int64(i) // int64 is a subset type, but not int32 default: + stmt.Close() return nil, errf("unsupported conversion for pre-bound parameter %q to type %q", value, ctype) } stmt.colValue = append(stmt.colValue, subsetVal) @@ -339,6 +354,7 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e } func (c *fakeConn) Prepare(query string) (driver.Stmt, error) { + c.numPrepare++ if c.db == nil { panic("nil c.db; conn = " + fmt.Sprintf("%#v", c)) } @@ -360,6 +376,7 @@ func (c *fakeConn) Prepare(query string) (driver.Stmt, error) { case "INSERT": return c.prepareInsert(stmt, parts) default: + stmt.Close() return nil, errf("unsupported command type %q", cmd) } return stmt, nil diff --git a/libgo/go/database/sql/sql.go b/libgo/go/database/sql/sql.go index 62b551d..51a357b 100644 --- a/libgo/go/database/sql/sql.go +++ b/libgo/go/database/sql/sql.go @@ -175,6 +175,16 @@ var ErrNoRows = errors.New("sql: no rows in result set") // DB is a database handle. It's safe for concurrent use by multiple // goroutines. +// +// If the underlying database driver has the concept of a connection +// and per-connection session state, the sql package manages creating +// and freeing connections automatically, including maintaining a free +// pool of idle connections. If observing session state is required, +// either do not share a *DB between multiple concurrent goroutines or +// create and observe all state only within a transaction. Once +// DB.Open is called, the returned Tx is bound to a single isolated +// connection. Once Tx.Commit or Tx.Rollback is called, that +// connection is returned to DB's idle connection pool. type DB struct { driver driver.Driver dsn string @@ -241,34 +251,56 @@ func (db *DB) conn() (driver.Conn, error) { func (db *DB) connIfFree(wanted driver.Conn) (conn driver.Conn, ok bool) { db.mu.Lock() defer db.mu.Unlock() - for n, conn := range db.freeConn { - if conn == wanted { - db.freeConn[n] = db.freeConn[len(db.freeConn)-1] - db.freeConn = db.freeConn[:len(db.freeConn)-1] - return wanted, true + for i, conn := range db.freeConn { + if conn != wanted { + continue } + db.freeConn[i] = db.freeConn[len(db.freeConn)-1] + db.freeConn = db.freeConn[:len(db.freeConn)-1] + return wanted, true } return nil, false } -func (db *DB) putConn(c driver.Conn) { +// putConnHook is a hook for testing. +var putConnHook func(*DB, driver.Conn) + +// putConn adds a connection to the db's free pool. +// err is optionally the last error that occured on this connection. +func (db *DB) putConn(c driver.Conn, err error) { + if err == driver.ErrBadConn { + // Don't reuse bad connections. + return + } db.mu.Lock() - defer db.mu.Unlock() + if putConnHook != nil { + putConnHook(db, c) + } if n := len(db.freeConn); !db.closed && n < db.maxIdleConns() { db.freeConn = append(db.freeConn, c) + db.mu.Unlock() return } - db.closeConn(c) // TODO(bradfitz): release lock before calling this? -} - -func (db *DB) closeConn(c driver.Conn) { - // TODO: check to see if we need this Conn for any prepared statements - // that are active. + // TODO: check to see if we need this Conn for any prepared + // statements which are still active? + db.mu.Unlock() c.Close() } // Prepare creates a prepared statement for later execution. func (db *DB) Prepare(query string) (*Stmt, error) { + var stmt *Stmt + var err error + for i := 0; i < 10; i++ { + stmt, err = db.prepare(query) + if err != driver.ErrBadConn { + break + } + } + return stmt, err +} + +func (db *DB) prepare(query string) (stmt *Stmt, err error) { // TODO: check if db.driver supports an optional // driver.Preparer interface and call that instead, if so, // otherwise we make a prepared statement that's bound @@ -279,12 +311,12 @@ func (db *DB) Prepare(query string) (*Stmt, error) { if err != nil { return nil, err } - defer db.putConn(ci) + defer db.putConn(ci, err) si, err := ci.Prepare(query) if err != nil { return nil, err } - stmt := &Stmt{ + stmt = &Stmt{ db: db, query: query, css: []connStmt{{ci, si}}, @@ -295,15 +327,22 @@ func (db *DB) Prepare(query string) (*Stmt, error) { // Exec executes a query without returning any rows. func (db *DB) Exec(query string, args ...interface{}) (Result, error) { sargs, err := subsetTypeArgs(args) - if err != nil { - return nil, err + var res Result + for i := 0; i < 10; i++ { + res, err = db.exec(query, sargs) + if err != driver.ErrBadConn { + break + } } + return res, err +} +func (db *DB) exec(query string, sargs []driver.Value) (res Result, err error) { ci, err := db.conn() if err != nil { return nil, err } - defer db.putConn(ci) + defer db.putConn(ci, err) if execer, ok := ci.(driver.Execer); ok { resi, err := execer.Exec(query, sargs) @@ -354,13 +393,25 @@ func (db *DB) QueryRow(query string, args ...interface{}) *Row { // Begin starts a transaction. The isolation level is dependent on // the driver. func (db *DB) Begin() (*Tx, error) { + var tx *Tx + var err error + for i := 0; i < 10; i++ { + tx, err = db.begin() + if err != driver.ErrBadConn { + break + } + } + return tx, err +} + +func (db *DB) begin() (tx *Tx, err error) { ci, err := db.conn() if err != nil { return nil, err } txi, err := ci.Begin() if err != nil { - db.putConn(ci) + db.putConn(ci, err) return nil, fmt.Errorf("sql: failed to Begin transaction: %v", err) } return &Tx{ @@ -406,7 +457,7 @@ func (tx *Tx) close() { panic("double close") // internal error } tx.done = true - tx.db.putConn(tx.ci) + tx.db.putConn(tx.ci, nil) tx.ci = nil tx.txi = nil } @@ -561,9 +612,11 @@ func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error) { return nil, err } rows, err := stmt.Query(args...) - if err == nil { - rows.closeStmt = stmt + if err != nil { + stmt.Close() + return nil, err } + rows.closeStmt = stmt return rows, err } @@ -609,7 +662,7 @@ func (s *Stmt) Exec(args ...interface{}) (Result, error) { if err != nil { return nil, err } - defer releaseConn() + defer releaseConn(nil) // -1 means the driver doesn't know how to count the number of // placeholders, so we won't sanity check input here and instead let the @@ -672,7 +725,7 @@ func (s *Stmt) Exec(args ...interface{}) (Result, error) { // connStmt returns a free driver connection on which to execute the // statement, a function to call to release the connection, and a // statement bound to that connection. -func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, err error) { +func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(error), si driver.Stmt, err error) { if err = s.stickyErr; err != nil { return } @@ -691,7 +744,7 @@ func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, e if err != nil { return } - releaseConn = func() { s.tx.releaseConn() } + releaseConn = func(error) { s.tx.releaseConn() } return ci, releaseConn, s.txsi, nil } @@ -700,7 +753,7 @@ func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, e for _, v := range s.css { // TODO(bradfitz): lazily clean up entries in this // list with dead conns while enumerating - if _, match = s.db.connIfFree(cs.ci); match { + if _, match = s.db.connIfFree(v.ci); match { cs = v break } @@ -710,22 +763,28 @@ func (s *Stmt) connStmt() (ci driver.Conn, releaseConn func(), si driver.Stmt, e // Make a new conn if all are busy. // TODO(bradfitz): or wait for one? make configurable later? if !match { - ci, err := s.db.conn() - if err != nil { - return nil, nil, nil, err - } - si, err := ci.Prepare(s.query) - if err != nil { - return nil, nil, nil, err + for i := 0; ; i++ { + ci, err := s.db.conn() + if err != nil { + return nil, nil, nil, err + } + si, err := ci.Prepare(s.query) + if err == driver.ErrBadConn && i < 10 { + continue + } + if err != nil { + return nil, nil, nil, err + } + s.mu.Lock() + cs = connStmt{ci, si} + s.css = append(s.css, cs) + s.mu.Unlock() + break } - s.mu.Lock() - cs = connStmt{ci, si} - s.css = append(s.css, cs) - s.mu.Unlock() } conn := cs.ci - releaseConn = func() { s.db.putConn(conn) } + releaseConn = func(err error) { s.db.putConn(conn, err) } return conn, releaseConn, cs.si, nil } @@ -749,7 +808,7 @@ func (s *Stmt) Query(args ...interface{}) (*Rows, error) { } rowsi, err := si.Query(sargs) if err != nil { - s.db.putConn(ci) + releaseConn(err) return nil, err } // Note: ownership of ci passes to the *Rows, to be freed @@ -800,7 +859,7 @@ func (s *Stmt) Close() error { for _, v := range s.css { if ci, match := s.db.connIfFree(v.ci); match { v.si.Close() - s.db.putConn(ci) + s.db.putConn(ci, nil) } else { // TODO(bradfitz): care that we can't close // this statement because the statement's @@ -827,7 +886,7 @@ func (s *Stmt) Close() error { type Rows struct { db *DB ci driver.Conn // owned; must call putconn when closed to release - releaseConn func() + releaseConn func(error) rowsi driver.Rows closed bool @@ -939,7 +998,7 @@ func (rs *Rows) Close() error { } rs.closed = true err := rs.rowsi.Close() - rs.releaseConn() + rs.releaseConn(err) if rs.closeStmt != nil { rs.closeStmt.Close() } @@ -963,7 +1022,7 @@ func (r *Row) Scan(dest ...interface{}) error { } // TODO(bradfitz): for now we need to defensively clone all - // []byte that the driver returned (not permitting + // []byte that the driver returned (not permitting // *RawBytes in Rows.Scan), since we're about to close // the Rows in our defer, when we return from this function. // the contract with the driver.Next(...) interface is that it diff --git a/libgo/go/database/sql/sql_test.go b/libgo/go/database/sql/sql_test.go index c985a10..b296705 100644 --- a/libgo/go/database/sql/sql_test.go +++ b/libgo/go/database/sql/sql_test.go @@ -5,13 +5,35 @@ package sql import ( + "database/sql/driver" "fmt" "reflect" + "runtime" "strings" "testing" "time" ) +func init() { + type dbConn struct { + db *DB + c driver.Conn + } + freedFrom := make(map[dbConn]string) + putConnHook = func(db *DB, c driver.Conn) { + for _, oc := range db.freeConn { + if oc == c { + // print before panic, as panic may get lost due to conflicting panic + // (all goroutines asleep) elsewhere, since we might not unlock + // the mutex in freeConn here. + println("double free of conn. conflicts are:\nA) " + freedFrom[dbConn{db, c}] + "\n\nand\nB) " + stack()) + panic("double free of conn.") + } + } + freedFrom[dbConn{db, c}] = stack() + } +} + const fakeDBName = "foo" var chrisBirthday = time.Unix(123456789, 0) @@ -47,9 +69,19 @@ func closeDB(t *testing.T, db *DB) { } } +// numPrepares assumes that db has exactly 1 idle conn and returns +// its count of calls to Prepare +func numPrepares(t *testing.T, db *DB) int { + if n := len(db.freeConn); n != 1 { + t.Fatalf("free conns = %d; want 1", n) + } + return db.freeConn[0].(*fakeConn).numPrepare +} + func TestQuery(t *testing.T) { db := newTestDB(t, "people") defer closeDB(t, db) + prepares0 := numPrepares(t, db) rows, err := db.Query("SELECT|people|age,name|") if err != nil { t.Fatalf("Query: %v", err) @@ -83,7 +115,10 @@ func TestQuery(t *testing.T) { // And verify that the final rows.Next() call, which hit EOF, // also closed the rows connection. if n := len(db.freeConn); n != 1 { - t.Errorf("free conns after query hitting EOF = %d; want 1", n) + t.Fatalf("free conns after query hitting EOF = %d; want 1", n) + } + if prepares := numPrepares(t, db) - prepares0; prepares != 1 { + t.Errorf("executed %d Prepare statements; want 1", prepares) } } @@ -216,6 +251,7 @@ func TestStatementQueryRow(t *testing.T) { if err != nil { t.Fatalf("Prepare: %v", err) } + defer stmt.Close() var age int for n, tt := range []struct { name string @@ -256,6 +292,7 @@ func TestExec(t *testing.T) { if err != nil { t.Errorf("Stmt, err = %v, %v", stmt, err) } + defer stmt.Close() type execTest struct { args []interface{} @@ -297,11 +334,14 @@ func TestTxStmt(t *testing.T) { if err != nil { t.Fatalf("Stmt, err = %v, %v", stmt, err) } + defer stmt.Close() tx, err := db.Begin() if err != nil { t.Fatalf("Begin = %v", err) } - _, err = tx.Stmt(stmt).Exec("Bobby", 7) + txs := tx.Stmt(stmt) + defer txs.Close() + _, err = txs.Exec("Bobby", 7) if err != nil { t.Fatalf("Exec = %v", err) } @@ -330,6 +370,7 @@ func TestTxQuery(t *testing.T) { if err != nil { t.Fatal(err) } + defer r.Close() if !r.Next() { if r.Err() != nil { @@ -345,6 +386,22 @@ func TestTxQuery(t *testing.T) { } } +func TestTxQueryInvalid(t *testing.T) { + db := newTestDB(t, "") + defer closeDB(t, db) + + tx, err := db.Begin() + if err != nil { + t.Fatal(err) + } + defer tx.Rollback() + + _, err = tx.Query("SELECT|t1|name|") + if err == nil { + t.Fatal("Error expected") + } +} + // Tests fix for issue 2542, that we release a lock when querying on // a closed connection. func TestIssue2542Deadlock(t *testing.T) { @@ -450,48 +507,48 @@ type nullTestSpec struct { func TestNullStringParam(t *testing.T) { spec := nullTestSpec{"nullstring", "string", [6]nullTestRow{ - nullTestRow{NullString{"aqua", true}, "", NullString{"aqua", true}}, - nullTestRow{NullString{"brown", false}, "", NullString{"", false}}, - nullTestRow{"chartreuse", "", NullString{"chartreuse", true}}, - nullTestRow{NullString{"darkred", true}, "", NullString{"darkred", true}}, - nullTestRow{NullString{"eel", false}, "", NullString{"", false}}, - nullTestRow{"foo", NullString{"black", false}, nil}, + {NullString{"aqua", true}, "", NullString{"aqua", true}}, + {NullString{"brown", false}, "", NullString{"", false}}, + {"chartreuse", "", NullString{"chartreuse", true}}, + {NullString{"darkred", true}, "", NullString{"darkred", true}}, + {NullString{"eel", false}, "", NullString{"", false}}, + {"foo", NullString{"black", false}, nil}, }} nullTestRun(t, spec) } func TestNullInt64Param(t *testing.T) { spec := nullTestSpec{"nullint64", "int64", [6]nullTestRow{ - nullTestRow{NullInt64{31, true}, 1, NullInt64{31, true}}, - nullTestRow{NullInt64{-22, false}, 1, NullInt64{0, false}}, - nullTestRow{22, 1, NullInt64{22, true}}, - nullTestRow{NullInt64{33, true}, 1, NullInt64{33, true}}, - nullTestRow{NullInt64{222, false}, 1, NullInt64{0, false}}, - nullTestRow{0, NullInt64{31, false}, nil}, + {NullInt64{31, true}, 1, NullInt64{31, true}}, + {NullInt64{-22, false}, 1, NullInt64{0, false}}, + {22, 1, NullInt64{22, true}}, + {NullInt64{33, true}, 1, NullInt64{33, true}}, + {NullInt64{222, false}, 1, NullInt64{0, false}}, + {0, NullInt64{31, false}, nil}, }} nullTestRun(t, spec) } func TestNullFloat64Param(t *testing.T) { spec := nullTestSpec{"nullfloat64", "float64", [6]nullTestRow{ - nullTestRow{NullFloat64{31.2, true}, 1, NullFloat64{31.2, true}}, - nullTestRow{NullFloat64{13.1, false}, 1, NullFloat64{0, false}}, - nullTestRow{-22.9, 1, NullFloat64{-22.9, true}}, - nullTestRow{NullFloat64{33.81, true}, 1, NullFloat64{33.81, true}}, - nullTestRow{NullFloat64{222, false}, 1, NullFloat64{0, false}}, - nullTestRow{10, NullFloat64{31.2, false}, nil}, + {NullFloat64{31.2, true}, 1, NullFloat64{31.2, true}}, + {NullFloat64{13.1, false}, 1, NullFloat64{0, false}}, + {-22.9, 1, NullFloat64{-22.9, true}}, + {NullFloat64{33.81, true}, 1, NullFloat64{33.81, true}}, + {NullFloat64{222, false}, 1, NullFloat64{0, false}}, + {10, NullFloat64{31.2, false}, nil}, }} nullTestRun(t, spec) } func TestNullBoolParam(t *testing.T) { spec := nullTestSpec{"nullbool", "bool", [6]nullTestRow{ - nullTestRow{NullBool{false, true}, true, NullBool{false, true}}, - nullTestRow{NullBool{true, false}, false, NullBool{false, false}}, - nullTestRow{true, true, NullBool{true, true}}, - nullTestRow{NullBool{true, true}, false, NullBool{true, true}}, - nullTestRow{NullBool{true, false}, true, NullBool{false, false}}, - nullTestRow{true, NullBool{true, false}, nil}, + {NullBool{false, true}, true, NullBool{false, true}}, + {NullBool{true, false}, false, NullBool{false, false}}, + {true, true, NullBool{true, true}}, + {NullBool{true, true}, false, NullBool{true, true}}, + {NullBool{true, false}, true, NullBool{false, false}}, + {true, NullBool{true, false}, nil}, }} nullTestRun(t, spec) } @@ -510,6 +567,7 @@ func nullTestRun(t *testing.T, spec nullTestSpec) { if err != nil { t.Fatalf("prepare: %v", err) } + defer stmt.Close() if _, err := stmt.Exec(3, "chris", spec.rows[2].nullParam, spec.rows[2].notNullParam); err != nil { t.Errorf("exec insert chris: %v", err) } @@ -549,3 +607,8 @@ func nullTestRun(t *testing.T, spec nullTestSpec) { } } } + +func stack() string { + buf := make([]byte, 1024) + return string(buf[:runtime.Stack(buf, false)]) +} |