Why is string(int) a valid cast operation?

polaris · · 175 次点击    
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
<p>Today I&#39;ve found out that this is valid in Go (unexpected)</p> <pre><code>string(65) </code></pre> <p>Where as this isn&#39;t (as expected)</p> <pre><code>int(string(65)) </code></pre> <p>Here&#39;s proof <a href="https://play.golang.org/p/kHHKHb7ISV">https://play.golang.org/p/kHHKHb7ISV</a> and here&#39;s the &#34;feature&#34; documented <a href="https://golang.org/ref/spec#Conversions_to_and_from_a_string_type">https://golang.org/ref/spec#Conversions_to_and_from_a_string_type</a>.</p> <p>While I&#39;m trying to get over my disbelief that this is possible, I&#39;d like to understand why in a relatively logical and uneventful type system something like this happened... There must be some historical context for this, but it seems like a big, bad oversight from the language creators.</p> <p><del>At the very least, I expected all cast operations to be reversible, as it is with all other types I know.</del></p> <p>EDIT: Removed the part about reversible. Bad wording on my side.</p> <p>UPDATE: Got an answer from Rob Pike <a href="https://www.reddit.com/r/golang/comments/78hxd7/why_is_stringint_a_valid_cast_operation/dou5s1g/">https://www.reddit.com/r/golang/comments/78hxd7/why_is_stringint_a_valid_cast_operation/dou5s1g/</a></p> <p>Thanks to everybody who replied for your input and for certain corrections and insights.</p> <hr/>**评论:**<br/><br/>robpike: <pre><p>The conversion string(int) was added at the dawn of time because it provided, before fmt, the simplest way to convert an integer to a string. In other words, it was just a hack, nothing principled.</p> <p>I think it should disappear in Go 2. <a href="https://github.com/golang/go/issues/3939">https://github.com/golang/go/issues/3939</a></p></pre>goomba_gibbon: <pre><p>It definitely seems like a hack given how it behaves in some cases. I take it you&#39;re quite familiar with UTF-8, so I&#39;ll take your word for it :)</p> <p>Thanks for creating the issue and linking it here!</p> <p>Personally I&#39;d prefer to see Itoa everywhere as it&#39;s more consistent with Atoi anyway.</p></pre>dlsniper: <pre><p>Int to string is a well understood conversion, there&#39;s nothing uncommon about, it&#39;s boring.</p> <p>String to int brings the following issues: overflows, invalid strings as numbers, strings starting with numbers then characters, empty strings. All of these are cases which can happen, and, sooner or later, will happen. How should the runtime handle this? Panic? Silent error? There is no error returned from casting so silent would be the only option. But then what if you could handle this better than the runtime chooses to do it?</p> <p>That&#39;s why it&#39;s not always possible to have a reversible cast operation.</p> <p>Also see <a href="https://godoc.org/strconv#Itoa">https://godoc.org/strconv#Itoa</a> and <a href="https://godoc.org/strconv#Atoi">https://godoc.org/strconv#Atoi</a> and their implementation. </p></pre>deafmalice: <pre><p>While I agree that int-to-string is kinda boring, and maybe well understood, I don&#39;t see any good reason for it to be accepted by the compiler, especially in a language that is so strictly typed. I can understand a int slice, since it&#39;s the same as a byte slice, but a single byte does not make a string...</p> <p>As for reversible, I did not mean that I expect int(string) to work. That was bad wording from my side. I would much prefer int-to-string type casts not be allowed, than have something like this.</p></pre>: <pre><p>[deleted]</p></pre>Sythe2o0: <pre><p>Just fyi-- string(123) will -not- result in &#34;123&#34;, but a character, specifically &#34;{&#34;. This is equivalent to <a href="https://docs.python.org/2/library/functions.html#chr">python&#39;s <code>chr</code></a>, for example.</p></pre>titpetric: <pre><p>I stand corrected. Also equivalent to chr/ord in PHP. And I guess many others. Somebody already mentioned that you can convert back from a rune, so you&#39;d have to do something like <code>func ord(s string) int { return int(s[0]) }</code>. Also valid as a null char, but I&#39;m not exactly sure you should use it for that purpose as there&#39;s also a decimal/octal notation: <a href="https://play.golang.org/p/Ehzlsh6234" rel="nofollow">https://play.golang.org/p/Ehzlsh6234</a></p></pre>ROFLLOLSTER: <pre><p>Go does not have a strong type system, Haskell does.</p></pre>Shammyhealz: <pre><p>That doesn&#39;t work, but this does:</p> <pre><code>int(rune(string(65)[0]))) </code></pre> <p>Strings aren&#39;t sized, where runes are. What would the type conversion for &#34;aaa&#34; be? Do you add the value of the UTF-8 characters together? Do you just concatenate them like strings? There&#39;s not really a sane answer.</p> <p>If you convert it to a single rune, you know it&#39;s only one character, and you can convert that to an int without all of the strange questions.</p></pre>deafmalice: <pre><p>Sorry for the confusion on the reversible part. That was bad wording.</p> <p>I would prefer that string(0) not be allowed. I agree that int(&#34;ABC&#34;) is dumb.</p></pre>Shammyhealz: <pre><p>string(0) has a logical conversion though. You&#39;re thinking of it as passing in an int, which is what you&#39;re doing in a literal sense, but in the contextual sense, you&#39;re passing in the value of a UTF-8 character. It&#39;s basically just letting the compiler know that it should start interpreting a given block of memory as a UTF-8 character rather than an int.</p> <p>They could add a UTF8Value type, but then you end up doing this:</p> <pre><code>string(UTF8Value(0)) </code></pre> <p>Which doesn&#39;t really accomplish anything, is harder to read (I think) and adds a few key presses without getting any benefit for it.</p></pre>deafmalice: <pre><p>A string is a slice of bytes. A UTF8 character is just that, a character. Not exactly a slice.</p> <p>It would&#39;ve been more logical to do:</p> <pre><code>string([]byte{1}) </code></pre> <p>Go already makes the programmer write pretty verbose and sometimes hard to read code. Adding readability and saving a few keypresses for a relatively strange edge case is kind of a moot point.</p></pre>Shammyhealz: <pre><blockquote> <p>A string is a slice of bytes. A UTF8 character is just that, a character. Not exactly a slice.</p> </blockquote> <p>If you go further down, a byte is just a type alias for uint8. So a string is a slice of unsigned integers.</p> <blockquote> <p>string([]byte{1})</p> </blockquote> <p>That doesn&#39;t work for non-ASCII characters. byte is an alias to uint8, so it&#39;s maximum value is 255. UTF-8 characters are up to 4 bytes, so they can be way higher than that. If I pass a UTF-8 int in there, it overflows and wraps around.</p> <p>That&#39;s also not to mention that that code snippet is what Go does under the hood. Take the int, convert it to a uint8, and then use that memory as a string. Go is just saving you the step of explicitly converting to a uint8.</p> <p>To be honest, I think this is the sane way of handling it. Yours is much harder, and involves segmenting an int on memory boundaries to handle non-ASCII characters. In addition, if I work with ASCII and UTF-8, your solution requires me to have different code paths for each due to the differing lengths of characters in those encodings.</p> <blockquote> <p>Adding readability and saving a few keypresses for a relatively strange edge case is kind of a moot point.</p> </blockquote> <p>I don&#39;t think it&#39;s really all that strange of an edge case. Keyboard input is commonly addressed as the ASCII code for the key that was pressed, instead of the character as a string. It&#39;s also a feature of C, so I think they preserve it for feature parity.</p></pre>deafmalice: <pre><p>Just an update: got an answer from Rob Pike - <a href="https://www.reddit.com/r/golang/comments/78hxd7/why_is_stringint_a_valid_cast_operation/dou5s1g/" rel="nofollow">https://www.reddit.com/r/golang/comments/78hxd7/why_is_stringint_a_valid_cast_operation/dou5s1g/</a></p> <p>I was wrong about the byte part. Still, you can always do this</p> <pre><code>string([]rune(256, 267, 300) </code></pre> <p>So my point stands. As for C feature parity, I don&#39;t think Go ever strived for that.</p></pre>titpetric: <pre><p><a href="https://play.golang.org/p/Ehzlsh6234" rel="nofollow">https://play.golang.org/p/Ehzlsh6234</a> - if you want a null terminated string, string(0) is as good a way to produce a null character as any other options. No?</p></pre>deafmalice: <pre><p>Appending to 0 a []byte would make more sense, though, both from a intent perspective and from a performance one, as the string concat will result in the string being copied to anther memory location.</p></pre>titpetric: <pre><p>Append allocates, and I&#39;m reasonably sure that <code>string(0)</code> might be completely optimized away by the compiler. Go isn&#39;t a scripting language ;)</p> <p>Edit: as I suspected:</p> <pre><code>5000000 333 ns/op 0 B/op 0 allocs/op </code></pre> <p>Code:</p> <pre><code>func BenchmarkString(b *testing.B) { var q string for i := 0; i &lt; b.N; i++ { for j := 0; j &lt; 1000; j++ { q = string(0) + &#34;null&#34; + string(0) } q = &#34;&#34; } println(q) b.ReportAllocs() } </code></pre></pre>deafmalice: <pre><p>Seems like you&#39;re mistaking compiler optimisations for actual no-allocation behaviour. Because you&#39;re using an empty string and always assigning the same thing, the compiler notices that and optimises it away. </p> <p>You&#39;ll have to run this on a local machine, since the playground takes too long <a href="https://play.golang.org/p/Aiv4lvO-b5" rel="nofollow">https://play.golang.org/p/Aiv4lvO-b5</a>.</p> <p>With compiler optimisation and inlining:</p> <pre><code>$ go build bmark.go $ ./bmark -concat AppendFunc | Total allocs: 63, Bytes Allocated: 6168958912, AllocPerBytes: 0, String: 1000000000 2.57 ns/op Append | Total allocs: 66, Bytes Allocated: 12049299392, AllocPerBytes: 0, String: 2000000000 3.05 ns/op ConcatFunc | Total allocs: 0, Bytes Allocated: 0, AllocPerBytes: 0, String: 50000000 28.7 ns/op String | Total allocs: 30000000, Bytes Allocated: 60000240, AllocPerBytes: 1, String: 30000000 38.1 ns/op Empty String | Total allocs: 0, Bytes Allocated: 0, AllocPerBytes: 0, String: 100000000 10.2 ns/op Slice | Total allocs: 1, Bytes Allocated: 16, AllocPerBytes: 0, String: 2000000000 0.68 ns/op BigConcat | Total allocs: 1000475, Bytes Allocated: 503995716080, AllocPerBytes: 1, String: 1000000 49594 ns/op </code></pre> <p>Without optimisation and inlining. Notice how, for some reason, the empty string variant stays at 0 allocations: </p> <pre><code>$ go build -gcflags &#39;-N -l&#39; bmark.go $ ./bmark -concat AppendFunc | Total allocs: 55, Bytes Allocated: 1034565568, AllocPerBytes: 0, String: 200000000 8.24 ns/op Append | Total allocs: 60, Bytes Allocated: 2526492688, AllocPerBytes: 0, String: 500000000 3.45 ns/op ConcatFunc | Total allocs: 30000001, Bytes Allocated: 120000480, AllocPerBytes: 1, String: 30000000 45.8 ns/op String | Total allocs: 30000000, Bytes Allocated: 60000176, AllocPerBytes: 1, String: 30000000 41.5 ns/op Empty String | Total allocs: 0, Bytes Allocated: 0, AllocPerBytes: 0, String: 100000000 11.0 ns/op Slice | Total allocs: 1, Bytes Allocated: 16, AllocPerBytes: 0, String: 500000000 3.10 ns/op BigConcat | Total allocs: 1000517, Bytes Allocated: 503995719440, AllocPerBytes: 1, String: 1000000 49635 ns/op </code></pre></pre>TheMerovius: <pre><blockquote> <p>At the very least, I expected all cast operations to be reversible, as it is with all other types I know.</p> </blockquote> <p>What does &#34;reversible&#34; mean? There&#39;s <code>[]byte(string(b))</code>, which will return a value that&#39;s different from <code>b</code>, so that&#39;s not really reversible. Or there&#39;s <code>float64(int(math.Pi))</code>, which isn&#39;t equal to <code>math.Pi</code> - or <code>float32(float64(math.Pi)) != float64(float32(math.Pi))</code>.</p> <p>On a statically type-checked level there is <code>io.Reader(io.ReadCloser(r))</code>, which works, while <code>io.ReadCloser(io.Reader(r))</code> doesn&#39;t. Or, similarly, <code>io.Reader((*io.PipeReader)(r))</code>. We have <code>(chan&lt;- int)((chan int)(x))</code>, which can be okay, while <code>(chan int)((chan&lt;- int)(x))</code> can&#39;t. 2 of these 6 combinations work, the rest don&#39;t:</p> <pre><code>u := []byte(string([]rune(&#34;&#34;))) v := []byte([]rune(string(&#34;&#34;))) w := string([]rune([]byte(&#34;&#34;))) x := string([]byte([]rune(&#34;&#34;))) y := []rune(string([]byte(&#34;&#34;))) z := []rune([]byte(string(&#34;&#34;))) </code></pre> <p>A bunch of these are nit-picky… But I really don&#39;t think the generalization you made is valid that way :)</p></pre>deafmalice: <pre><p>Good point here. What I meant was to be able to do type1(type2(type1)) without the compiler complaining. I probably went a bit overboard with the reversible part. </p> <p>Losing some fidelity with going from a bigger type to a smaller one is understandable, but float-to-int is more or less in the same ballpark compared to int-to-string.</p> <p>As for your examples, most go from a specific type to a more generic type. Where as int to string kind of jumps the gun, string not coming close to being a &#34;general&#34; kind of int. But they do show that not all conversions are type acceptable. </p></pre>mcandre: <pre><p>What mind of string is produced by that? I guess it makes sense as a convenience for ASCII codes and such to be castable to strings, but I agree that this operation is somewhat awkward compared to casting int to rune.</p></pre>TheMerovius: <pre><p>It seems like a natural consequence of a) having rune be an int-type, meaning untyped integer constants can be used for them (and <code>&#39;x&#39;</code> being effectively an untyped integer constant) and b) enabling converting runes to string, i.e. enabling <code>foo(string(&#39;x&#39;))</code>.</p> <p>Honestly, it seems like an edge-case, but one that won&#39;t make a lot of problems in practice, so <em>shrug</em>?</p></pre>deafmalice: <pre><p>Do have problems with it... Recently changed a variable from int to string, and was hoping for a build fail for all situations and tests. Because of this conversion, there&#39;s no check for that and I&#39;ll have to comb a lot of code for this.</p></pre>skidooer: <pre><blockquote> <p>I&#39;ll have to comb a lot of code for this.</p> </blockquote> <p>You should be able to build a pretty simple static analyzer to detect occurrences across your codebase. As an added benefit, you can run it on future code to make sure the same problem doesn&#39;t creep back in.</p></pre>TheMerovius: <pre><p>That seems admittedly unfortunate (though the problem only seem to matter in cases where you&#39;d convert that variable to another string-type again; I agree, that it can&#39;t be excluded though). It might warrant a bug filed to consider changing this for Go 2.</p></pre>DeedleFake: <pre><p>It&#39;s so that you can do <code>string(&#39;C&#39;)</code>, I would assume.</p></pre>binaryblade: <pre><p>65 is a constant not a number, constants have some very special properties. string(65) says cast the constant to a string while int(string(65)) say cast a constant to a string and then cast a string to an int which then fails. </p></pre>deafmalice: <pre><p>Try this in the playground</p> <pre><code>var d int = 65 c = string(d) print(c) </code></pre></pre>


入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:muxilin131420 备注:入群;或加QQ群:815126783

175 次点击  
加入收藏 微博
0 回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传