Go nulls and SQL
ente.io
ente.io
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....
It’s also something I realized was lost in the JS progression towards Promise APIs and eventually async/await. Promises have ergonomic benefits over callback hell, but damn if the Node convention of cb: (error, result) => { /* … */ } wasn’t a Result type sitting there begging to be embraced. Again, not a Monad, but it’s a shame the good API design was thrown out with the inconvenient API design.
I'd say the opposite. I'd say it beats the hell out of Rust syntax, as Rust would force you to have staircase code here. No thank you.
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?
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.
(But thanks, I overlooked T? conversions inside if.)
The whole idea of optional is to make not optional possible.
The billion dollar mistake is overblown?
I actually like the language support more, as it flows a lot better than the (flat)map chain you get in monadic style. Similar to async/await v.s. Future.
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.
> Tell me more about these unexplodable languages.
I don't think anyone else mentioned an entire language being unexplodable, just a pointer type.
Later on the thread you talk about `Option` in Rust, which is not what I think most people would consider a pointer type, but even if it is, it's certainly not the only one. I think GP was talking about the basic reference type (i.e. `&T`), which will not ever be null in safe code, so dereferencing it will not explode.
#[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.The point is that you can't use the inner value without choosing a course of action if it happens to be none.
> > 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).
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. :)
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.
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.
Yes, Rust options explode if you unwrap a None. But that's explicitly what `unwrap()` does. It a big improvement over any random access potentially implicitly exploding.
And in turn, the claim above was that some languages have plain pointers that aren't dangerous.
Yes, you can put a pointer into an Option that can fail, but that's a different issue. The point is that "can fail" is not built directly into the pointer type, and you can use pointers that don't have that risk.
And to be clear, that claim is not directly addressing the issue of SQL null. It's a baseline discussion of what pointers actually imply by nature of being pointers. We can then better build upon that knowledge after we separate "refers to a data location" and "might not have contents" into separate attributes.
I am a Rust novice but I have not found a way to get to uninitialised memory in safe Rust. Yet.
Yes you can: https://doc.rust-lang.org/std/ptr/fn.null.html
You just won't use it unless you're doing FFI or similar things.
I meant "using null pointer" means dereference it. But careless language....
/*
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.
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.
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.
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.
Except inner joins and the like, I guess.
I hear this advice quite often, but that doesn't relief you of handling NULL values. E.g. a NOT NULL column can be NULL in a result of an outer join.
I have no idea what is the issue here?
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...
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))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.)