2013-03-03
Why a netcat-like program is a good test of a language
When I talked about my first Go experience, I mentioned in passing that a netcat-like program is actually not a bad test program for a language (or for certain sorts of libraries in, eg C). Today I feel like explaining that.
To start with, it's not an empty and artificial challenge; a netcat-like
program does something meaningful and practical (although it may not
be necessary if you already have netcat). The problem itself touches
many levels of a language and its library, since it has to interact with
standard input and output, deal with command line arguments, look up
hostnames and ports, make network connections and talk over them, and
deal with buffering and byte input and output. It also involves some
level of network concurrency, either through real concurrency (as in
Go) or through the equivalent with select()
,
poll()
, or the like. There are also some subtle and taxing aspects
to the problem, such as shutdown()
, that test whether the language
(and library) designers were paying attention or thought it worthwhile
to expose the entire underlying system API.
(In a low-level language like C you'll also wind up exploring things
like memory allocation and any safe buffer handling libraries that are
available. If you're working with select()
et al you can also extend
the problem to playing around with nonblocking IO, again if the language
gives you access to this.)
Of course there are many aspects to a language and its libraries beyond relatively low level networking, so this problem doesn't come anywhere near to exploring all of a language and its libraries. Still, I've found that it covers a lot of ground that's interesting to me personally and the whole experience is a good way of seeing what the language feels like.
Some people will want to write HTTP-based test programs instead because that's more directly relevant to them. I'm the kind of cynical person who wants to see the low-level plumbing in action too, partly because I think it's more revealing of the language's core attitudes. Since the web is so pervasive and important, my feeling is that everyone doing a new language environment is going to make sure they have good HTTP support (assuming they care about such usability at all). And if a language doesn't have either high-level HTTP support or good low-level networking support, well, that tells me a lot about its priorities.
Go: when I'd extend an interface versus making a new one
One of the reddit suggestions in response to my entry on using
type assertions to reach through interfaces
noted
that you could embed one interface inside another one, effectively
extending the interface that you embed, so my Closer
interface
could have been:
type ConnCloser interface { net.Conn CloseWrite() error }
When I saw this my instinctive reaction was that this was wrong for my situation; since then I've spent some time thinking about why I feel that way. My conclusion is that I think I have good reasons but I may be wrong.
Simplifying, the dividing point for me is whether all of the values I'm
dealing with would be instances of the new interface, for example if
I was writing code that only dealt with TCP and Unix stream sockets.
In that situation my life would be simpler if I immediately converted
the net.Conn
values into ConnCloser
values and then had the rest of
my code deal with the latter (freely calling .CloseWrite()
when it
wanted to). What I'm doing is converting net.Conn
values into what
they really are, which is values that have a wider interface.
But if not all of the values I'm dealing with are convertible and if
I'm only doing the conversion in one spot (and only once), extending
net.Conn
doesn't feel like an accurate description of what I'm doing.
I'm just fishing through it to see if I can call another routine and
then immediately calling that routine. Using just an interface with
CloseWrite()
makes my actual intentions clear.
I'd feel different if I was passing the converted values around between
functions or storing them in something. The issue here is that such
functions don't really want to accept anything that simply has a
CloseWrite()
method with the right signature; they want to deal
specifically with net.Conn
values that also have that method. A bare
Closer
interface that only specifies a CloseWrite()
method is too
broad an allowance for what I actually mean and thus would be the wrong
approach. (At this point I start waving my hands vaguely.)
The more I think about it the less I'm sure what proper Go style
should be here, and I have to admit that part of my feelings against
ConnCloser
are based purely on it having another line that doesn't do
anything in my original situation (I'm often a terseness person).