I've been working with net.IP a lot recently, and I've got some questions about the design choices behind it, and generally I'd like to have a discussion about it (because discussions are interesting, and grokking is fun, and whatnot).
net.IP is defined as:
type IP []byte
This allows that IPv4 addresses (which are 4 bytes long) and IPv6 addresses (which are 16 bytes long) can be represented using the same type. My guess is that the reason for doing this is that it allows the other types in the package which contain an IP address (such as UDPAddr and TCPAddr) and functions which accept or return IP addresses don't have to be duplicated to take separate IPv4 and IPv6 address types.
However, this introduces a lot of really annoying behavior. It means, first off, that two addresses cannot be compared for equality using "==". Second, it means that IP addresses cannot be used as may keys. Third, it means that code cannot be written which, by type contract, expects solely IPv4 or solely IPv6 addresses. Fourth, it means that if one or the other type is expected, this contract is not enforced, and it requires the programmer to either introduce an error condition or a panic condition (for example, by calling addr.To4() or addr.To6(), which respectively convert the addr to the given form, and return nil if addr is not the corresponding version - using the address would then panic if it was originally the wrong type since it'd be nil after calling addr.ToN()). Fifth, if all of these explicit checks are not performed, it can introduce annoying bugs (for example, the code I've been writing recently has often accidentally propagated addresses "0.0.0.0" since I neglected to explicitly call addr.To4(), meaning that when I accessed the first four bytes of addr, they were 0, since addr was 16 bytes and the 4 bytes of the address were stored from bytes 11 through 15). Sixth, it means that, since addresses are passed by reference, they cannot be safely stored without being copied for fear that, after returning, the caller may modify the underlying bytes (or vice versa for the callee modifying the bytes during the call)
As I see it, a much nicer way of addressing the concern that types and functions need to be able to accept arbitrary IP types is to have IPv4, IPv6, and IP types, something like this:
type IP interface {
IPVersion() int // Or "IPv4() bool" or whatever
}
type IPv4 [4]byte
func (i IPv4) IPVersion() int { return 4 }
type IPv6 [16]byte
func (i IPv6) IPVersion() int { return 6 }
This way, you could still have types like UDPAddr and TCPAddr simply have an IP field, but you wouldn't have to worry about any of the problems I mentioned above.
The questions I'm asking, then, are:
- Does anybody disagree with my characterization of the challenge (that is, the need to have multiple types of IP addresses in a single type for simplicity's sake)?
- Does anybody disagree with my characterization of the problems that the current solution introduces?
- Does anybody think that my proposed solution fails to address the challenge in some way, or introduces some other problems?
- Any other thoughts, feedback, or hate mail?
Cheers,
Josh