Search code examples
goabstract-data-type

Abstract data type constructor can be accidentally bypassed?


I'm trying to make an abstract data type representing a positive number:

package m

type positiveNum int

func MakePositiveNum(i int) positiveNum {
    if i < 1 { panic("non positive number") }
    return positiveNum(i)
}

// some function that expects a positive number
func UsePositiveNum(s positiveNum) {}

Here are some example uses:

package main
import "m"
func main() {
    pn := m.MakePositiveNum(123)
    //i := 1; m.UsePositiveNum(i) // fails as expected because
                                  // int is passed instead of positiveNum
    //useInt(pn) // fails because trying to pass positiveNum instead of int
    //pn = m.positiveNum(0) // fails as expected because the type is private
    m.UsePositiveNum(pn)
}

func UseInt(int) {}

If you replace m.UsePositiveNum(pn) with m.UsePositiveNum(0), it still compiles, bypassing the positive number typecheck. Why?


Solution

  • What is happening here is that 0 is an untyped constant. Such constants are covered by this rule about assignability:

    A value x is assignable to a variable of type T ("x is assignable to T") in any of these cases:

    • ...
    • x is an untyped constant representable by a value of type T.

    Since positiveNum's underlying type is int, which can represent 0, the conversion happens without error.

    @peterSO's answer provides a way to avoid this implicit conversion, since there is no implicit conversion from an integer constant to a struct. Note that it won't protect against a malicious user creating a value like positive.Positive{0}, but that is usually not a concern.