Proposal Details

We currently have "int" case handled with strconv.Atoi and strconv.Itoa.

In my practice it is not unexpected to need the same for int8, … int64 and all kinds of uints.

I propose to introduce the following functions:

type convert to string parse from string
int8 strconv.Itoa8 strconv.Atoi8
int16 strconv.Itoa16 strconv.Atoi16
int32 strconv.Itoa32 strconv.Atoi32
int64 strconv.Itoa64 strconv.Atoi64
uint strconv.Utoa strconv.Atou
uint8 strconv.Utoa8 strconv.Atou8
uint16 strconv.Utoa16 strconv.Atou16
uint32 strconv.Utoa32 strconv.Atou32
uint64 strconv.Utoa64 strconv.Atou64

With trivial implementation probably made as strconv.ParseInt(x, 10, 32) wrapper for strconv.Atoi32.

Comment From: apparentlymart

The current typical pattern for parsing explicit integer sizes to use strconv.ParseInt a limited bitSize and then explicitly convert the result:

v64, err := strconv.ParseInt(s, 10, 32)
if err != nil {
    // (suitable error handling here)
}
v := int32(v64)

I guess the main question for this proposal is whether offering 18 additional functions that duplicate functionality that's already provided is worth the cost.

I think the typical way to advocate for proposals in that category is to demonstrate that there are many existing examples of code following the existing pattern that would either have their readability materially improved by switching to the new proposed functions, or where the existing pattern was error-prone in a way that would not be true for the new proposed functions.

As a starting-point for that, I note that at the time of writing GitHub Code Search for strconv.ParseInt produces lots of examples but that a large number of them are setting bitSize to 64, and so the result type already matches. However, I do note a small number of examples where bitSize is set to 32 and the result is then converted either to int32 or to int. I found only one example with a bitSize that was neither 32 or 64, but it was in a contrived example that appeared to be part of someone's notes rather than being used in real code.

It's been my experience that fixed-sized integers are more often used for unsigned values than for signed values, because they are most often used to match fixed-size storage at lower levels of abstraction such as file formats or hardware registers, and indeed the results for strconv.ParseUint seem to agree: there are various examples of bitSize 8 here, although I note that several also use base 16 instead of base 10 and so they would not be able to use the proposed strconv.Utoa8 function. Perhaps this proposal would be more impactful as sized variants of ParseInt/ParseUint, rather than of Atoi. 🤔


Overall my take here is that the benefit here seems pretty marginal, though I'd feel differently about a single new variant of strconv.ParseInt/strconv.ParseUint that is generic over all of the integer types and chooses bit size and signedness based on the type parameter, instead of based on the function name and an explicit argument:

package strconv

// (I don't like this function name)
func ParseIntOf[T constraints.Integer](s string, base int) (T, error)

An integer-specific generic function like this was discussed in the comments of the more general earlier proposal https://github.com/golang/go/issues/57975, though the original general proposal was declined shortly after that compromise was raised so there wasn't much discussion about that specialized variant.

Comment From: adonovan

We definitely should not provide n new functions, one for each size. A single generic function (like your ParseIntOf for all sizes and, more compellingly, named variants of integers is more appealing, and is not so easily dismissed by the same arguments that resulted in the closure of #57975.

Here's a sketch; the implementation is regrettably fiddly due to the lack of type parameter switch or a simple way to express is signed[T]() { ... } as a constant.

https://go.dev/play/p/HO0tAEoarvN

func ParseInteger[T Integer](s string, base int) (res T, err error) {
    // The switch is a dynamic workaround for #45380.
    rv := reflect.ValueOf(&res).Elem()
    switch reflect.TypeFor[T]().Kind() {
    case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
        // signed
        var i int64
        i, err = strconv.ParseInt(s, base, rv.Type().Bits())
        if err == nil {
            rv.SetInt(i)
        }
    default: // unsigned
        var u uint64
        u, err = strconv.ParseUint(s, base, rv.Type().Bits())
        if err == nil {
            rv.SetUint(u)
        }
    }
    return
}

func main() {
    d, err := ParseInteger[time.Duration]("123", 10)
    log.Fatal(d, err) // 123ns, nil
}

Comment From: seankhliao

if we're down to just dispatching between ParseInt and ParseUint, is there really a need for a new function?

Comment From: adonovan

if we're down to just dispatching between ParseInt and ParseUint, is there really a need for a new function?

ParseInteger does slightly more than that: it chooses the correct bits value, and handles the conversion.

Comment From: jimmyfrasche

With #60274 it would just be

func ParseInteger[T Integer](s string, base int) (res T, err error) {
    k := math.Reflect[T]()
    if k.Signed() {
        var i int64
        i, err = strconv.ParseInt(s, base, k.Size())
        res = T(i)
    } else {
        var u uint64
        u, err = strconv.ParseUint(s, base, k.Size())
        res = T(u)
    }
    return
}

Comment From: jimmyfrasche

Also if you fix the base to 10 you could have it work with all numbers by adding paths to dispatch to ParseFloat and ParseComplex as well.

Comment From: thediveo

wondering if the res = T(i) triggers codeql bitsize-related checks?