[HN Gopher] Go Nulls and SQL
___________________________________________________________________
Go Nulls and SQL
Author : mnvrth
Score : 34 points
Date : 2022-05-22 11:43 UTC (1 days ago)
(HTM) web link (ente.io)
(TXT) w3m dump (ente.io)
| tedunangst wrote:
| > Pointers are easy first, but then you realize that you have to
| put nilness checks everywhere.
|
| Well, yeah, if something may or may not be there, you have to
| check before you use it. How else do people want it to work?
| solar-ice wrote:
| In some languages, the concept of "may or may not be there" is
| cleanly separated from "exists in a separate block of memory".
| There's plenty of pointers which will never be nil - checking
| that they're nil at the entry and exit points of every function
| is line noise.
| throwaway894345 wrote:
| I don't think the parent was advocating for checking for nil
| before/after each function, but rather noting that this
| option pattern doesn't buy you any more type safety in Go
| because there's nothing enforcing you to check appropriately
| any more than there is for a pointer.
|
| The salient rebuttal is that this pattern is a strong hint to
| check, whereas a pointer is ambiguous (it's unclear whether
| or not a bare pointer may ever be nil, but you would only use
| this pattern to be explicit that a check is needed). This
| pattern can also support value types so you don't have to
| worry as much about unnecessary allocations.
| xdfgh1112 wrote:
| Sane languages have pointers which either can't be null or
| require you to check explicitly. Rather than leaving it to
| explode at runtime.
| tedunangst wrote:
| Tell me more about these unexplodable languages.
| adastra22 wrote:
| Rust being the popular example. But even C++ has this with
| std::optional, for example.
| tedunangst wrote:
| So rust doesn't explode if I unwrap None?
| Gwypaas wrote:
| You're explicitly choosing to panic if it is None. Would
| never pass a code review unless you can in very clear
| terms tell why it will never now or in the future be
| none.
|
| The point is that you can't use the inner value without
| choosing a course of action if it happens to be none.
| stouset wrote:
| Only if you want it to.
| #[derive(Default)] struct Foo { ...
| } struct Bar { ... }
| fn do_something(foo: Foo) -> Bar { # do
| something with Foo and return a Bar ...
| } fn main() { let opt_foo:
| Option<Foo> = None; // panics
| let foo: Foo = opt_foo.unwrap(); //
| panics, but with an error message of your choosing
| let foo: Foo = opt_foo.expect("foo was supposed to be
| there"); // converts the Option (Some or
| None) to a Result (Ok or Err, // where Err
| can contain an error type or message and then passed
| // around or returned or whatever) let foo:
| Result<Foo> = opt_foo.ok_or("foo was supposed to be
| there"); // replaces it with a value of
| your choosing if it's not there let foo: Foo
| = opt_foo.unwrap_or(Foo { ... }); //
| replaces it with the default value the type defines
| let foo: Foo = opt_foo.unwrap_or_default();
| // keeps it as `None`, or if it's actually something then
| // replaces the internal contents with the result of the
| // function call let bar: Option<Bar> =
| opt_foo.map(do_something); // does
| arbitrary logic in the match arm if foo is there
| match opt_foo { Some(foo) => { do
| something with foo }, None => { do
| something else }, } }
|
| There are a few dozen other things you can do with an
| Option that handle the rarer use-cases, but the above are
| like 95%+ of what you want.
| avgcorrection wrote:
| The comment that you respondend to:
|
| > > Sane languages have pointers which either can't be
| null or require you to check explicitly.
|
| You cannot try to dereference a nullable pointer in safe
| Rust. It has got nothing to do with Optional (Some/None).
| tedunangst wrote:
| That would make a rust pointer rather less useful for
| representing sql null, no?
| avgcorrection wrote:
| What?
|
| The comment that you -- with aloof disbelief -- replied
| to only talked about pointers. I don't know what point
| you think you are proving with these pithy replies. It
| sure does go above my head. :)
| tedunangst wrote:
| Well, this is a thread about storing sql null. I'm trying
| to suss out how sane languages represent sql null in a
| way that can't go wrong. So far the answers mostly seem
| to be use a type that can't represent sql null or use a
| type that explodes when you use it wrong. With a side of
| real programmers don't let bugs pass code review.
| stouset wrote:
| With all due respect, it appears as if you have _entered_
| this conversation with this belief and have used that
| perspective as a filter when other commenters have tried
| to correct that perspective.
|
| It may be surprising to learn that languages without
| implicitly-nullable types are real, but you should
| understand that none of these people would be trying to
| tell you about this if these languages simply exploded
| any time you used them the wrong way.
| avgcorrection wrote:
| > I'm trying to [sass] out how sane languages represent
| sql null in a way that can't go wrong.
|
| There is a difference between a type Pointer where `null`
| is an instance of that type and a type Box which only has
| valid (can be dereferenced) values. If the language has
| e.g. algebraic data types and pattern matching then
| Option(al)<Box> can be safely matched on and Box can be
| used in the branch (the pattern match arm) where
| Some(Box). Meanwhile in a language with Pointer and no
| flow analysis any dereference of Pointer could lead to
| some kind of "panic".
|
| Say "unwrap()" all you want but the two approaches are
| clearly very different.
| giraffe_lady wrote:
| Maybe Some other time.
| worik wrote:
| You cannot use a null pointer in Rust.
|
| I am a Rust novice but I have not found a way to get to
| uninitialised memory in safe Rust. Yet.
| tedunangst wrote:
| How do you create a pointer to uninitialized memory in
| go?
| TheDong wrote:
| All it takes is a specially formatted comment
| /* int g() { volatile int x;
| return x; } */ import "C"
| import "fmt" func main() {
| fmt.Printf("%d\n", C.g()) }
|
| But I know you'll say that 'import "C"' or 'import
| "unsafe"' is the same thing as using an unsafe block in
| rust or such, and really shouldn't count against go.
|
| Which is fair and true, but you're chasing down a
| pointless detail. The point isn't that go is memory
| unsafe. It's not. The point is that Go's type-system is
| not powerful enough to express various types of type-
| safety, and as such it's an error-prone language where
| you can expect null pointer exceptions frequently.
| stouset wrote:
| You can't. But you can absolutely have a `nil` that will
| unavoiad runtime panics if you don't check it. And
| further, you only can't do this because go _defines by
| fiat_ that the zero-value of a type must be legal. Of
| course, this is wildly inconvenient and annoying for many
| types, including those that come bundled in the standard
| library.
|
| There are other, very convincingly better options to this
| that many in this thread have been trying to teach you
| about in the expectation that your responses have been in
| good faith.
| Gwypaas wrote:
| You can't. But a runtime panic also very easily causes an
| outage. At least it's not as subtle.
|
| Still. Rust is infinitely better in this regard. Go feels
| like a crude hammer, especially regarding the default
| interaction between deserialization and missing struct
| members.
| dtech wrote:
| If this question is in good faith, for example Kotlin
| differentiates between the nullable T? and the non-nullable
| T. An if x != null check automatically casts T? to T. You
| can't call a method on T?, preventing null pointer
| exceptions. C# and Typescript have similar language
| support. Other languages push users away from the problem
| by favoring monadic Optional/Maybe solutions.
| tedunangst wrote:
| There are some differences here, but you can't for
| instance use a string pointer with string functions in go
| either. IMO The distinction between pointer and optional
| is not as vast as people make it out to be.
|
| (But thanks, I overlooked T? conversions inside if.)
| akavi wrote:
| The distinction between pointer and optional is not vast.
| The distinction between pointer and _not_ optional is.
|
| The whole idea of optional is to make _not_ optional
| possible.
| morelisp wrote:
| You can call a value method on a nil pointer and it
| panics, though, which I think was a mistake.
| adastra22 wrote:
| T? is just syntactic sugar for Optional/Maybe, no?
| stouset wrote:
| Type systems can enforce that types always contain a value
| of that type and _cannot be null_. There is another type
| that is either null _or_ a value of the enclosed type.
|
| At any boundary where something might be null, you do the
| null check. If it's null, you do whatever logic is
| necessary _right there and only right there_. That might be
| to skip the computation, to use a default value, to get the
| value from somewhere else, to return an error, or whatever.
| If it 's _not_ null, you use the internal value and from
| then on every user can operate on a guarantee that it 's
| not null.
| [deleted]
| AtNightWeCode wrote:
| Maybe I passed out among the nonsense but most langs provide a
| dbnull constant to check against, no?
| jrockway wrote:
| You could do that in Go, but it's not what database/sql's API
| supports. You read columns out of each row with
| "row.Scan(&col1, &col2, ...)". The types of col1 and col2 are
| declared at compile time, and they don't have to be able to
| represent the concept of null. So there would be no way to
| store the state that represents that something was null.
|
| You could of course have an API that just returns a slice of
| "any", and conditionally check whether a value is of type
| "string" or "mylibrary.NullValue" after the fact. This isn't
| clearly better to me than the Scan API. You are going to have
| to eventually cast "any" to a real type in order to use it;
| with Scan the library does that for you.
|
| Your own types can implement sql.Scanner to control exactly how
| you want to handle something. (Indeed, your "Scan" method
| receives something with type "any", and you need to check what
| the type is and convert it to your internal representation.)
|
| Also wanted to throw this out here; you don't have to be
| satisfied with lossy versions of your database's built in
| types. Libraries like
| https://pkg.go.dev/github.com/jackc/pgtype@v1.11.0 will give
| you a 1:1 mapping to Postgres's types. (I'm sure other database
| systems have similar libraries.)
| davidkuennen wrote:
| Using go and postgres for my App's backend.
|
| After using NULLs this way at first, I noticed it's generally
| much much easier for me to just avoid nullable SQL columns
| wherever possible (it was always possible so far). Most of the
| time there is a much easier was to say a value is empty. For
| strings '' for example.
|
| This seriously made everything so much easier. Not necessarily
| anything to do with go tho.
| SoftTalker wrote:
| I think this is generally good, unless your database treats
| NULL and '' as the same thing.... e.g. Oracle.
| eknkc wrote:
| So, to avoid checking for null, you'll check for
| `NullString.Valid` now? The string pointer is a part of the
| language. You can pass it around, expose as part of a library
| etc. And it conveys the intent perfectly.
|
| I have no idea what is the issue here?
| psanford wrote:
| The nice thing about the Null* type is they can reduce the
| number of allocations done on the heap and thus also reduce
| total GC your program needs to do.
| legorobot wrote:
| I haven't thought about it that way! I've not hit the GC as a
| performance wall when it comes to accessing nullable DB
| values yet. Thanks for the insight.
| throwaway894345 wrote:
| It's generally useful, not just in SQL. Any time you have a
| type that could be multiple things, you have to choose
| between a reference-based implementation (e.g., interfaces)
| or a tagged struct (a struct with a flag field that tells
| you what its runtime type). The tagged struct version is
| guaranteed not to allocate, while the interface version
| very likely will. Most of the time it will only matter if
| you're in a tight loop.
| legorobot wrote:
| I think the idea is `sql.NullString` can be used to have a SQL
| NULL still be an empty string in Go (just avoid checking the
| `Valid` field, or cast to string -- no check necessary).
|
| It seems like the intent of a string pointer vs.
| `sql.NullString` is their goal with this type anyways[1]?
|
| In practice I've used a string pointer when I need a nullable
| value, or enforce `NOT NULL` in the DB if we can't/don't want
| to handle null values. Use what works for you, and keep the DB
| and code's expectations in-sync.
|
| [1]: https://groups.google.com/g/golang-
| nuts/c/vOTFu2SMNeA/m/GB5v...
| jackielii wrote:
| I have an alternative solution to work with an existing struct
| that you can't change, e.g. protobuf generated code:
| https://jackieli.dev/posts/pointers-in-go-used-in-sql-scanne...
| type nullString struct { s *string }
| func (ts nullString) Scan(value interface{}) error { if
| value == nil { *ts.s = "" // nil to empty
| return nil } switch t := value.(type) {
| case string: *ts.s = t default:
| return fmt.Errorf("expect string in sql scan, got: %T", value)
| } return nil } func (n *nullString)
| Value() (driver.Value, error) { if n.s == nil {
| return "", nil } return *n.s, nil }
|
| Then use it: var node struct { Name string }
| db.QueryRow("select name from node where id=?",
| id).Scan(nullString(&node.Name))
| Mawr wrote:
| Great, except:
|
| 1. The type is merely a weak hint that you should check .Valid
| before you use the value, there's no enforcement:
| https://go.dev/play/p/nS8RxGujMBk
|
| It's much better when the struct members are private and the only
| way to access the value is through a method that returns (value,
| bool): if value, ok := optional.Get(); ok {
| // value is valid } else { // value is invalid
| }
|
| This is a strong hint that you should check the bool before using
| the value. It's also a common go pattern - checking for existence
| of a key in a map is done this way.
|
| 2. We have generics now. Why use type-and-sql-specific wrappers
| when you could use a generic Option? Example implementation:
| https://gist.github.com/MawrBF2/0a60da26f66b82ee87b98b03336e....
| psadauskas wrote:
| Your comment is helpful, so this sarcasm isn't directed at you.
| However, this looks like an extremely cumbersome way to wish
| your language had the Result monad.
| brushyamoeba wrote:
| I'm surprised this doesn't discuss JSON marshalling
___________________________________________________________________
(page generated 2022-05-23 23:01 UTC)