diff --git a/README.md b/README.md index f0fd423..f4926af 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,30 @@ -## null [![GoDoc](https://godoc.org/github.com/guregu/null?status.svg)](https://godoc.org/github.com/guregu/null) [![Coverage](http://gocover.io/_badge/github.com/guregu/null?)](http://gocover.io/github.com/guregu/null) +## null [![GoDoc](https://godoc.org/github.com/guregu/null?status.svg)](https://godoc.org/github.com/guregu/null) [![Coverage](http://gocover.io/_badge/github.com/guregu/null)](http://gocover.io/github.com/guregu/null) null is a library with opinions on how to deal with nullable SQL and JSON values -### String +There are two packages, `null`, and `nuller`. +Types in `null` are treated like zero values in Go: blank string input will produce a null `null.String`, and null Strings will JSON encode to `""`. If you need zero and null treated the same, use these. +Types in `nuller` will only be considered null on null input, and will JSON encode to `null`. If you need zero and null be considered separate values, use these. +All types implement `sql.Scanner`, so you can use this library in place of `sql.NullXXX`. + +### null.String A nullable string. Implements `sql.Scanner`, `encoding.Marshaler` and `encoding.TextUnmarshaler`, providing support for JSON and XML. -Will marshal to a blank string if null. Blank string input produces a null String. In other words, null values and empty values are considered equivalent. +Will marshal to a blank string if null. Blank string input produces a null String. In other words, null values and empty values are considered equivalent. Can unmarshal from `sql.NullString` JSON input. -`UnmarshalJSON` supports `sql.NullString` input. +### null.Int +A nullable int64. -### Int -A nullable int64. +Will marshal to 0 if null. Blank string or 0 input produces a null Int. In other words, null values and empty values are considered equivalent. Can unmarshal from `sql.NullInt64` JSON input. -Unlike null.String, null.Int will marshal to null if null. Zero input will not produce a null Int. +### nuller.String +An even nuller nullable string. -`UnmarshalJSON` supports `sql.NullInt64` input. +Unlike `null.String`, `nuller.String` will marshal to null if null. Zero (blank) input will not produce a null String. Can unmarshal from `sql.NullString` JSON input. + +### nuller.Int +An even nuller nullable int64. + +Unlike `null.Int`, `nuller.Int` will marshal to null if null. Zero input will not produce a null Int. Can unmarshal from `sql.NullInt64` JSON input. ### Bugs `json`'s `",omitempty"` struct tag does not work correctly right now. It will never omit a null or empty String. This should be [fixed in Go 1.4](https://code.google.com/p/go/issues/detail?id=4357). diff --git a/int.go b/int.go index 557cb1b..be23bd2 100644 --- a/int.go +++ b/int.go @@ -11,11 +11,6 @@ type Int struct { sql.NullInt64 } -// IntFrom creates a new Int that will always be valid. -func IntFrom(i int64) Int { - return NewInt(i, true) -} - // NewInt creates a new Int func NewInt(i int64, valid bool) Int { return Int{ @@ -26,9 +21,23 @@ func NewInt(i int64, valid bool) Int { } } +// IntFrom creates a new Int that will be null if zero. +func IntFrom(i int64) Int { + return NewInt(i, i != 0) +} + +// StringFrom creates a new String that be null if i is nil. +func IntFromPtr(i *int64) Int { + if i == nil { + return NewInt(0, false) + } + n := NewInt(*i, true) + return n +} + // UnmarshalJSON implements json.Unmarshaler. // It supports number and null input. -// 0 will not be considered a null Int. +// 0 will be considered a null Int. // It also supports unmarshalling a sql.NullInt64. func (i *Int) UnmarshalJSON(data []byte) error { var err error @@ -43,12 +52,12 @@ func (i *Int) UnmarshalJSON(data []byte) error { i.Valid = false return nil } - i.Valid = err == nil + i.Valid = (err == nil) && (i.Int64 != 0) return err } // UnmarshalText implements encoding.TextUnmarshaler. -// It will unmarshal to a null Int if the input is a blank or not an integer. +// It will unmarshal to a null Int if the input is a blank, zero, or not an integer. // It will return an error if the input is not an integer, blank, or "null". func (i *Int) UnmarshalText(text []byte) error { str := string(text) @@ -58,37 +67,39 @@ func (i *Int) UnmarshalText(text []byte) error { } var err error i.Int64, err = strconv.ParseInt(string(text), 10, 64) - i.Valid = err == nil + i.Valid = (err == nil) && (i.Int64 != 0) return err } // MarshalJSON implements json.Marshaler. // It will encode null if this Int is null. func (i Int) MarshalJSON() ([]byte, error) { + n := i.Int64 if !i.Valid { - return []byte("null"), nil + n = 0 } - return []byte(strconv.FormatInt(i.Int64, 10)), nil + return []byte(strconv.FormatInt(n, 10)), nil } // MarshalText implements encoding.TextMarshaler. -// It will encode a blank string if this Int is null. +// It will encode a zero if this Int is null. func (i Int) MarshalText() ([]byte, error) { + n := i.Int64 if !i.Valid { - return []byte{}, nil + n = 0 } - return []byte(strconv.FormatInt(i.Int64, 10)), nil + return []byte(strconv.FormatInt(n, 10)), nil } -// Pointer returns a pointer to this Int's value, or a nil pointer if this Int is null. -func (i Int) Pointer() *int64 { +// Ptr returns a pointer to this Int's value, or a nil pointer if this Int is null. +func (i Int) Ptr() *int64 { if !i.Valid { return nil } return &i.Int64 } -// IsZero returns true for invalid Ints, for future omitempty support (Go 1.4?) +// IsZero returns true for null or zero Ints, for future omitempty support (Go 1.4?) func (i Int) IsZero() bool { - return !i.Valid + return !i.Valid || i.Int64 == 0 } diff --git a/int_test.go b/int_test.go index eb6f5f8..cee723b 100644 --- a/int_test.go +++ b/int_test.go @@ -8,6 +8,7 @@ import ( var ( intJSON = []byte(`12345`) nullIntJSON = []byte(`{"Int64":12345,"Valid":true}`) + zeroJSON = []byte(`0`) ) func TestIntFrom(t *testing.T) { @@ -15,11 +16,21 @@ func TestIntFrom(t *testing.T) { assertInt(t, i, "IntFrom()") zero := IntFrom(0) - if !zero.Valid { - t.Error("IntFrom(0)", "is invalid, but should be valid") + if zero.Valid { + t.Error("IntFrom(0)", "is valid, but should be invalid") } } +func TestIntFromPtr(t *testing.T) { + n := int64(12345) + iptr := &n + i := IntFromPtr(iptr) + assertInt(t, i, "IntFromPtr()") + + null := IntFromPtr(nil) + assertNullInt(t, null, "IntFromPtr(nil)") +} + func TestUnmarshalInt(t *testing.T) { var i Int err := json.Unmarshal(intJSON, &i) @@ -31,6 +42,11 @@ func TestUnmarshalInt(t *testing.T) { maybePanic(err) assertInt(t, ni, "sq.NullInt64 json") + var zero Int + err = json.Unmarshal(zeroJSON, &zero) + maybePanic(err) + assertNullInt(t, zero, "zero json") + var null Int err = json.Unmarshal(nullJSON, &null) maybePanic(err) @@ -43,6 +59,11 @@ func TestTextUnmarshalInt(t *testing.T) { maybePanic(err) assertInt(t, i, "UnmarshalText() int") + var zero Int + err = zero.UnmarshalText([]byte("0")) + maybePanic(err) + assertNullInt(t, zero, "UnmarshalText() zero int") + var blank Int err = blank.UnmarshalText([]byte("")) maybePanic(err) @@ -60,11 +81,11 @@ func TestMarshalInt(t *testing.T) { maybePanic(err) assertJSONEquals(t, data, "12345", "non-empty json marshal") - // invalid values should be encoded as null + // invalid values should be encoded as 0 null := NewInt(0, false) data, err = json.Marshal(null) maybePanic(err) - assertJSONEquals(t, data, "null", "null json marshal") + assertJSONEquals(t, data, "0", "null json marshal") } func TestMarshalIntText(t *testing.T) { @@ -73,22 +94,22 @@ func TestMarshalIntText(t *testing.T) { maybePanic(err) assertJSONEquals(t, data, "12345", "non-empty text marshal") - // invalid values should be encoded as null + // invalid values should be encoded as zero null := NewInt(0, false) data, err = null.MarshalText() maybePanic(err) - assertJSONEquals(t, data, "", "null text marshal") + assertJSONEquals(t, data, "0", "null text marshal") } func TestIntPointer(t *testing.T) { i := IntFrom(12345) - ptr := i.Pointer() + ptr := i.Ptr() if *ptr != 12345 { t.Errorf("bad %s int: %#v ≠ %s\n", "pointer", ptr, 12345) } null := NewInt(0, false) - ptr = null.Pointer() + ptr = null.Ptr() if ptr != nil { t.Errorf("bad %s int: %#v ≠ %s\n", "nil pointer", ptr, "nil") } @@ -106,8 +127,8 @@ func TestIntIsZero(t *testing.T) { } zero := NewInt(0, true) - if zero.IsZero() { - t.Errorf("IsZero() should be false") + if !zero.IsZero() { + t.Errorf("IsZero() should be true") } } diff --git a/nuller/int.go b/nuller/int.go new file mode 100644 index 0000000..458b22e --- /dev/null +++ b/nuller/int.go @@ -0,0 +1,106 @@ +package null + +import ( + "database/sql" + "encoding/json" + "strconv" +) + +// Int is an even nuller nullable int64. +// It does not consider zero values to be null. +// It will decode to null, not zero, if null. +type Int struct { + sql.NullInt64 +} + +// NewInt creates a new Int +func NewInt(i int64, valid bool) Int { + return Int{ + NullInt64: sql.NullInt64{ + Int64: i, + Valid: valid, + }, + } +} + +// IntFrom creates a new Int that will always be valid. +func IntFrom(i int64) Int { + return NewInt(i, true) +} + +// StringFrom creates a new String that be null if i is nil. +func IntFromPtr(i *int64) Int { + if i == nil { + return NewInt(0, false) + } + n := NewInt(*i, true) + return n +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports number and null input. +// 0 will not be considered a null Int. +// It also supports unmarshalling a sql.NullInt64. +func (i *Int) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + json.Unmarshal(data, &v) + switch x := v.(type) { + case float64: + i.Int64 = int64(x) + case map[string]interface{}: + err = json.Unmarshal(data, &i.NullInt64) + case nil: + i.Valid = false + return nil + } + i.Valid = err == nil + return err +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null Int if the input is a blank or not an integer. +// It will return an error if the input is not an integer, blank, or "null". +func (i *Int) UnmarshalText(text []byte) error { + str := string(text) + if str == "" || str == "null" { + i.Valid = false + return nil + } + var err error + i.Int64, err = strconv.ParseInt(string(text), 10, 64) + i.Valid = err == nil + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this Int is null. +func (i Int) MarshalJSON() ([]byte, error) { + if !i.Valid { + return []byte("null"), nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// MarshalText implements encoding.TextMarshaler. +// It will encode a blank string if this Int is null. +func (i Int) MarshalText() ([]byte, error) { + if !i.Valid { + return []byte{}, nil + } + return []byte(strconv.FormatInt(i.Int64, 10)), nil +} + +// Ptr returns a pointer to this Int's value, or a nil pointer if this Int is null. +func (i Int) Ptr() *int64 { + if !i.Valid { + return nil + } + return &i.Int64 +} + +// IsZero returns true for invalid Ints, for future omitempty support (Go 1.4?) +// A non-null Int with a 0 value will not be considered zero. +func (i Int) IsZero() bool { + return !i.Valid +} diff --git a/nuller/int_test.go b/nuller/int_test.go new file mode 100644 index 0000000..f9fc4ec --- /dev/null +++ b/nuller/int_test.go @@ -0,0 +1,149 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + intJSON = []byte(`12345`) + nullIntJSON = []byte(`{"Int64":12345,"Valid":true}`) +) + +func TestIntFrom(t *testing.T) { + i := IntFrom(12345) + assertInt(t, i, "IntFrom()") + + zero := IntFrom(0) + if !zero.Valid { + t.Error("IntFrom(0)", "is invalid, but should be valid") + } +} + +func TestIntFromPtr(t *testing.T) { + n := int64(12345) + iptr := &n + i := IntFromPtr(iptr) + assertInt(t, i, "IntFromPtr()") + + null := IntFromPtr(nil) + assertNullInt(t, null, "IntFromPtr(nil)") +} + +func TestUnmarshalInt(t *testing.T) { + var i Int + err := json.Unmarshal(intJSON, &i) + maybePanic(err) + assertInt(t, i, "int json") + + var ni Int + err = json.Unmarshal(nullIntJSON, &ni) + maybePanic(err) + assertInt(t, ni, "sq.NullInt64 json") + + var null Int + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullInt(t, null, "null json") +} + +func TestTextUnmarshalInt(t *testing.T) { + var i Int + err := i.UnmarshalText([]byte("12345")) + maybePanic(err) + assertInt(t, i, "UnmarshalText() int") + + var blank Int + err = blank.UnmarshalText([]byte("")) + maybePanic(err) + assertNullInt(t, blank, "UnmarshalText() empty int") + + var null Int + err = null.UnmarshalText([]byte("null")) + maybePanic(err) + assertNullInt(t, null, `UnmarshalText() "null"`) +} + +func TestMarshalInt(t *testing.T) { + i := IntFrom(12345) + data, err := json.Marshal(i) + maybePanic(err) + assertJSONEquals(t, data, "12345", "non-empty json marshal") + + // invalid values should be encoded as null + null := NewInt(0, false) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, "null", "null json marshal") +} + +func TestMarshalIntText(t *testing.T) { + i := IntFrom(12345) + data, err := i.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "12345", "non-empty text marshal") + + // invalid values should be encoded as null + null := NewInt(0, false) + data, err = null.MarshalText() + maybePanic(err) + assertJSONEquals(t, data, "", "null text marshal") +} + +func TestIntPointer(t *testing.T) { + i := IntFrom(12345) + ptr := i.Ptr() + if *ptr != 12345 { + t.Errorf("bad %s int: %#v ≠ %s\n", "pointer", ptr, 12345) + } + + null := NewInt(0, false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s int: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestIntIsZero(t *testing.T) { + i := IntFrom(12345) + if i.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := NewInt(0, false) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } + + zero := NewInt(0, true) + if zero.IsZero() { + t.Errorf("IsZero() should be false") + } +} + +func TestIntScan(t *testing.T) { + var i Int + err := i.Scan(12345) + maybePanic(err) + assertInt(t, i, "scanned int") + + var null Int + err = null.Scan(nil) + maybePanic(err) + assertNullInt(t, null, "scanned null") +} + +func assertInt(t *testing.T, i Int, from string) { + if i.Int64 != 12345 { + t.Errorf("bad %s int: %d ≠ %d\n", from, i.Int64, 12345) + } + if !i.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullInt(t *testing.T, i Int, from string) { + if i.Valid { + t.Error(from, "is valid, but should be invalid") + } +} diff --git a/nuller/string.go b/nuller/string.go new file mode 100644 index 0000000..1206459 --- /dev/null +++ b/nuller/string.go @@ -0,0 +1,87 @@ +// Package null provides an opinionated yet reasonable way of handling null values. +package null + +import ( + "database/sql" + "encoding/json" +) + +// String is an even nuller nullable string. +type String struct { + sql.NullString +} + +// StringFrom creates a new String that will never be blank. +func StringFrom(s string) String { + return NewString(s, true) +} + +// StringFrom creates a new String that be null if s is nil. +func StringFromPtr(s *string) String { + if s == nil { + return NewString("", false) + } + str := NewString(*s, true) + return str +} + +// NewString creates a new String +func NewString(s string, valid bool) String { + return String{ + NullString: sql.NullString{ + String: s, + Valid: valid, + }, + } +} + +// UnmarshalJSON implements json.Unmarshaler. +// It supports string and null input. Blank string input produces a null String. +// It also supports unmarshalling a sql.NullString. +func (s *String) UnmarshalJSON(data []byte) error { + var err error + var v interface{} + json.Unmarshal(data, &v) + switch x := v.(type) { + case string: + s.String = x + case map[string]interface{}: + err = json.Unmarshal(data, &s.NullString) + case nil: + s.Valid = false + return nil + } + s.Valid = (err == nil) && (s.String != "") + return err +} + +// MarshalJSON implements json.Marshaler. +// It will encode null if this String is null. +func (s String) MarshalJSON() ([]byte, error) { + if !s.Valid { + return []byte("null"), nil + } + return json.Marshal(s.String) +} + +// UnmarshalText implements encoding.TextUnmarshaler. +// It will unmarshal to a null String if the input is a blank string. +func (s *String) UnmarshalText(text []byte) error { + s.String = string(text) + s.Valid = s.String != "" + return nil +} + +// Ptr returns a pointer to this String's value, or a nil pointer if this String is null. +func (s String) Ptr() *string { + if !s.Valid { + return nil + } + return &s.String +} + +// IsZero returns true for null or empty strings, for future omitempty support. (Go 1.4?) +// Will return false s is blank but non-null. +func (s String) IsZero() bool { + return !s.Valid +} diff --git a/nuller/string_test.go b/nuller/string_test.go new file mode 100644 index 0000000..3b5b07a --- /dev/null +++ b/nuller/string_test.go @@ -0,0 +1,172 @@ +package null + +import ( + "encoding/json" + "testing" +) + +var ( + stringJSON = []byte(`"test"`) + blankStringJSON = []byte(`""`) + nullStringJSON = []byte(`{"String":"test","Valid":true}`) + nullJSON = []byte(`null`) +) + +type stringInStruct struct { + Test String `json:"test,omitempty"` +} + +func TestStringFrom(t *testing.T) { + str := StringFrom("test") + assertStr(t, str, "StringFrom() string") + + zero := StringFrom("") + if !zero.Valid { + t.Error("StringFrom(0)", "is invalid, but should be valid") + } +} + +func TestStringFromPtr(t *testing.T) { + s := "test" + sptr := &s + str := StringFromPtr(sptr) + assertStr(t, str, "StringFromPtr() string") + + null := StringFromPtr(nil) + assertNullStr(t, null, "StringFromPtr(nil)") +} + +func TestUnmarshalString(t *testing.T) { + var str String + err := json.Unmarshal(stringJSON, &str) + maybePanic(err) + assertStr(t, str, "string json") + + var ns String + err = json.Unmarshal(nullStringJSON, &ns) + maybePanic(err) + assertStr(t, ns, "sql.NullString json") + + var blank String + err = json.Unmarshal(blankStringJSON, &blank) + maybePanic(err) + assertNullStr(t, blank, "blank string json") + + var null String + err = json.Unmarshal(nullJSON, &null) + maybePanic(err) + assertNullStr(t, null, "null json") +} + +func TestTextUnmarshalString(t *testing.T) { + var str String + err := str.UnmarshalText([]byte("test")) + maybePanic(err) + assertStr(t, str, "UnmarshalText() string") + + var null String + err = null.UnmarshalText([]byte("")) + maybePanic(err) + assertNullStr(t, null, "UnmarshalText() empty string") +} + +func TestMarshalString(t *testing.T) { + str := StringFrom("test") + data, err := json.Marshal(str) + maybePanic(err) + assertJSONEquals(t, data, `"test"`, "non-empty json marshal") + + // empty values should be encoded as an empty string + zero := StringFrom("") + data, err = json.Marshal(zero) + maybePanic(err) + assertJSONEquals(t, data, `""`, "empty json marshal") + + null := StringFromPtr(nil) + data, err = json.Marshal(null) + maybePanic(err) + assertJSONEquals(t, data, `null`, "null json marshal") +} + +// Tests omitempty... broken until Go 1.4 +// func TestMarshalStringInStruct(t *testing.T) { +// obj := stringInStruct{Test: StringFrom("")} +// data, err := json.Marshal(obj) +// maybePanic(err) +// assertJSONEquals(t, data, `{}`, "null string in struct") +// } + +func TestStringPointer(t *testing.T) { + str := StringFrom("test") + ptr := str.Ptr() + if *ptr != "test" { + t.Errorf("bad %s string: %#v ≠ %s\n", "pointer", ptr, "test") + } + + null := NewString("", false) + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s string: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestStringIsZero(t *testing.T) { + str := StringFrom("test") + if str.IsZero() { + t.Errorf("IsZero() should be false") + } + + blank := StringFrom("") + if blank.IsZero() { + t.Errorf("IsZero() should be false") + } + + empty := NewString("", true) + if empty.IsZero() { + t.Errorf("IsZero() should be false") + } + + null := StringFromPtr(nil) + if !null.IsZero() { + t.Errorf("IsZero() should be true") + } +} + +func TestStringScan(t *testing.T) { + var str String + err := str.Scan("test") + maybePanic(err) + assertStr(t, str, "scanned string") + + var null String + err = null.Scan(nil) + maybePanic(err) + assertNullStr(t, null, "scanned null") +} + +func maybePanic(err error) { + if err != nil { + panic(err) + } +} + +func assertStr(t *testing.T, s String, from string) { + if s.String != "test" { + t.Errorf("bad %s string: %s ≠ %s\n", from, s.String, "test") + } + if !s.Valid { + t.Error(from, "is invalid, but should be valid") + } +} + +func assertNullStr(t *testing.T, s String, from string) { + if s.Valid { + t.Error(from, "is valid, but should be invalid") + } +} + +func assertJSONEquals(t *testing.T, data []byte, cmp string, from string) { + if string(data) != cmp { + t.Errorf("bad %s data: %s ≠ %s\n", from, data, cmp) + } +} diff --git a/string.go b/string.go index 6e54c52..e951cd7 100644 --- a/string.go +++ b/string.go @@ -11,11 +11,6 @@ type String struct { sql.NullString } -// StringFrom creates a new String that will be null if s is blank. -func StringFrom(s string) String { - return NewString(s, s != "") -} - // NewString creates a new String func NewString(s string, valid bool) String { return String{ @@ -26,6 +21,21 @@ func NewString(s string, valid bool) String { } } +// StringFrom creates a new String that will be null if s is blank. +func StringFrom(s string) String { + return NewString(s, s != "") +} + +// StringFrom creates a new String that be null if s is nil or blank. +// It will make s point to the String's value. +func StringFromPtr(s *string) String { + if s == nil { + return NewString("", false) + } + str := NewString(*s, *s != "") + return str +} + // UnmarshalJSON implements json.Unmarshaler. // It supports string and null input. Blank string input produces a null String. // It also supports unmarshalling a sql.NullString. @@ -64,7 +74,7 @@ func (s *String) UnmarshalText(text []byte) error { } // Pointer returns a pointer to this String's value, or a nil pointer if this String is null. -func (s String) Pointer() *string { +func (s String) Ptr() *string { if !s.Valid { return nil } diff --git a/string_test.go b/string_test.go index abba506..9e0d8b7 100644 --- a/string_test.go +++ b/string_test.go @@ -81,13 +81,29 @@ func TestMarshalString(t *testing.T) { func TestStringPointer(t *testing.T) { str := StringFrom("test") - ptr := str.Pointer() + ptr := str.Ptr() if *ptr != "test" { t.Errorf("bad %s string: %#v ≠ %s\n", "pointer", ptr, "test") } null := StringFrom("") - ptr = null.Pointer() + ptr = null.Ptr() + if ptr != nil { + t.Errorf("bad %s string: %#v ≠ %s\n", "nil pointer", ptr, "nil") + } +} + +func TestStringFromPointer(t *testing.T) { + test := "test" + testptr := &test + str := StringFromPtr(testptr) + assertStr(t, str, "StringFromPtr()") + + testptr = nil + null := StringFromPtr(testptr) + assertNullStr(t, null, "StringFromPtr()") + + ptr := null.Ptr() if ptr != nil { t.Errorf("bad %s string: %#v ≠ %s\n", "nil pointer", ptr, "nil") }