-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: time/v2: make Duration safer to use #20757
Comments
It would be nice to have an actual proposal here. |
One actual proposal: package time
type Duration struct { ns int64 } That is, hide the representation, and make it impossible to use the normal math operators on it. That's kinda how I read this bug originally. |
I think the best fit depends on the outcome of a number of other decisions for Go 2, particularly #21130 (const struct literals), #19624 (checked overflow) and #15292 (generic programming). It also depends upon whether we want to make the Without assuming any of those, I'd propose an API like the one @bradfitz suggests, with the top-level constants replaced by functions. type Duration struct { ns int64 }
func Nanoseconds(int64) Duration
func Microseconds(int64) (Duration, bool)
func MustMicroseconds(int64) Duration
[…]
func (Duration) ScaleBy(float64) (Duration, bool)
func (Duration) MustScaleBy(float64) Duration
func (Duration) DivideBy(Duration) float64
func (Duration) Add(Duration) (Duration, bool)
func (Duration) MustAdd(Duration) Duration
func (Duration) Sub(Duration) (Duration, bool)
func (Duration) MustSub(Duration) Duration
// And keep the existing time.Duration methods.
[…] |
Or perhaps get rid of the type Duration struct { ns float64 }
func Nanoseconds(int64) Duration
func Microseconds(int64) Duration
[…]
func (Duration) ScaleBy(float64) Duration
func (Duration) DivideBy(Duration) float64
func (Duration) Add(Duration) Duration
func (Duration) Sub(Duration) Duration
// And keep the existing time.Duration methods.
[…] |
A lot of existing code will break if |
To me, that sounds like a good argument for also adding struct constants, although I admit that that leads down a long and winding path toward generalized compile-time evaluation. The other alternative is to move those constants into some compatibility package ( package main
import (
"fmt"
"time"
)
const (
SamplingDuration = 10 * time.Second
SamplingPeriod = time.Second
)
type Samples [SamplingDuration / SamplingPeriod]int64
func main() {
var samples Samples
for i := range samples {
samples[i] = time.Now().Unix()
time.Sleep(SamplingPeriod)
}
fmt.Printf("%v\n", samples)
} would be rewritten (automatically) to: package main
import (
"fmt"
"go1time"
"time"
)
const (
SamplingDuration = 10 * go1time.Second
SamplingPeriod = go1time.Second
)
type Samples [SamplingDuration / SamplingPeriod]int64
func main() {
var samples Samples
for i := range samples {
samples[i] = time.Now().Unix()
time.Sleep(SamplingPeriod.Go2Duration())
}
fmt.Printf("%v\n", samples)
} |
or make a new package |
@tandr If you don't touch existing code, you severely limit the impact of any improvements. If the new types are assignable to the existing types, then it's easy to accidentally convert to the existing types and write the existing bug-patterns. On the other hand, if the new types are not assignable to the existing types, then anyone who calls a package that uses the new types has to do their own (explicit) conversions, and they will have a strong incentive to stick to the old package (and its bug-prone API). |
@bcmills Bryan, thank you very much for the explanation. I understand the value of "lets all move forward", but realistically, it is not always possible. Fresh example being yesterday with vendored code that we are using, and cannot upgrade due to conflicts with another library. And this code came from another team, not even from 3rd party outside (well, breaking changes were introduced by 3rd party, and other team does not have time to deal with it, and decided to stay on an older dependency). The assumption that we can run "go fix" on everything is a bit too brave, sorry - it changes "constness" of code and its surroundings (libraries etc). The moment we change that code, this assumption of fixed and tested dependencies that they were using goes out of the window from the original maintainer's prospective (if he/she/them are even still around), sorry. "It works for us", "You break it - you keep it" (you touched it - you are fixing it) is far more common in development world that we are willing to admit due to lack of time, resources, or just desire to deal with "someone else's" problems. Sorry for the long rant. TL;DR I still would prefer a new "namespace" for breaking changes on standard library. |
Here's another fun example: |
The multiplication problem is an argument for units in the type system. That plus checked integer types should IIUC make this safe to use without making the API clunky. |
I agree, but that would be a much larger proposal than this one.
Checked integer types are (much to my chagrin) not a foregone conclusion. |
Do they though? We don't allow functions to be parametric over the current family of numeric types, and AFAIK nobody has asked for even that much yet. |
I actually never had any issue with time package and time.Duration. Maybe because I never wrote code like shown in the examples. While I agree that in some cases unexpected bugs can occur but I always felt that go constants use including time.Duration is genius invention. |
@ianlancetaylor has there been any proposal or discussion of 'unboxing' or 'unwrapping' single-field structs? Imagining a thought experiment where that's done, single-field struct values could be unwrapped and treated as consts if the field is a I am happy to write something separately in more detail if this has not been considered yet. |
@yawaramin I can't recall anything along those lines. It sounds a bit complicated, and it's not obvious to me how it would help with the problems mentioned in the original post on this issue. If we automatically unwrap, then it seems that those problems occur. If don't automatically unwrap, then we break existing code. |
time.Duration
is currently very prone to accidental misuse, especially when interoperating with libraries and/or wire formats external to Go (c.f. http://golang.org/issue/20678).Some common errors include:
time.Duration
values.time.Duration
before scaling instead of after.https://play.golang.org/p/BwwVO5DxTj illustrates some of these issues. The bugs are unsurprising once detected, but subtle to casual readers of the code — and all of the errors currently produce unexpected values silently. (Some but not all of them would be caught by #19624.)
For Go 2, I believe we should revisit the
Duration
part of thetime
API.A quick survey of other languages shows that Go's
Duration
type is unusually unsafe. Among modern languages, only Swift appears to be prone to the same bugs as Go.Out-of-range conversion and overflow
Exceptions:
checked
variants of arithmetic operations.OverflowException
orArgumentException
for out-of-range arguments (to FromMilliseconds and friends).ArithmeticException
on overflow to its arithmetic methods.timedelta
raisesOverflowError
.Time
exception if the argument is out of range.Floating-point or arbitrary-precision representations:
Integer
argument, but it does not appear to check ranges on floating-point to integer conversions.Double-scaling
Integer
to aTimeInterval
is an explicit function call, not just a reinterpretation of the type. However, it won't stop you from erroneously multiplying aTimeInterval
by aTimeInterval
.The text was updated successfully, but these errors were encountered: