您可以防止创建无效的自定义类型吗?

Say we want to create a custom Probability type to represent numbers between 0 and 1. We can do this:

type Probability float64

func NewProbability(p float64) (*Probability, error) {
    if p < 0 || p > 1 {
        return nil, errors.New("Invalid Probability")
    }
    tmp := Probability(p)
    return &tmp, nil
}

This works as long as clients of our code always use our NewProbability constructor. But they can get around it with a type conversion:

func main() {
    // works as intended
    p1, _ := NewProbability(0.5)
    fmt.Println(*p1)

    // errors as intended
    _, err := NewProbability(2)
    fmt.Println(err)

    // circumvents our constraints...
    // creates invalid Probability
    p3 := Probability(2)
    fmt.Println(p3)
}

https://play.golang.org/p/xJZQhkZLi_H

2 questions:

  1. Is there any approach that could prevent such circumvention, so that if you had a Probability it would always be valid?
  2. Unrelated to main question: If I omit the tmp variable in the constructor and instead do return &Probability(p), nil, I get the error cannot take the address of Probability(p) (Try it). Why does this error while using a tmp variable does not?

1) You could try making Probability a struct with a hidden float value in it, but you won't be able to use it as a number. Another option would be to add IsValid() method to Probability (somewhat similar to a NaN).

2) Probability(p) is a copy of p with type Probability. It is a value that is the result of an operation, with no address until it is assigned to a variable. When you assign it to a variable, you can get the address of that variable.

Can you prevent an invalid custom type from being created?

No.

And the idea promoted by "classical" OOP languages that if you prevent misuse no problems can arise is not warranted. If the user does not read your documentation problems will arise.

This question comes up every once in a while. The only way to "ensure" that no invalid values are assigned to some custom type is to guard it with getters and setters, in an unexported struct field:

type Probability struct {
    p float64
}

func NewProbability(p float64) (Probability, error) {
    if p < 0 || p > 1 {
        return Probability{}, errors.New("invalid probability")
    }
    return Probability{p}
}

For something as simple as a float, this is probably bloated overkill. A saner approach is usually to check that you receive a valid probability whenever you accept such a parameter:

func DidItHappen(p probability) (bool, error) {
    if p < 0 || p > 1 {
        return false, errors.New("invalid probability")
    }
    if /* roll the dice */ {
        return true, nil
    }
    return false, nil
}