There are many models for sharing information between communicating processes. One of the more elegant is Hoare's concept of channels. In this, there is no shared memory, so that none of the issues of accessing common memory arise. Instead, one process will send a message along a channel to another process. Channels may be synchronous, or asynchronous, buffered or unbuffered.
Go has channels as first order data types in the language. The canonical example of using channles is Erastophene's prime sieve: one goroutine generates integers from 2 upwards. These are pumped into a series of channels that act as sieves. Each filter is distinguished by a different prime, and it removes from its stream each number that is divisible by its prime. So the '2' goroutine filters out even numbers, while the '3' goroutine filters out multiples of 3. The first number that comes out of the current set of filters must be a new prime, and this is used to start a new filter with a new channel.
The efficacy of many thousands of goroutines communicating by many thousands of channels depends on how well the implementation of these primitives is done. Go is designed to optimise these, so this type of program is feasible.
Go also supports distributed channels using the netchan
package. But network communications are thousands of times slower than
channel communications on a single computer. Running a sieve on a
network over TCP would be ludicrously slow. Nevertheless, it gives
a programming option that may be useful in many situations.
Go's network channel model is somewhat similar in concept to the RPC model: a server creates channels and registers them with the network channel API. A client does a lookup for channels on a server. At this point both sides have a shared channel over which they can communicate. Note that communication is one-way: if you want to send information both ways, open two channels one for each direction.
In order to make a channel visible to clients, you need to
export it. This is done by creating an exporter
using NewExporter
. This takes two parameters,
the first being the underlying transport mechanism such as "tcp"
and the second being the network listening address (usually
just a port number. Any number of channels can then be exported
using this. At the time of export, the direction of communication
must be specified. Clients search for channels by name, which is a
string. This is speecified to the exporter.
The server then uses the channels in the normal way, reading or writing on them. We illustrate with an "echo" server which reads lines and sends them back. It needs two channels for this. The channel that the client writes to we name "echo-out". On the server side this is a read channel. Similarly, the channel that the client reads from we call "echo-in", which is a write channel to the server.
The server program is
/* EchoServer
*/
package main
import ("fmt"; "os"; "netchan")
func main() {
exporter, err := netchan.NewExporter("tcp", ":2345")
checkError(err)
echoIn := make(chan string)
echoOut := make(chan string)
exporter.Export("echo-in", echoIn, netchan.Send)
exporter.Export("echo-out", echoOut, netchan.Recv)
for {
if closed(echoOut) {
fmt.Println("Channel closed")
break
}
fmt.Println("Getting from echoOut")
s := <- echoOut
fmt.Println("Got from echoOut")
if closed(echoOut) {
fmt.Println("Channel closed")
break
}
fmt.Println("received", s)
if closed(echoIn) {
fmt.Println("Channel closed")
break
}
fmt.Println("Sending to echoIn")
echoIn <- s
fmt.Println("Sent to echoIn")
}
}
func checkError(err os.Error) {
if err != nil {
fmt.Println("Fatal error ", err.String())
os.Exit(1)
}
}
In order to find an exported channel, the client must import
it. This is created using NewImporter
which takes a
protocol and a network service address of "host:port". This is
then used to import a channel by name. Note that channel variables
are references, so you do not need to pass their
addresses to functions that change them.
The following client gets two channels to and from the echo server, and then writes and reads ten messages:
/* EchoClient
*/
package main
import ("fmt"; "netchan"; "os")
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
importer, err := netchan.NewImporter("tcp", service)
checkError(err)
fmt.Println("Got importer")
echoIn := make(chan string)
importer.Import("echo-in", echoIn, netchan.Recv)
fmt.Println("Imported in")
echoOut := make(chan string)
importer.Import("echo-out", echoOut, netchan.Send)
fmt.Println("Imported out")
for n := 1; n < 10; n++ {
echoOut <- "hello "
if closed(echoIn) {
fmt.Println("In closed")
}
s := <- echoIn
fmt.Println(s, n)
}
close(echoOut)
os.Exit(0)
}
func checkError(err os.Error) {
if err != nil {
fmt.Println("Fatal error ", err.String())
os.Exit(1)
}
}
Warning: this does not work correctly yet, due to incorrect closing of network channels. See issue 805
The online Go tutorial at http://golang.org/doc/go_tutorial.html has an example of multiplexing, where channels of channels are used. The idea is that instread of sharing one channel, a new communicator is given their own channel to have a privagye conversation. That is, a client is sent a channel from a server through a shared channel, and uses that private channel.
This doesn't work directly with network channels: a channel cannot be sent over a network channel. So we have to be a little more indirect. Each time a client connects to a server, the server builds new network channels and exports them with new names. Then it sends the names of these new channels to the client which imports them. It uses these new channels for communicaton.
A server is
/* EchoServer
*/
package main
import ("fmt"; "os"; "netchan"; "strconv")
var count int = 0
func main() {
exporter, err := netchan.NewExporter("tcp", ":2345")
checkError(err)
echo := make(chan string)
exporter.Export("echo", echo, netchan.Send)
for {
sCount := strconv.Itoa(count)
lock := make(chan string)
go handleSession(exporter, sCount, lock)
<- lock
echo <- sCount
count++
exporter.Drain(-1)
}
}
func handleSession(exporter *netchan.Exporter, sCount string, lock chan string) {
echoIn := make(chan string)
exporter.Export("echoIn" + sCount, echoIn, netchan.Send)
echoOut := make(chan string)
exporter.Export("echoOut" + sCount, echoOut, netchan.Recv)
fmt.Println("made " + "echoOut" + sCount)
lock <- "done"
for {
if closed(echoOut) {
break
}
s := <- echoOut
echoIn <- s
}
// should unexport net channels
}
func checkError(err os.Error) {
if err != nil {
fmt.Println("Fatal error ", err.String())
os.Exit(1)
}
}
and a client is
/* EchoClient
*/
package main
import ("fmt"; "netchan"; "os")
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
importer, err := netchan.NewImporter("tcp", service)
checkError(err)
fmt.Println("Got importer")
echo := make(chan string)
importer.Import("echo", echo, netchan.Recv)
fmt.Println("Imported in")
count := <- echo
fmt.Println(count)
echoIn := make(chan string)
importer.Import("echoIn" + count, echoIn, netchan.Recv)
echoOut := make(chan string)
importer.Import("echoOut" + count, echoOut, netchan.Send)
for n := 1; n < 10; n++ {
echoOut <- "hello "
s := <- echoIn
fmt.Println(s, n)
}
close(echoOut)
os.Exit(0)
}
func checkError(err os.Error) {
if err != nil {
fmt.Println("Fatal error ", err.String())
os.Exit(1)
}
}
/* EchoClient
*/
package main
import ("fmt"; "netchan"; "os")
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "host:port")
os.Exit(1)
}
service := os.Args[1]
importer, err := netchan.NewImporter("tcp", service)
checkError(err)
fmt.Println("Got importer")
echoIn := make(chan string)
importer.Import("echo-in", echoIn, netchan.Recv)
fmt.Println("Imported in")
echoOut := make(chan string)
importer.Import("echo-out", echoOut, netchan.Send)
fmt.Println("Imported out")
for n := 1; n < 10; n++ {
echoOut <- "hello "
if closed(echoIn) {
fmt.Println("In closed")
}
s := <- echoIn
fmt.Println(s, n)
}
close(echoOut)
os.Exit(0)
}
func checkError(err os.Error) {
if err != nil {
fmt.Println("Fatal error ", err.String())
os.Exit(1)
}
}