In an earlier post (
http://groups.google.com/group/golang-nuts/
browse_thread/thread/e770ee059b9ac961) I looked carefully at the idea
of static null safety -- catching all potential null dereferences at
compile-time -- and concluded that it was unworkable for a
constructorless, imperative language like Go, because of
initialization problems. Here is a weaker proposal, that I think
ultimately also fails. A lot of what I'm saying here was discussed in
the thread "Proposal for non-nil pointers and alias" (http://
groups.google.com/group/golang-nuts/browse_thread/thread/
da0f4c6dee193829), so you can view this post as a summary of that
thread.
The idea is to have a type modifier used on formals and return types
that acts like an assertion of non-nullity. In detail:
Let a nullable type be any type which has nil as a possible value --
pointers, maps, channels, slices, functions and interfaces.
If a "!" appears prior to a nullable type in a formal parameter list,
the compiler guarantees that the corresponding variables are not null
when they are first referenced in the function, and never become null.
If a return value type has "!" prepended, then the compiler guarantees
that the function will not return nil in that return-value position.
The "!" can also prepend a nullable type in a variable declaration
with initialization, with the guarantee that the initialization value
will not be null. The "!" modifier is not allowed anywhere else.
Example:
func foo(p !*T) !*T {
p.x++;
return p
}
The obvious implementation is for the compiler to prove that the value
is non-null using these type annotations, or insert a runtime check if
it cannot do the proof. For example, in the above, no check is needed
on the return, since the compiler knows p cannot be null from the
formal declaration. Similarly, if foo is called like this:
var q !*T = new(T);
foo(q)
then no check is needed. But if instead it is called like this:
func bar(q *T) { foo(q); }
then a null check would be silently inserted at the call site.
Advantages of this scheme: it has no initialization problems; it works
for any nullable type, not just pointers; it introduces minimal code
clutter; you can completely ignore it if you don't like it; it is
backwards-compatible with the existing language; the additional
compiler complexity is small. It makes no attempt to get rid of null
pointers or the exceptions they engender, but it does push those
errors back to the interface between nullable and non-nullable, and it
allows one to write arbitrarily large swaths of null-safe code.
An example:
Let's rewrite the List data structure to use this, e.g.:
func New() !*List { return new(List).Init() }
func (l !*List) Front() *Element { return l.front }
Now I can use lists in a null-safe way with no runtime overhead:
lst := list.New();
... lst.Front() ...
Here the compiler gives lst the type !*List, because that what
list.New returns. That looks pretty good.
Now here is why no one will use this feature:
Say I have a list field in a struct:
type S struct { lst *List, ... }
I cannot write !*List above, because "!" cannot appear in that
position (due to the initialization problem).
Now whenever I use this list, I incur a runtime check:
s := S{list.New(), ...}; // Information about non-nullity lost here
.... s.lst.Front() ... // silent runtime check
Will this runtime check affect the performance of the program? Hard to
say. A typical profiler won't be able to tell you, because the check
is just a couple of inline instructions at the call, and it is
pervasive -- at every call. A profiler will show every function that
uses S.lst as running a bit slower than it might have -- but of course
that's not a pattern you can detect.
So what will a performance-sensitive programmer do? She will copy
list.go to fastlist.go, remove the "!"s, and use fastlist everywhere.
(And she is unlikely to switch back even after discovering that there
was no speedup.) Pretty soon, you will only see the list package used
in tutorials and in training new Go programmers. The trainees will
switch to fastlist the moment someone mocks their newbie ways.
I should also mention the variation on this proposal that requires an
explicit conversion when going from nullable to non-nullable. This
would introduce so much code clutter that it wouldn't even be taught
to newbies.