The meaning of Go’s interfaces has changed since Go 1.18, and there are three new concepts related to Go interfaces that many people are not aware of: type set, specific type and structural type.

type set

The type set is called a type set and is a new concept added to Go 1.18 for those who follow Go generics.

Unlike Java, which requires a class to be defined explicitly to implement an interface, Go does not require this. In Go, as long as a type implements all the methods defined by an interface, it implements that interface and can be assigned to variables of that interface type, or as real or return values of methods of that interface type, a design sometimes referred to as `duck typing’. As long as it walks like a duck and quacks like a duck, then it is a duck, which is a classic description of duck typing.

In Go 1.18, interfaces no longer represent a collection of methods, but a collection of types (type set). Whenever a type is in the type set of an interface, then we say that this type implements the interface. If the interface is used as a type constraint, then any element in the type set defined by the interface can instantiate the type parameter.

So, in effect, the Go language specification has had to redefine the meaning of interfaces in order to support the extension of interfaces as type constraints, which is why type collections have emerged.

In fact, the concept of an interface’s method set is also in The method set of an interface is the intersection of the set of methods of all elements in the set of types of that interface.

rThis article assumes that you have some knowledge of Go generics. If you don’t, you must know that Go 1.18 supports the inclusion of type elements in addition to the original method elements, which can be a type T, or an approximate type ~T, or a union of them int|int8|int16|int32|int64|~string.

If an interface I is embedded in another interface E, then the type set of I is the intersection of the set of types it displays and the set of types of the embedded interface E. This is equivalent to E narrowing the type set of interface I.

How do you determine the type set of an interface? Follow these principles:

  • The type set of an empty interface any, interface{} is the set of all types So things like int, string, strcut{}, MyStruct, func foobar(), chan int, map[int]string, []int and so on are in the set of types of the empty interface
  • The set of types of a non-empty interface is the intersection of the set of types of the interface elements So what is the set of types of interface elements? See the following four articles. We have already mentioned that interface elements contain type elements and method elements.
    • A method’s type set is the set of all types that define the method, i.e. as long as the set of methods of a certain type contains the method, then it belongs to the method’s type set For example, if an interface has a method such as String() string, then all types that implement the method belong to the set of types defined by String() string, e.g. net.IP.
    • A collection of types that is not an interface type is a collection of types that contains only that type For example, a type collection of int contains only one element like int.
    • The set of types of a close element ~T is the set of all types whose underlying type is T e.g. MyInt in type MyInt int is the set of types of ~int
    • The set of types of the union element t1|t2|...|tn is the merge set of the set of types of these union elements

The following examples enumerate sets of types.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 这个集合的类型集合只有int这一种类型
interface {
	int
}
// 这个接口代表所有底层为int类型的所有类型
interface {
	~int
}
// 这个接口代表底层为int,并且实现了String方法的所有类型
interface {
	~int
	String() string
}
// 这个接口的类型集合是空集,因为不可能一个元素既是int又是string类型
interface {
	int
	string
}
// Floats代表所有底层是浮点数的类型 (底层为float32或者float64)
type Floats interface {
	~float32 | ~float64
}

specific type and specific type set

Another important concept of an interface is specific type.

Only interfaces containing a type element define a specific type (which may be an empty type).

If not strictly speaking, specific types are those types that appear in the type elements defined in T, ~T, t1|t2|... |tn in t1, t2, ... , tn .

More precisely, for a given interface I, the set of specific types corresponds to the set of types represented by that interface 𝑅, where 𝑅 is required to be non-empty and finite. Otherwise, if 𝑅 is empty or infinite, the interface has no specific type.

For a given interface, type element or type, the set of types it represents 𝑅 is defined as follows.

  • For an interface without any type elements, its 𝑅 is all elements (infinite). So it does not have a specific type.
  • If an interface has a type element, its 𝑅 is the intersection of the types represented by its elements Whether or not it has a specific type depends on whether 𝑅 is non-empty and finite.
  • For a non-interface type T, or ~T, its 𝑅 is the set containing the type T
  • For a union element t1|t2|...|tn , its 𝑅 is the merge set of the types represented by these items

Here is an example of a particular type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Celsius float32
type Kelvin  float32
interface{}                    // 无限,所以没有特定类型
interface{ int }               // 特定类型是int
interface{ ~string }           // 特定类型是string
interface{ int|~string }       // 特定类型是int, string
interface{ Celsius|Kelvin }    // 特定类型是Celsius, Kelvin
interface{ float64|any }       // 没有特定类型,因为联合类型的代表类型是无限的
interface{ int; m() }          // 特定类型是int
interface{ ~int; m() }         // 特定类型是int
interface{ int; any }          // 特定类型是int,int和any的交集
interface{ int; string }       // 没有特定类型

type set vs specific type set

There is a difference between a type set and a specific type set, as can be seen from their definitions above.

An interface may have a specific type set that is not empty, even if the type is empty. For example, interface{ int; m() } , its type set is empty (int does not implement the m method), but its specific type is int .

An interface may have an infinite set of types even if it has a finite number of specific types For example, interface{ ~int; m() } , its specific type is int, but its set of types is infinite (any type that has an underlying int and implements method m is part of its set of types).

So what is the point of defining a specific type?

Application of specific types

Specific types are used to determine whether a type argument supports indexing, such as a[x].

For example, an expression a[x] , an instance of a could be of type array, pointer to an array, slice, string, map.

If the type of a is that of the type parameter P, under what conditions will our code a[x] not compile with an error?

The required conditions would be related to the specific type.

  • P must be of a particular type
  • the indexing of a[x] is supported for the value a of a particular type of P
  • All specific types of P must be the same. In this case, the element type of the string type is byte (https://github.com/golang/go/issues/49551)
  • If the specific type of P contains the type map, then all of its specific types must be map, and all of its keys must be of the same type So sometimes if you define a joint element interface with map, slice, string, you can’t use the a[x] index type for instances of this interface, the elements are all of type int
  • a[x] is an element of array, slice, string with index x, or an element of type map with key x. The type of a must be the same
  • if the specific type of P contains the string type, then a[x] cannot be assigned to it (strings are immutable)

The specific type is also used on the type conversion definition. For a variable x, if its type is V and the type to be converted is T, x can be converted to type T provided that the following clause is satisfied:

  • every value of a particular type of V can be converted to every particular type of T
  • only if V is a type parameter and T is not, then every value of a particular type of V can be converted to T
  • only if T is a type parameter, x can be converted to every specific type of T

In a nutshell, every specific type is satisfied if it is a type parameter, and this type is satisfied if it is not.

Also, for type arguments to call the built-in functions len, cap, their specific types must be required to allow the use of these built-in functions.

structural type

For an interface T to be structural, one of the following conditions must be satisfied:

  • there exists a single type U, which is the same underlying type as every element in the type set of T
  • the set of types of T contains only chan types, and their element types are all E , and all chans have the same orientation (not necessarily identical)

A structured type contains a structure type which, depending on the conditions above, may be:

  • type U , or
  • if T contains only bidirectional chan, the structure type is chan E , otherwise it may be chan<- E or <-chan E

The following is a structured interface containing structure types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface{ int }                          // 结构类型为 int
interface{ Celsius|Kelvin }               // 结构类型为 float32
interface{ ~chan int }                    // 结构类型为 chan int
interface{ ~chan int|~chan<- int }        // 结构类型为 chan<- int
interface{ ~[]*data; String() string }    // 结构类型为 *data
// 下面的例子不包含结构类型,所以是非结构化接口
interface{}                               // 没有固定单一的底层类型
interface{ Celsius|float64 }              // 底层类型不相同
interface{ chan int | chan<- string }     // channel的元素类型不相同
interface{ <-chan int | chan<- int }      // channel没有相同的方向

The Go language specification doesn’t have much more to say about the structured interface and how to use it, it’s more about it getting the underlying structural type internally and doing type checking, like the example below which throws a no structural type compiler error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type myByte1 []byte
func _[T interface{ []byte | myByte1 | []int }] (x T, i, j, k int) { 
    var _ T = x[i:j:k] // 底层类型不一致
}
func _[T interface{ []byte | myByte1 | []int | string }] (x T, i, j, k int) { 
    var _ T = x[i:j] 
}
// 下面这个函数没问题,因为string的底层页被看做[]byte
func _[T interface{ []byte | myByte1 | myByte2 | string }] (x T, i, j, k int) { 
    var _ T = x[i:j] 
}

The following code also throws a M has no structural type compiler error

1
2
3
4
5
6
7
8
9
type multiMapOfInt interface {
	map[int]int | map[float64]int | map[string]int | map[complex64]int
}
func arraySummer[M multiMapOfInt](mp M) (sum int) {
	for _, v := range mp {
		sum += v
	}
	return
}

For example, the following mistake is often made when using Go generics, although []byte, map[int]byte and string can all range, and the key(index) and value types are the same, it also throws the error R has no structural type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main
import "fmt"
type rangeType interface {
	[]byte | map[int]byte | string
}
func rangeIt[R rangeType](r R) {
	for i, v := range r {
		fmt.Println(i, v)
	}
}
func main() {
	rangeIt(map[int]byte{1: 1, 2: 2, 3: 3})
}