Open gopherbot opened 11 years ago
I'm against this. If it would have to have constant semantics then its run time costs are the same as today, only hidden. const c = []byte{1} a := c a[0] = 42 b := c fmt.Println(b[0] == 1) The above can print 'true' only if the c's backing array is copied in assignment to 'a', however the const declaration gives an illusion of always using the same backing array - like is the case of a string's backing array. IOW: 1. nothing is gained by const []T and 2. run time costs get hidden and thus potentially confusing.
If slice constants have this hidden cost problem, could we at least have constant arrays and arrays of arrays? const sentence = [...][...]byte{"a", "series", "of", "pieces", "of", "text"} 1. This should be sufficiently const that it could, in the right environment, go into rom or the code segment. That is, be immutable, ideally. 2. I should be able to type it without getting RSI from repeating '[]byte' for every word. 3. For byte structures like this, it isn't absolutely necessary that we would be allowed to enter arbitrary runes, although that would be technically feasible, and useful. Especially for those who aren't English-language programmers. And it would be consistent with the rest of Go.
Some comments: 1) This is neither a defect of the language nor the design. The language was _deliberately_ designed to only permit constant of basic types. 2) The implications of such a change are much more far-fetching than meets the eye: there are numerous open questions that would have to be answered _satisfactorily_; and I don't think we are there yet. For instance, if we allow such constants, where is the limit? Do we allow constant maps? What about constant channels? Constant pointers? Is it just a special case for slices? etc. A first step might be to allow constant arrays and structs as long as they are only composed of fields that can have constant types themselves. An even smaller step (which I do think we should do) is to make "foo"[i] a constant if i is a constant (right now it's a non-constant byte). Finally, note that it's often not very hard for a compiler to detect that a package-level variable is never modified. Thus, an implementation may choose to optimize the variable initialization and possibly compute it compile time. At this point, the const declaration only serves as documentation and for safety; there's no performance loss anymore. But again, we have tried to keep the type system (incl. what constants are) relatively simple so that it doesn't get into the way. It's not clear the benefits are worth the price in additional complexity.
Status changed to LongTerm.
In many projects I have programmed, there is a need for non-writable initialized data larger than a single variable. This is analogous to an 'asm' directive that initializes bits in the code segment. Something that the compiler has to do anyway. Also, embedded systems often need to put data into read-only-memory (ROM). This has more to do with storage class than the type system. Declarations rather like the following might be simplest: const ( data = []byte("The quick brown fox jumped over the lazy dog.") π = float64(3.14159) bits = []uint32{0x12345678, 0xBabeAbed} ) I don't see any additional complexity here. But it might be easier to introduce a new modifier, "readonly" to supplement "const", if for some reason the above chokes the grammar. Putting data in the code segment is just my way of saying that it's simple. Actually, it would be a security risk. Anything marked "const" or "readonly" should be NX (Not eXecutable).
Ah. It was a typo on my part to use slice notation []byte, instead of array notation, [...]byte. I don't propose constant slices. I propose constant (initialized) arrays. See here: http://play.golang.org/p/eCn6ip--w0.
any news about this?
It'd be nice to have something like const keys = [...]string{"a", "b", ... }
This will not change in the foreseeable future.
The bar for language changes is extremely high at this point. "It'd be nice" is certainly not sufficient even as a starting point. To have a chance of even just being considered, there would need to be a full proposal together with a detailed analysis of cost and benefit.
In this specific case, extending the concept of constants to other than just basic types would be a significant change with all kinds of repercussions. I like to add also to the comment in the initial issue report that the current situation is not a "defect" in the spec - it was conceived as is pretty much from day one, for very good reasons.
Leaving open for a future Go 2 if there will ever be one.
This is about whether Go should have immutable values. There are a number of things to consider.
One thing to consider is how to handle
const s = [...]int{ f() }
That is, do the elements of an const array have to be const themselves? Is it possible to set up such an array in an init
function?
People have already raised questions about const slices. Another question is whether const values are addressable. If I can take a pointer to a const value, such as a field in a const struct, presumably I can't modify the value through that pointer, but what stops me from doing that? This proposal doesn't have any language mechanism for distinguishing a normal pointer from a pointer to a const (which I think is a good thing). So something has to catch erroneous writes and, presumably, panic. If we can put the initializer in read-only memory then I guess that will happen automatically, but then it's hard to initialize the elements in a function.
One idea I've mentioned elsewhere is a freeze
function, that would do a shallow copy of a value into memory that is then made read-only (somehow). I don't know if that can be implemented efficiently but it is an approach to this general problem that does not involve a language change.
Update: If you follow the link I pasted below, you'll see how I realized that module-level state is bad(tm). Please check @wora's proposal thread.
This discussion is pretty open ended, because we're talking about at least two concerns... one about deep immutability, and one about shallow immutability.
I propose that we introduce shallow immutability only. See the proposal for the const-mutable type.
In the above proposal, const
is just another kind of var
except you can't change what it points to, e.g. it's not addressable. It's got nothing to do with deep immutability, and I imagine it would be fairly easy to implement even for Go1. Combined with the proposal for read-only slices, you get a sufficiently complete set of orthogonal language rules that allow you to program safe and performant code.
That is, do the elements of an const array have to be const themselves? Is it possible to set up such an array in an init function?
In the compile-time functions proposal, I proposed that package-level functions could themselves be marked const
(i.e., made available at compile time), where the arguments to those functions could be:
The focus of that proposal was on computing types, but you could fairly easily strip out the first-class types and I think end up with a reasonable proposal for generalizing const
to other value types (but not slices or pointers).
It would, however, be a lot of work to implement.
I wanted to think through what this would mean:
[N]T
, to be constant when T
can be a constant.Note: This is not an argument against constant slices, maps, etc.; just an exploration of what allowing only the above to be constants would mean.
To answer @ianlancetaylor's questions, these, like other constants, are not addressable (nor are their indices/fields), cannot be created in init()
functions, and const s [...]int{ f() }
would be illegal unless f()
is a constant expression. Each element or field is itself constant (so ca[0] = v
and cs.f = v
are both illegal).
This would make
const (
x int = iota
y
z
)
and
const vec = [...]int{0, 1, 2}
and
const vec = struct {
x, y, z int
}{1, 2, 3}
all equivalent ways of organizing and labeling the same constants.
Constant arrays being inaddressable has a major downside: they cannot be sliced (unless constant slices are also allowed). The primary use cases for constant arrays are, as far as I am aware, tables of precomputed values and file/network signatures. None of these especially require slicing. It would still be very inconvenient, though.
It would be possible to define slicing a constant array to always duplicate the backing array but this would make an expensive operation look like an expensive one. No go.
Another option would be to extend the copy
builtin to take arrays, so that copy(s, a)
behaved like copy(s, a[:])
does now. This would make the expense of the operation clear.
That's still a bit awkward. It would be nice, but not essential, to also define a new built in, dup
, which takes a slice or an array and returns a slice with a copy of the (backing) array. (µ-experience report: I've written similar for byte slices at least a dozen times). This would make it easier to convert a constant array into a slice as part of an expression and allow the compiler to optimize the copy out when possible, like for _, v := range dup(ca)[1:] {
. Limited to slices, it could be written as a generic function but would need to be a builtin to operate on arrays directly so it only makes sense to consider it if there are constant arrays.
iota
in a constant array should behave the same way as iota
in any other const
block so
const (
a, x = [2]int{iota, iota + 1}, iota
b, y
c, z
)
would result in a = [2]int{0, 1}
, x = 0
, b = [2]int{1, 2}
, y = 1
, c = [2]int{2, 3}
, and z = 2
.
It is unclear to me whether the below, in whole or in part, should be allowed:
const (
a = [1+iota]int{iota: iota}
b
c
)
However, if it is, it should result in a = [1]int{0}
, b = [2]int{0, 1}
, and c = [3]int{0, 0, 2}
. While logical, the utility is unclear.
Since constant structs are free of pointers, they can always be passed by value without aliasing. Passing a constant struct to a function duplicates the original and the duplicate is then mutable (thought the compiler would be free to elide the duplication when it can prove an absence of mutation).
Given
type T struct {
a, b, c int
}
var X = T{1, 2, 3}
const Y = T{1, 2, 3}
the only differences between X
and Y
is that the initial definition of Y
cannot be changed and pointer methods cannot be called directly on Y
.
A rough equivalent that can be done today would be to define Y
as
func Y() T {
return T{1, 2, 3}
}
That offers the same safety guarantees and, I'm sure, some of the optimization opportunities, but it's more verbose and makes it awkward to define large sets of pseudo-constants without code generation.
Allowing constant structs would be useful for sentinel/named values that should never be changed or grouping related constants into a single complex.
(Everything said about constant structs would apply to constant discriminated unions, should those be added).
I can certainly see better why constants were limited to basic types. The gains of extending constants to the product types—without adding immutability to the language—are not mind blowing. If these were allowed, I'd certainly use them with carefree abandon, but they don't seem to quite clear the bar by themselves.
Combined with something like https://github.com/golang/go/issues/23637#issuecomment-376002346 I could see it being worth the effort, since it provides a more concrete need and additional value than they have on their own.
Being able to use iota when defining a lot of named struct values would be useful, though @griesemer proposed allowing iota
in var
blocks in #21473. That would not disallow allowing another package to change the value, but, as disquieting as that prospect is, it doesn't seem to have caused any serious issues in practice.
See also #21130
An alternative proposal for the same goal.
(This post is moved here per @ianlancetaylor's suggestion.)
I like this proposal and I made a proposal which specifies more detailed rules for all kinds of operations and provides a possible implementation.
The following proposal should keep compatibility if it re-uses the const
keyword instead of using a new keyword final
.
(The following is moved from https://github.com/golang/go/issues/36528)
=====================
The main intention of this proposal is to let Go support package-level immutable values of any composite types, though local immutable values are also supported.
A new keyword final
is introduced to declare final values. (Maybe we can re-use the const
keyword. Both choices have their respective benefits and drawbacks.)
Basic values can't be declared as final values (they can be declared as constants already), though basic values might be presented as immutable elements of some composite values. (So maybe it is not bad to re-use the const
keyword to declare final composite values.)
Final values can't be modified and should not be taken addresses. Taking addresses of final values doesn't compile or panics at run time.
The value referenced by a pointer, whether the pointer is final or not, is always modifiable.
If a final value is bound with a literal in its declaration, all the values presenting as sub-literals (or final identifiers) in the bound literal are also final values, except the composite literals being taken addresses, either explicitly or implicitly. (See below for examples.)
For convenience, final values can be assigned to variables, so that final values can be passed as function arguments.
Some modifications of final values can be detected at compile time, some others can only be detected at run time. Compile-time modifications of final values don't compile. Run-time modifications of final values cause panics (unrecoverable panics are recommended).
The modifications to final array elements and final struct fields can be detected and rejected at compile time.
The modifications to directly declared finals (the values bound to identifiers) can be detected and rejected at compile time.
The elements of final slices and maps might be final or not, depends on whether or not the elements are presented in the literals bound to declared final values. The elements of a slice or map, whether or not the slice or map is final, must be either all immutable (final) or all mutable (variable).
The elements of a slice derived from final arrays or a final slice whose elements are finals are also finals.
The memory blocks allocated for final elements of final slices and maps (and final arrays) will be tagged as immutable. An additional run-time condition check is needed when modifying slice/map elements or taking addresses of slice elements. The additional check will not cause an obvious efficiency loss for map element modifications (and address taking), but might cause a small but non-ignorable efficiency loss (about 25% more overhead) for slice elements modifications (and address taking). This is a drawback of this proposal,
Example:
var s = []int{1, 2, 3}
func _() {
//===== slice
final x = [][]int{
s,
[]int{7, 8, 9},
}
x[0] = nil // panic
x[0][0] = 5 // ok, elements of x[0] are mutable
x[1][0] = 6 // panic, elements of x[1] are immutable
_ = &x[1][0] // panic
x = nil // not compile
x2 := x // ok, x2 is a variable.
x2[0][0] = 5 // ok
x2[1][0] = 6 // panic
_ = &x2[1][0] // panic
x2 = nil // ok
// append calls should not modify immutable elements.
_ = append(x[1][:1], 9) // panic
_ = append(x[1], 9) // ok, for a new memory block is allocated.
//===== map
type T struct{n int}
final y = map[T][]int{
T{1}: s,
T{5}: x[1],
T{3}: []int{7, 8, 9},
}
y[T{1}] = nil // panic
y[T{1}][1] = 5 // ok
y[T{5}] = nil // panic
_ = &y[T{5}][0] // panic
y[T{5}][0] = 5 // panic
y[T{3}] = nil // panic
y[T{3}][2] = 5 // panic
y = nil // not compile
y2 := y // ok, y2 is a variable
y2[T{1}] = nil // panic
y2[T{3}] = nil // panic
_ = &y2[T{5}][0] // panic
y2[T{5}] = nil // panic
y2 = nil // ok
// Entries of final maps may not change.
y[T{9}] = []int{} // panic
delete(y, T{9}) // panic
//===== function
final f = func() {
// f can call itself if it is a final (might be not a good idea).
}
f = nil // not compile
f2 := f // ok, f2 is a variable
f2 = nil // ok
//===== channel
final c = make(chan int, 10)
c = nil // not compile
// Sends to and receives from final channels are ok.
c <- 1 // ok
<-c // ok
//===== interface
final ia interface{} = s
final ib interface{} = []int{7, 8, 9}
ia = nil // not compile
ib = nil // not compile
ia.([]int)[1] = 5 // ok
ib.([]int)[1] = 5 // panic
//===== pointer
final p1 = new(int)
final p2 = &struct{}
p1 == nil // not compile
p2 == nil // not compile
// The values referenced by final pointers are always modifiable.
*p = 5 // ok
*p = struct{} // ok
}
func _ () {
type T struct {
x int
}
final ts = []*T {
{1}, // <=> &T{1}
{2}, // <=> &T{2}
}
// Both are ok.
ts[0].x = 9
ts[1].x = 9
// Both are ok.
*ts[0] = T{}
*ts[1] = T{}
// Both fail to compile.
ts[0] = &T{}
ts[1] = &T{}
}
func _ () {
final ss = []*[]int {
&[]int{1, 2, 3},
{7, 8, 9}, // <=> &[]int{7, 8, 9}
}
// Both are ok.
(*ss[0])[1] = 5
(*ss[1])[1] = 5
// Both are ok.
*ss[0] = []int{}
*ss[1] = []int{}
// Both fail to compile.
ss[0] = &[]int{}
ss[1] = &[]int{}
}
It looks the current slice element modification operations have already checked whether or not the target element is allocated on an immutable zone. See the last example in the wiki page for evidence. But I'm not sure whether or not this is true. If this is true, then final elements of slices can also be allocated in immutable zones so that the above mentioned "25% more overhead" will not exist.
[update]: Ralph Corderoy explained that constant strings are allocated on read-only memory pages. So I think final strings can also be allocated on read-only memory pages. This is so great that the above mentioned "25% more overhead" can be avoided! ([update:] there is still a problem. Values allocated on read-only memory pages can be taken addresses, which is disallowed by this proposal. So maybe it is not a bad idea to merge this solution with the following solution 2 into one: taking addresses will check cap but modifying elements will not. After all, taking element addresses is a rare operation in practice.)
Maybe it is not needed to tag map, slice and array element memory blocks as immutable. The internal structure of a map records whether or not the map is a final. For slices, maybe the final info can be recorded in the cap
field, a negative cap means the elements of the corresponding slice are finals. So the slice element modification operation is like:
// Assume to modify slice x at index i with value v.
if i < 0 || i >= x.len {
panic("index out of range")
}
// This condition judgement is the new overhead.
// This overhead could be saved by cooperating with solution 1.
if x.cap < 0 {
panic("final value can not be modified")
}
*(x.ptr+ i * elemSize) = v
// or
// return x.ptr+ i * elemSize
// for address taking.
The slice internal structure can be modified with one more indirection:
type slice struct {
len, cap int
elements *sturct {
ptr unsafe.Pointer
immutable bool
}
}
The language already has constants, of course. So arguably this is just extending the existing concept of a constant to additional types. The suggestion, then, is to permit a const
value of any type for which we can use a composite literal: struct, slice, map, array types. But we can't permit these values to be addressable, which implies 1) we can not permit const
values that are or contain pointer types; 2) we can not permit slicing a const
slice or array. The composite literal would itself have to contain only constant values (which could in turn be constant struct, etc. types).
This seems implementable. Since the values are not addressable, the compiler could easily detect and prevent any attempt to change a const
value.
The question is: is this worth it? Does it provide enough additional functionality to the language that the change is worth making?
Just one comment on "is this worth it?":
It is unbelievable that we cannot write constant errors:
- const ErrVerification = errors.New("crypto/rsa: verification error")
+ var ErrVerification = errors.New("crypto/rsa: verification error")
Seriously? If the following code appears in one of the project dependencies, then it destroys the whole systems:
import "cropto/rsa"
func init() {
rsa.ErrVerification = nil
}
The only reason listed above to bar addressability is that it precludes initialization by a function; see https://github.com/golang/go/issues/6386#issuecomment-349786380. Why is that a problematic limitation?
The only reason listed above to bar addressability is that it precludes initialization by a function; see #6386 (comment). Why is that a problematic limitation?
I may be missing something here, but even initialization by a function does not sound impossible. Excluding partial evaluation a-la graalvm, it should be possible to do initialization at runtime, taking care to store const values in pages that will be marked read-only as soon as initialization is over. It wouldn't be trivial to implement, but it shouldn't necessarily prevent addressability either.
@CAFxX Marking memory holding constants as read-only after initialization would certainly provide the most flexibility. But w/o evaluation at compile-time, the compiler won't know the values of those constants, and transitively, the values of any constant expressions depending on those constants. A variety of compile-time checks would need to become runtime checks. Not impossible (and perhaps still backward-compatible), but something to consider.
Hence the question whether the much simpler approach would provide enough benefit.
As structs are typically passed by pointer, the inability to use a constant struct where a pointer is required is a rather severe limitation.
To support compile-time evaluation, could the compiler generate offsets into a table for constant addresses that gets populated at runtime?
@changkun It's a good point that we should consider permitting const
for values of interface type. But it's still hard to permit initializing them with a function while retaining safety.
@networkimprov The reason to ban addressability is that if a program can take the address of a value, then we have no completely reliable way of preventing that program from using that pointer to change the value. A const
value that can be changed during program execution cannot really be described as a constant. Yes, this is a severe restriction.
The idea of making the memory read-only after it has been initialized is certainly tempting, but it's hard to see how to make that completely reliable in all cases.
Generating offsets into a table would imply that different values of the same type have different kinds of values. That seems difficult to implement.
There are all serious concerns and may well mean that the simple approach suggested in https://github.com/golang/go/issues/6386#issuecomment-613699012 is not really useful.
offsets into a table would imply that different values of the same type have different kinds of values
Sorry, I didn't follow; could you elaborate? (I meant that the compiler would replace pointers to constants with a table lookup yielding a pointer.)
making the memory read-only after it has been initialized is certainly tempting, but it's hard to see how to make that completely reliable
In what cases would that not work?
@networkimprov Perhaps I don't understand your suggestion. The issue is how to handle a composite literal with an embedded pointer, where that pointer might point to some package-scope variable that can in turn by changed while the program is running. The problem is that the struct field (say) has a pointer type. I think you are suggesting that the pointer type could be represented not as a pointer, but as an index into a table. But then we have a pointer type that could be either a normal pointer value or an index into a table. How does arbitrary code, which may be in a different package, know how to interpret that value? As I say, perhaps I misunderstand your suggestion, so can you clarify? Thanks.
Making memory read-only after it has been initialized would not work if we can't reliably identify and collect all the memory in question. We can only mark entire pages as read-only, so we have to be sure that all read-only memory is in one set of pages while all modifiable memory is in a different set of pages. Suppose now that we initialize a constant value with a slice of some non-constant variable, where the values being slices themselves have pointers. What should we do and how can we make it completely reliable?
Suppose now we initialize a constant value with a slice of some non-constant variable ... ... a composite literal with an embedded pointer, where that pointer might point to some package-scope variable
Wouldn't those fail as initialization by non-constant expression? Shouldn't pointers that are constant only reference constants?
you are suggesting that the pointer type could be represented not as a pointer, but as an index into a table. But then we have a pointer type that could be either a normal pointer value or an index into a table.
Apologies if this is naive, but I'm suggesting the compiler produce a complete table lookup at any site taking the address of a constant. It can't just substitute an offset for an address.
const kVa = 1 // address placed in const_addrs at startup
const kVb = 2 // ditto
const kVc = 3 // not in const_addrs
const kPa = &kVa // not in const_addrs
...
f(&kVa, &kVb) // compiles to f((*int)(const_addrs + 0), (*int)(const_addrs + 8))
func f(a, b *int) { *a = 9 } // runtime error for above call
*kPa = 9 // compiler error
OK, even if we only permit pointers to constant values, still other code can pull out those pointers and modify the values to which they point, so that can only be permitted if we can be absolutely certain that we can always put those values in read-only memory.
I think I now understand what you are getting at with your constant offset, but I don't understand how that solves the problem. That is an implementation that we could use if we assume that we can always reliably identify which memory has to be read-only, and if we assume that we have a way to initialize that memory and then make it read-only. I find it hard to convince myself that both of those are true on all the platforms for which Go works.
The offsets into a table of addresses are meant to support compile-time evaluation, where each address of a constant needs a compile-time value, raised above by @griesemer.
On which platforms do you think read-only memory could be difficult to achieve, and why? Maybe we could pull in domain experts to comment on them...
In what cases would it be difficult to determine that an object should be read-only?
@networkimprov Let's look at a concrete example:
type List struct {
next *List
data int
}
const c = List{next: &List{next: &List{}}}
var v = List{next: &List{next: &List{}}}
Are you suggesting that the next field behaves differently when we have a "constant list" vs in the normal case (offset vs regular pointer)?
Consider wasm, for example. Also, the bare metal tamago port that is under discussion.
I completely agree that there may be simpler options with a narrower scope. But just to ensure I am getting the right picture:
But w/o evaluation at compile-time, the compiler won't know the values of those constants, and transitively, the values of any constant expressions depending on those constants.
The compiler wouldn't know the values of those constants(-at-runtime), but would know their addresses, so other constants expressions that depend on those values would become constant(-at-runtime) as well, i.e. transitevely be initialized at startup, and then marked read-only.
The compiler would need to track that these are constants(-at-runtime) and report error if code attempts to modify their value.
In a sense, they would behave like const
for writes, and like var
for reads.
As compilers improve and are progressively able to partially evaluate more at compile-time, more of these constant-at-runtime instances would turn into regular constants and not require initialization at runtime.
A variety of compile-time checks would need to become runtime checks.
Even in the model described just now? Wouldn't marking read-only the memory used to store the constants at runtime sidestep additional runtime checks?
@CAFxX No, it wouldn't. For instance, given an array a
and a constant expression x
, right now a[x]
will not require a runtime index bounds check. But if we don't know the exact value of x
, it will need an index bound check at runtime. The compiler may become smarter over time, but we could still not guarantee the constant evaluation in general.
I am not saying this is a show-stopper, but it is something that would have to be taken into account and which would change existing behavior. What now leads to a compile-time error may lead to a runtime panic if the compiler cannot evaluate all constant values at compile time.
Are you suggesting that the next field behaves differently when we have a "constant list" ...?
EDIT: there would be no difference to the user between the two List
chains you defined, except that for the const
one:
a) c.next.data = v
gives a compiler error,
b) var p = &c ... p.next.data = v
gives a runtime error.
If you printed a .next
field, you'd see an address from the const_addrs
table I described in https://github.com/golang/go/issues/6386#issuecomment-614311625.
Note that I am not advocating "constant-at-runtime" semantics.
Consider wasm, for example. Also, the bare metal tamago port that is under discussion.
@ianlancetaylor wasm lacks rather a lot of features; should a nascent world limit Go progress on mature platforms? And it appears this issue has been raised: https://github.com/WebAssembly/design/issues/1278.
Re tamago, I'd imagine they'll happily evolve as Go does; cc @abarisani.
@networkimprov What happens if one assigns that const list c
to variable v
in my example?
That copies the outermost List
into a variable, with same effect as:
const kL = List{next: &List{}}
var vL = List{next: &kL}
...
vL.data = v // ok
kL.data = v // compiler error
kL.next.data = v // compiler error
vL.next.data = v // runtime error
Re tamago, I'd imagine they'll happily evolve as Go does; cc @abarisani.
I can confirm.
It is unbelievable that we cannot write constant errors:
- const ErrVerification = errors.New("crypto/rsa: verification error") + var ErrVerification = errors.New("crypto/rsa: verification error")
You can though @changkun...
type Error string
func (e Error) Error() string {
return string(e)
}
const ErrVerification = Error("crypto/rsa: verification error")
You can though @changkun...
type Error string func (e Error) Error() string { return string(e) } const ErrVerification = Error("crypto/rsa: verification error")
No. It does not solve the problem generally, because this approach cannot be applied to const errChain = wrap(errArgs)
. The fundamental problem is that we are missing immutable data, whereas typecasting here is just a verbose workaround for unpacked error.
Just wanted to bump / voice immense support for this. As @changkun notes, this is honestly critical for security of the code. Additionally, its huge for API safety. Theres a large class of situations where you want publicly exported structs that other modules should be able to Read, but not accidentally set.
(I mean these can still be gotten around via many unsafe hacks, but thats not the point. The points to get people to not accidentally mess up, due to inability to provide a Safe API)
Just a remainder reminder that, if arrays could be declared as constants, then there is a parsing ambiguity in the following code:
type C[T [int]*string] struct{}
It looks the custom generics feature has sentenced this proposal to death.
const S = [2]int{1, 2}
const T = 2
// The following declaration is always thought as a generic type declaration.
type BoolArray [S[1] * T]bool // T (untyped int constant 2) is not a type
// If S is a constant array, then the above line may also viewed as array type declaration.
[update]: To avoid absolutely sentencing the constant array proposal to death, elements of constant array must not be treated as constants, just as elements of constant strings are not treated as constants (they are just immutable but not constants).
Any progress, this has been proposed for nearly ten years.
I think a more interesting approach would be to try and adapt the untyped nature of Go's const
s to other Kinds:
package main
func main() {
const aConst = {
1: "a",
3: "b",
5: "c",
}
var aMap1 map[int]string = aConst // OK
var aMap2 map[any]any = aConst // OK
var aSlice []string = aConst // OK, results in a slice of length 6
var aArray [4]string = aConst // Index out of bounds error
const bConst = {
"name": "bob",
"age": 30,
}
var bMap1 map[string]any = bConst // OK
var bMap2 map[string]int = bConst // Incompatible assign error due to "name": "bob"
var bStruct1 struct { // OK
name string
email string
age int
} = bConst
var bStruct2 struct { // Incompatible assign error due to lack of an "age" field
name string
} = bConst
}
by RickySeltzer: