4

Coming from JavaScript and TypeScript, I wanted to check out Go and make a simple calculator. Since there's the difference between int and float, what is the preferred way to write a function that takes any number?

For example:

package main

func add(a float64, b float64) float64 {
  return a + b;
}

func main() {
  a := 1;
  b := 2;
  fmt.Println(add(1, 2)); // 3
  fmt.Println(add(a, b)); // Cannot use a (type int) as type float64 in argument to add
  fmt.Println(add(1.5, 3.2)); // 4.7
  fmt.Println(add(2.5, 2)); // 4.5
}

Do I need to convert everything to float (since it "covers" the int range) or do I create a separate functions for each type, like addInt(a int, b int) int and addFloat(a float64, b float64) float64? Or might there be a more elegant way at all?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Lavariet
  • 497
  • 8
  • 15
  • 6
    Note that float does not cover the integer range. For large integers, there may be no floating point value that is precisely equal. The general answer, though, is to go back to your actual goal and do that. A simple calculator doesn't work on "any kind of number value." It works on some number type that you design the system around (generally floating point or fixed point). Write your functions for that and you'll be fine. – Rob Napier May 24 '21 at 20:20

3 Answers3

23

Go 1.18 and above

With the introduction of type parameters in Go 1.18, this is easier to accomplish.

You can define a function parametrized in T and use an interface constraint to restrict T to numeric types.

func add[T Number](a, b T) T {
    return a + b
}

The constraint Number can be defined using golang.org/x/exp/constraints package (still experimental):

import "golang.org/x/exp/constraints"

type Number interface {
    constraints.Integer | constraints.Float
}

Where:

  • Number is the union of the type sets of constraints.Integer and constraints.Float
  • constraints.Integer is the set of all signed and unsigned integer types
  • contraints.Float is the set of float types

This will allow you to call add with any two arguments of numeric type. Then in the function body you will be able to use any operation that is supported by all types in the constraint. So in case of numbers, this includes also arithmetic operators. Then declaring similar functions is easy:

func multiply[T Number](a, b T) T {
    return a * b
}

Keep in mind that the arguments must have the same type. Regardless of generics, you can't use different types; from the specs Operators:

[...] the operand types must be identical unless the operation involves shifts or untyped constants.

Therefore our generic add and multiply functions are defined with only one type parameter T. This implies that you also can't call the add function with untyped constants whose default types are incompatible:

add(2.5, 2) // won't compile

In this case the compiler will infer the type of T from the first argument 2.5, which defaults to float64, and then won't be able to match the type of 2, which defaults to int.

Full program:

package main

import (
    "fmt"

    "golang.org/x/exp/constraints"
)

type Number interface {
    constraints.Integer | constraints.Float
}

func main() {
    a := 1
    b := 2
    
    fmt.Println(add(1, 2))     // 3
    fmt.Println(add(a, b))     // 3
    fmt.Println(add(1.5, 3.2)) // 4.7
    // fmt.Println(add(2.5, 2)) // default type int of 2 does not match inferred type float64 for T
}

func add[T Number](a, b T) T {
    return a + b
}

Playground: https://go.dev/play/p/rdqi3_-EdHp

Warning: since these functions handle also floats, keep in mind that floats can hold NaN values and infinities.


About complex numbers

Go has complex64 and complex128 predeclared types. You can use them too in the Number constraint:

import "golang.org/x/exp/constraints"

type Number interface {
    constraints.Integer | constraints.Float | constraints.Complex
}

This doesn't restrict the capabilities of these generic functions: the arithmetic operators that are supported by integers and floats (only +, -, * and /) and all order operators are supported by complex types too. The remainder operator % and bitwise operators are supported only by integers, and therefore by type parameters constrained to constraints.Integer.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
3

Up until Go 1.17 (pre-generics). See other answer(s) for an updated solution


The simplest option is to just convert arguments at the call site.

add(float64(a), float64(b))
Hymns For Disco
  • 7,530
  • 2
  • 17
  • 33
  • I think this answers the question for now, although it can lead to imprecision. And it might be simpler to just define all value which potentially will be added in the future as float64 to prevent the conversion on each run. Also I'm a bit sad there are no generics (yet it seems?) but I can also understand the cost that comes with adding the feature – Lavariet May 24 '21 at 20:29
  • 1
    The "everything is a float" method is how JavaScript itself works by the way https://developer.mozilla.org/en-US/docs/Glossary/Number . Generics are planned (and loosely scheduled) for addition to the language. It has been demanded since forever, but the project has been inherently resistant to big changes and generics will be by far the biggest change since Go 1. – Hymns For Disco May 24 '21 at 20:47
1

It is possible now thanks to generics, but it's extremely tedious because you have to specify every numeric type by hand in the function declaration.

// Adds two integers and returns the result together with a boolean
// which indicates whether an overflow has occurred
func AddInt[I int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64](a, b I) (I, bool) {
    c := a + b
    if (c > a) == (b > 0) {
        return c, true
    }
    return c, false
}

You could also define an interface that will contain the verbose list of int types

type Int interface {
    int | uint | int8 | uint8 | int16 | uint16 | int32 | uint32 | int64 | uint64
}

// Adds two integers and returns the result together with a boolean
// which indicates whether an overflow has occurred
func AddInt[I Int](a, b I) (I, bool) {
    c := a + b
    if (c > a) == (b > 0) {
            return c, true
    }
    return c, false
}

There is a constraints package that provides a more DRY way of defining such functions, but it's experimental and could be removed from the language at some point, so I wouldn't recommend using it.

  • As ugly as it might look like, this option does allow you to cast into the chosen type using `I(1234)`, something that the above answers using `constraints` do not allow. – ibarrond Aug 02 '23 at 19:32