Go Type System and Type Conversion¶
A Failure of Assignment¶
Recently, my friend asked me a question, why inthe following go code, b
cannot be assigned to f
? With knowledges about AST, I got intrigue on it than ever before.
The failure is caused by Bar
and Foo
are not same type, hence, they cannot be assigned to each other. However, assign a string to Bar
or Foo
directly is valid.
This blog talks why the conversion fails and will introduce the type, type definition, properities of types with some examples.
Assumption
Type string
could assign to either Foo
or Bar
, but Bar
can't assign to Foo
. My assumption is when compiler parse AST, it checks the AST with grammar
and aborts as the assignment violates the grammar because the error is raised in compiling, not runtime.
- when parsing AST, the parser recursively traces the underlying type until a predeclared type.
- the grammar of assigning requires satisfies either same type or underlying type.
At first, I try to figure out the anwser by reading source code directly. However, I found it's more effiecient to read the Go Language Specification as it states the esstential rules in go.
Then, the following content confirms my assumption and briefly introduce related knowledges.
Knowledges of Go Type¶
Type Definition in EBNF¶
The specification used here is on 1.18
, which supports generic. The EBNF definition of type is shown below:
Type = TypeName [ TypeArgs ] | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeArgs = "[" TypeList [ "," ] "]" .
TypeList = Type { "," Type } .
TypeLit = ArrayType | StructType | PointerType | FunctionType |
InterfaceType | SliceType | MapType | ChannelType .
[]
refers 0 or 1 time and TypeArgs
stands for some types quoted by []
. It could express basic type declartion and generic type declartion. The crucial concept for this blog is named type
, denotes whether a type has a name. For example, the Example
is a named type, but (int, error)
isn't.
This syntax is used in multiple returned values(e.g, (int,error)
by functions. The whole part is treated as a single part. That's why you CANNOT do this because the (int, error)
is a single type.
Underlying Type¶
Based on the go spec in topic properties of types and values.
The assumption follows the spec, there is an field called underlying type
in the type. Moreover, there is a core type
as well, however, as it's designed for interface, it's out of topic and won't be mentioned here.
Each type T has an underlying type: If T is one of the predeclared boolean, numeric, or string types, or a type literal, the corresponding underlying type is T itself. Otherwise, T's underlying type is the underlying type of the type to which T refers in its declaration. For a type parameter that is the underlying type of its type constraint, which is always an interface.
Type Identity¶
Before we talks about assignment, it's important to know type identity.
Two types are either identical or different. A named type is always different from any other type. Otherwise, two types are identical if their underlying type literals are structurally equivalent; that is, they have the same literal structure and corresponding components have identical types. (check spec to learn more detailed rules).
The reason why named type is always different from any other type
is caused the representation of structure type in the implementation. Go defines a Named
type to represent the named types. Hence, one named type will always be different from another named type.
// src/go/types/predicates.go#L222
func (c *comparer) identical(x, y Type, p *ifacePair) bool {
// ignore lines
switch x := x.(type) {
// ignore lines
// L436
case *Named:
// Two named types are identical if their type names originate
// in the same type declaration; if they are instantiated they
// must have identical type argument lists.
if y, ok := y.(*Named); ok {
// check type arguments before origins to match unifier
// (for correct source code we need to do all checks so
// order doesn't matter)
xargs := x.TypeArgs().list()
yargs := y.TypeArgs().list()
if len(xargs) != len(yargs) {
return false
}
for i, xarg := range xargs {
if !Identical(xarg, yargs[i]) {
return false
}
}
return indenticalOrigin(x, y)
}
}
}
Assignability¶
This topic will explain the problem, why string
can but Bar
cannot. There are some rules to judge the assignability of value x
of type V
to a variable of type T
. The idea is simple, because both Foo
and Bar
have underlying type string
, so string could assign them. However, because neither type identifiers vary between them, Foo
and Bar
cannot be assigned.
Alias and New Type¶
When defining types, we could also define an alias for one type with =
.
type Foo = string
type Bar = Foo
func main(){
var f Foo
var b Bar
f = b // valid assignment
b = f // valid assignment
}
Foo
and Bar
are string
essentially. Hence, they could assign to each other as they are all string
type. Why Does Assignment Fail?¶
The answer is named type Foo
and Bar
have different Identical. Hence, they are not same so they cannot be assigned to each other even though they have the same underlying type. The src/go/types/operand.go#assignableTo implements all the behaviors in the document.
How Unsigned Integer and Float64 Are Converted¶
The last blog about conversion precise lose between float and uint analyzes why the precise is lost. Here we take a further step to learn more about them.
First, all float number has a default float64
type unless you declare it explicitly with float32
.
- Question: does the
3.14
) have a type? - Anwser: no,
3.14
is a float-point literal, which doesn't has a type.
Literal and Type¶
In computer science, a literal is a notation for representing a fixed value in source code. In contrast to literals, variables or constants are symbols that can take on one of a class of fixed values, the constant being constrained not to change. According to introduction of literal, literal is a concept in token layer, which is generated by lexer and consumed by an AST parser.
Hence, there is a mapping between literals and types. Moreover, in a language layer, we usually call constant
for all types of literals. For example, go has rune, integer, floating-point, imaginary, string literal and so forth.
As a result, the type of a constant refers to the type of the literal.
Representablity of Constant¶
As we mentioned before, a constant has a type. However, constant allows an implicit cast, which is called representability in go. It means when we assign a variable with a constant, it's valid if the destination is representable to the constant.
A constant x is representable by a value of type T, where T is not a type parameter(generic feature case, ignore), if one of the following conditions applies:
-
X is in the set of values determined by T:
-
T is a floating-point type and x can be rounded to T's precision without overflow. Rounding uses IEEE 754 round-to-even rules but with an IEEE negative zero further simplified to an unsigned zero. Note that constant values never result in an IEEE negative zero, NaN, or infinity.
-
T is a complex type, and x's components real(x) and imag(x) are representable by values of T's component type (float32 or float64).