Creating a Constant Type and Restricting the Type's Values
I have a question about types of constants which are restricted to certain values and how you accomplish that in Go. Say I create a type unary
which has two constant values Positive(1)
and Negative(-1)
and I want to restrict the user of that type (unary
) from creating other values of type unary
. Do I achieve this by creating a package and making the values Positive
and Negative
visible and making the type unary
restricted to the containing package? See code below for example
package unary
type unary int////not visible outside of the package unary
const (
Positive unary = 1//visible outside of the package unary
Negative unary = -1//visible outside of the package unary
)
func (u unary) String() string {//visible outside of the package unary
if u == Positive {
return "+"
}
return "-"
}
func (u unary) CalExpr() int {//visible outside of the package unary
if u == Positive {
return 1
}
return -1
}
Is this the correct way to restrict a type to certain constant values?
Solution 1:
Flaws
Your proposed solution is not safe in a way you want it to be. One can use untyped integer constants to create new values of unary
having a different int
value than 1
or -1
. See this example:
p := unary.Positive
fmt.Printf("%v %d\n", p, p)
p = 3
fmt.Printf("%v %d\n", p, p)
Output will be:
+ 1
- 3
We could change p
's value to store the int
value 3
which is obviously not equal to Positive
nor to Negative
. This is possible because Spec: Assignability:
A value
x
is assignable to a variable of typeT
("x
is assignable toT
") in any of these cases:
- ...
x
is an untyped constant representable by a value of typeT
.
3
is an untyped constant, and it is representable by a value of type unary
which has underlying type int
.
In Go you can't have "safe" constants of which "outsider" packages cannot create new values of, for the above mentioned reason. Because if you want to declare constants in your package, you can only use expressions that have "untyped" versions–which may be used by other packages too in assignments (just as in our example).
Unexported struct
If you want to fulfill the "safe" part, you may use unexported struct
s, but then they cannot be used in constant declarations.
Example:
type unary struct {
val int
}
var (
Positive = unary{1}
Negative = unary{-1}
)
func (u unary) String() string {
if u == Positive {
return "+"
}
return "-"
}
func (u unary) CalExpr() int {
return u.val
}
Attempting to change its value:
p := unary.Positive
p.val = 3 // Error: p.val undefined (cannot refer to unexported field or method val)
p = unary.unary{3} // Error: cannot refer to unexported name unary.unary
// Also error: implicit assignment of unexported field 'val' in unary.unary literal
Note that since we're now using a struct
, we can further simplify our code by adding the string
representation of our values to the struct
:
type unary struct {
val int
str string
}
var (
Positive = unary{1, "+"}
Negative = unary{-1, "-"}
)
func (u unary) String() string { return u.str }
func (u unary) CalExpr() int { return u.val }
Note that this solution still has a "flaw": it uses exported global variables (more precisely package-level variables) whose values can be changed by other packages. It's true that other packages cannot create and assign new values, but they can do so with existing values, e.g.:
unary.Positive = unary.Negative
If you want to protect yourself from such misuse, you also have to make such global variables unexported. And then of course you have to create exported functions to expose those values, for example:
var (
positive = unary{1}
negative = unary{-1}
)
func Positive() unary { return positive }
func Negative() unary { return negative }
Then acquiring/using the values:
p := unary.Positive()
Interface
Care must be taken if you plan to use an interface type for your "constants". An example can be seen in Kaveh Shahbazian's answer. An unexported method is used to prevent others from implementing the interface, giving you the illusion that others truly can't implement it:
type Unary interface {
fmt.Stringer
CalExpr() int
disabler() // implementing this interface outside this package is disabled
}
var (
Positive Unary = unary(1) // visible outside of the package unary
Negative Unary = unary(-1) // visible outside of the package unary
)
type unary int // not visible outside of the package unary
func (u unary) disabler() {}
func (u unary) String() string { /* ... */ }
func (u unary) CalExpr() int { /* ... */ }
This is not the case however. With a dirty trick, this can be circumvented. The exported Unary
type can be embedded, and an existing value can be used in order to implement the interface (along with the unexported method), and we can add our own implementations of the exported methods, doing / returning whatever we want to.
Here is how it may look like:
type MyUn struct {
unary.Unary
}
func (m MyUn) String() string { return "/" }
func (m MyUn) CalExpr() int { return 3 }
Testing it:
p := unary.Positive
fmt.Printf("%v %d\n", p, p)
p = MyUn{p}
fmt.Printf("%v %d\n", p, p.CalExpr())
Output:
+ 1
/ 3
Special case
As Volker mentioned in his comment, in your special case you could just use
type unary bool
const (
Positive unary = true
Negative unary = false
)
As the type bool
has two possible values: true
and false
, and we've used all. So there are no other values that could be "exploited" to create other values of our constant type.
But know that this can only be used if the number of constants is equal to the number of possible values of the type, so the usability of this technique is very limited.
Also keep in mind that this does not prevent such misuses when a type of unary
is expected, and someone accidentally passes an untyped constant like true
or false
.
Solution 2:
If you like to just work with int
without introducing a wrapper type: a classic way to do that in Go is using a public interface with a private function; so everybody can use it but nobody can implement it; like:
type Unary interface {
fmt.Stringer
CalExpr() int
disabler() //implementing this interface outside this package is disabled
}
var (
Positive Unary = unary(1) //visible outside of the package unary
Negative Unary = unary(-1) //visible outside of the package unary
)
type unary int //not visible outside of the package unary
func (u unary) disabler() {}
func (u unary) String() string { //visible outside of the package unary
if u == Positive {
return "+"
}
return "-"
}
func (u unary) CalExpr() int { //visible outside of the package unary
if u == Positive {
return 1
}
return -1
}
Others are able to set Positive
to nil
though; but that's not a thing in Go world - in such cases.
As @icza mentioned, one can overwrite public methods. But for private methods, Go will not call "the most shallow" one, but instead calls the original.