Golang started to support generic types from version 1.8 start, and I believe many people are in the same wait-and-see state as me. Now that Golang has been released to version 1.19, I think it’s a good idea to give it a try.

Example of adding two numbers together

One of the most common examples of previous calls for Golang to introduce generics is a scenario like adding two numbers together. Taking the example of supporting adding integers and floating point numbers, we need to implement two functions and call them separately before supporting generics.

1
2
3
4
5
6
7
8
9
package main

func AddInt(v1, v2 int) int             { return v1 + v2 }
func AddFloat64(v1, v2 float64) float64 { return v1 + v2 }

func main() {
    dInt(1, 2)
    AddFloat64(0.1, 0.2)
}

With generic support, we only need to define a function and directly call.

1
2
3
4
5
6
7
8
package main

func Add[T int | float64](v1, v2 T) T { return v1 + v2 }

func main() {
    Add[int](1, 2)
    Add[float64](0.1, 0.2)
}

It can even be further simplified as follows.

1
2
3
4
5
6
7
8
package main

func Add[T int | float64](v1, v2 T) T { return v1 + v2 }

func main() {
    Add(1, 2)
    Add(0.1, 0.2)
}

Of course, this latter, most simplified version works because it is possible to invert type arguments T by the types of function arguments v1, v2. If there are no parameters, or function parameters can not be deduced from the type is, can not be simplified.

Using Generics in the HTTP API

Although the example of adding two numbers is typical, I usually hardly ever care about it though. After all, in a static language, most of the time the type of the data is already determined and there is no need to really call three functions in the same place.

But when I was wrapping the HTTP API wrapper, I had to wrestle with whether supporting generics would make my code make a little more sense each time.

Specifically, for the common HTTP APIs, the return value is mostly a JSON structured text. This structure is different for each API, but they all usually have some of the same information again, such as error codes and error messages. For example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
type CommonResponse struct{
	ErrCode int
	ErrMsg string
}

func (resp CommonResponse) CheckError() error {
	if resp.ErrCode == 0 && resp.ErrMsg == "" {
		return nil
	}

	return fmt.Errorf("[%d]%s", resp.ErrCode,resp.ErrMsg)
}

type Responser interface {
	CheckError() error
}

type LoginResponse struct{
	CommonResponse
	Result struct{
		Token  string
		Expire int
	}
}

type HomeResponse struct{
	CommonResponse
	Result struct{
		Username   string
		LoginCount int
	}
}

I usually want to be able to easily and uniformly implement the following two features within the API.

  • Uniform JSON structure parsing
  • Uniform error handling

Not using generics

In the days when there were no generics, we handled this by first initializing the response data structure of the API. Then pass its pointer as a parameter to the function (a common out reference usage). This approach works because json.Encoder.Decode() itself supports pointers of type any. Of course, there are some risks. For example, theoretically the caller can exist to pass nil and other unreasonable use, resulting in code panic.

Also, in order to do uniform error handling, we implement a CheckError method on the public structure CommonResponse and define the method as an interface. It is used to constrain the incoming parameters. The advantage of this approach is that it does the most minimal type checking on incoming parameters. But since Golang’s interfaces are not the same as class inheritance, any structure that implements the method but is not a combination of CommonResponse or CommonResponse can be used here. There is some risk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func request(url string, resp Responser) error {
	res, err := http.Get(url)
	if err!=nil {
		return err
	}
	defer res.Body.Close()

	err := json.NewDecoder(res.Body).Decode(resp)
	if err!=nil {
		return err
	}

	if err:=resp.CheckError(); err!=nil {
		return err
	}

	return nil
}

func APILogin() (LoginResponse,error) {
	var resp LoginResponse
	err := json.Unmarshal(data, &resp)
	return resp,err
}

func APIHome() (HomeResponse,error) {
	var resp HomeResponse
	err := json.Unmarshal(data, &resp)
	return resp,err
}

Using Generic Types

With generic support, we can declare the response data interface, as a type parameter. Then when executing the call, just explicitly specify the type parameter.

This way, in the underlying function, what is passed to the json library is still a pointer of type any. But this pointer is initialized in this function with an explicit type, so there is no risk of calling an implausible call.

Also, in theory, the type constraints of the generic type T can be directly refined to all API response structure lists. Avoid the problem that any interface that satisfies the CommonResponse.CheckError function signature can be passed in as a parameter. However, this problem does not have a significant impact, at most, it does not parse reasonable data and does not panic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func request[T Responser](url string) (T, error) {
	res, err := http.Get(url)
	if err!=nil {
		return err
	}
	defer res.Body.Close()

	var respInfo T
	err := json.NewDecoder(res.Body).Decode(&respInfo)
	if err!=nil {
		return err
	}

	if err:=resp.CheckError(); err!=nil {
		return err
	}

	return nil
}

func APILogin() (LoginResponse,error) {
	return request[LoginResponse](data)
}

func APIHome() (HomeResponse,error) {
	return request[HomeResponse](data)
}

Two comparisons

Purely from the code point of view, in the scenario of HTTP API encapsulation, whether or not to use generics has basically no impact on the amount of code. But in terms of code quality, there is no doubt that generics are relatively more complete.