Saturday, September 21, 2019

go - How does an untyped constant 'n' get converted into a byte when passed as method arg?



I was watching this talk given at FOSDEM '17 about implementing "tail -f" in Go => https://youtu.be/lLDWF59aZAo



In the author's initial example program, he creates a Reader using a file handle, and then uses the ReadString method with delimiter '\n' to read the file line by line and print its contents. I usually use Scanner, so this was new to me.




Program below | Go Playground Link



package main

import (
"bufio"
"fmt"
"log"
"os"
)


func main() {
fileHandle, err := os.Open("someFile.log")
if err != nil {
log.Fatalln(err)
return
}
defer fileHandle.Close()

reader := bufio.NewReader(fileHandle)


for {
line, err := reader.ReadString('\n')
if err != nil {
log.Fatalln(err)
break
}
fmt.Print(line)
}


}


Now, ReadString takes a byte as its delimiter argument[https://golang.org/pkg/bufio/#Reader.ReadString]



So my question is, how in the world did '\n', which is a rune, get converted into a byte? I am not able to get my head around this. Especially since byte is an alias for uint8, and rune is an alias for int32.



I asked the same question in Gophers slack, and was told that '\n' is not a rune, but an untyped constant. If we actually created a rune using '\n' and passed it in, the compilation would fail. This actually confused me a bit more.



I was also given a link to a section of the Go spec regarding Type Identity => https://golang.org/ref/spec#Type_identity




If the program is not supposed to compile if it were an actual rune, why does the compiler allow an untyped constant to go through? Isn't this unsafe behaviour?



My guess is that this works due to a rule in the Assignability section in the Go spec, which says




x is an untyped constant representable by a value of type T.




Since '\n' can indeed be assigned to a variable of type byte, it is therefore converted.




Is my reasoning correct?


Answer



TL;DR Yes you are correct but there's something more.



'\n' is an untyped rune constant. It doesn't have a type but a default type which is int32 (rune is an alias for int32). It holds a single byte representing the literal "\n", which is the numeric value 10:



package main

import (

"fmt"
)

func main() {
fmt.Printf("%T %v %c\n", '\n', '\n', '\n') // int32 10 (newline)
}


https://play.golang.org/p/lMjrTFDZUM




The part of the spec that answers your question lies in the § Calls (emphasis mine):




Given an expression f of function type F,



f(a1, a2, … an)


calls f with arguments a1, a2, … an. Except for one
special case, arguments must be single-valued expressions assignable

to the parameter types of F
and are evaluated before the function is
called.




"assignable" is the key term here and the part of the spec you quoted explains what it means. As you correctly guessed, among the various rules of assignability, the one that applies here is the following:




x is an untyped constant representable by a value of type T.





In our case this translates to:




'\n' is an untyped (rune) constant representable by a value of type byte




The fact that '\n' is actually converted to a byte when calling ReadString() is more apparent if we try passing an untyped rune constant wider than 1 byte, to a function that expects a byte:



package main


func main() {
foo('α')
}

func foo(b byte) {}


https://play.golang.org/p/W0EUZppWHH



The code above fails with:





tmp/sandbox120896917/main.go:9: constant 945 overflows byte




That's because 'α' is actually 2 bytes, which means it cannot be converted to a value of type byte (the maximum integer a byte can hold is 255 while 'α' is actually 945).



All this is explained in the official blog post, Constants.


No comments:

Post a Comment

hard drive - Leaving bad sectors in unformatted partition?

Laptop was acting really weird, and copy and seek times were really slow, so I decided to scan the hard drive surface. I have a couple hundr...