Closed oakad closed 1 year ago
Please note that you should fill https://github.com/golang/proposal/blob/master/go2-language-changes.md when proposing a language change.
CC @golang/runtime
What's wrong with
package foo
var debugging = false
func Bar() {
if debugging {
// seldom used diagnostic code
}
// oft used code
}
(or use a sync/atomic.Bool
if you want it multithreaded-safe)
The branch is very predictable, so it isn't that much more expensive than a raw jump.
It's exactly the problem I'm trying to address here. :-)
Even if we look at global flags in particular, they are already out of hand: stuff like GODEBUG
or GRPC_TRACE
or any other similar feature commonly found. They are difficult to work with for both users and implementers, not consistent across packages, poorly documented, never focus on the problem at hand (a lot of noise), etc, etc.
And it's not going to get any better.
Some 20 years ago Linux Kernel moved to per-location switches for printks and function tracing. Those were exceptionally successful, but depend a lot on fancy macros.
Go has no macros and has no runtime code transforms akin to JVM. Thus, to implement something sane in this department we need compiler help (aka language feature).
It seems to me that you are replacing one mechanism that is difficult to work with by another mechanism that is difficult to work with.
Why don't we try to standardize on one of our existing approaches, such as GODEBUG
, and make it simpler?
It's time for code snippets. :-)
Let's say we want to be able to look at inputs and outputs of typical handler functions and do so without browsing through a lot of noise. In present day Go, the best approach looks as following:
package some
import "flag_registry"
import "sync/atomic"
// The flag variables have to be int, because we need 3 states: disabled, enabled and "first seen"
// Additionally, we would prefer atomic flags, but typed atomic have no initializers and assigning
// "0" to "first seen" state feels somewhat lame.
var _flagFooIn int32 = 1
var _flagFooOut int32 = 1
func foo() {
if atomic.LoadInt32(&_flagFooIn) != 0 && flag_registry.Register(&_flagFooIn) {
// input diagnostics
}
// do things
if atomic.LoadInt32(&_flagFooOut) != 0 && flag_registry.Register(&_flagFooOut) {
// output diagnostics
}
}
Whereupon, the flag_registry
implements something like:
package flag_registry
func Register(flag *int32) bool {
flagValue := atomic.LoadInt32(flag)
if flagValue < 0 {
return true
} else if flagValue == 0 {
panic("Improper use of guard flag")
}
// Register new activation location
pc, file, line, ok := runtime.Caller(0)
// Add whatever entries to controller map and what not
atomic.StoreInt32(flag, 0)
return false
}
This is more or less the best approach right now to have many optional code blocks. It is verbose, typo prone and may need to be repeated many times if there are complex nested handlers.
In a typical C library, this stuff can be packaged as macro, with the actual guard flags having "static" linkage (Go has neither macros nor static vars). Moreover, if we feel like being fancy, C libraries can rather easily utilize a distinct linker section to avoid that flag_registry.Register
call - instead, on start up, the distinct section is simply cast into an array and mapped into some convenient control structure.
None of this is possible in Go.
But, it will really make a lot of things easier if it was.
var flagFooIn atomic.Bool
func init() {
Register ("flagFooIn", &flagFooIn)
}
func foo() {
if flagFooIn.Load() {
// input diagnostics
}
}
This is actually an inferior solution. Not refactor friendly, not copy-paste resilient, not scalable in the long run (have ample user stories to substantiate). And still a lot of typing for a common pattern. :-)
Basically, I keep claiming the following:
Indeed:
And only in Go we are forced to keep breaking fingers or keep writing custom code preprocessors (I have several of those :-).
If you are including things like C macros, then in Go we have build tags that let us select the default value of some constant at build time.
Of course it has. Have I ever claimed the contrary?
When talking about macros in this context, I'm talking about stuff like this (it's a fairly common technique in C logging these days, and it is also applicable to wide range of other scenarios):
The activation metadata: https://github.com/torvalds/linux/blob/e8bc52cb8df80c31c73c726ab58ea9746e9ff734/include/linux/dynamic_debug.h#L162
The end user construct which can be toggled in runtime via side channel: https://github.com/torvalds/linux/blob/4ac6d90867a4de2e12117e755dbd76e08d88697f/include/linux/dev_printk.h#L228
In Rust, the same technique applies as is.
However, at this point, I would actually like to close this thread and write another, much better worded proposal. Assuming my argument holds at least some water. :-)
I suppose that my feeling right now is that there are already multiple ways to do the kind of thing you are discussing. I don't see the need to add another one. Of course I'm only speaking for myself.
(Also the specific suggestion of using reflect seems impossible to implement, but I expect that we could make some vaguely similar approach work.)
Yes, reflect was not the best idea. It's more a debug into thing.
Go being a Turing complete language means that indeed, anything can be implemented. The key question is, of course, one of end user utility.
For example:
// some code
OptionalBlock(func() {
// seldom used code to be activated
})
// some more code
Does OptionalBlock
can do what I'm asking of it? Yes, clearly.
But, it suffers from one major flaw: the "disabled" case is way too expensive for most uses. The closure is always allocated, OptionalBlock
has to always query its caller and some sort of synchronous map to figure out whether the optional code is intended to run and so on.
And something like this will never work outright:
OptionalBlock(fmt.Println("Some expensive check", expensiveCheck())
Because Go evaluates function arguments eagerly.
However, if OptionalBlock
was a macro (or at least could be made to behave like macro by compiler fiat) the desired outcome will be achieved.
Based on the discussion above, and the negative emoji voting, this is a likely decline. Leaving open for four weeks for final comments.
Not widely recognized patterns are doomed to linger. :-)
No change on consensus.
There exist a very potent diagnostic technique, long and very successfully used in bigger C language projects (most prominently in Linux Kernel). Unfortunately, managed memory languages seldom implement it even though managed memory makes it rather fail safe, as compared to something like C (it is doable in JVM, of course, but then, almost anything is doable in JVM :-).
In Go, it may look the following:
Here we have an
if
statement without a condition block. By default, it's simply a jump to the (possibly implicit) "else" code block. However, we may add some sort of API (based onreflect
for example) which can be used to "steer" thatif
:This will make the
if
statement atmyLabel
to enter the "then" code block. Setting the controller back to "false" will restore the initial state ("then" code block will be skipped). The label can also be made implicit, auto generated out of code location, thus saving some programmers effort.The feature is pretty straightforward to implement in the compiler. Indeed, 2 methods can be readily employed:
reflect
directly into the "myLabel" location (invoking the necessary mprotect and icache magic as necessary, steering performance being of not much concern).reflect
(possibly in dedicated linker section).It shall be noted, that Linux kernel employs both methods for its various features (the "safe" bool flag method uses a macro and a dedicated linker section).
Why is this useful and what are the alternatives?
When working with highly loaded or otherwise long running network servers we often want to extract some data for diagnostics. Unfortunately, side channel data extraction always carries a rather high performance cost. Even a single extra call to a logging system can have a substantial cost if its parameters are chosen in unfortunate way.
Thus, server developers and operators can really benefit from some mechanism to selectively enable diagnostic code blocks on one by one basis in runtime; the benefit will be even greater if the mechanism for doing so is consistent across the wider Go ecosystem, that is, happens to be part of the language.
Right now, Go doesn't offer any sort of viable alternative to address the problem of selective diagnostics in runtime.
Instead, we are left at the mercy of coarse grained, library level, non standardized global debug switches, which invariably emit either too little or too much data and never play nice with the application code (examples being Go runtime itself, libraries like GRPC and many other). The switches themselves require too much boilerplate and programmer's discipline to use on the library level, may only affect things on start-up and can be rather troublesome in general, both for the implementer and for the library user.
Providing an efficient runtime code steering facility in the core language will substantially improve observability and maintainability of a large number of Go programs.