proposal: strconv: dedicated functions for intX, uint and uintX types and decimal numbers for .
Opened this issue · 6 comments
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.
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](https://github.com/search?q=%22strconv.ParseInt%22&type=code) produces lots of examples but that a large number of them are setting bitSizeto 64, and so the result type already matches. However, I do note a small number of examples wherebitSizeis set to 32 and the result is then converted either toint32or toint. 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 #57975, though the original general proposal was declined shortly after that compromise was raised so there wasn't much discussion about that specialized variant.
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
}if we're down to just dispatching between ParseInt and ParseUint, is there really a need for a new function?
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.
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
}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.