Implementing UDP vs TCP in Golang
Go is known to be a very capable systems programming language. Programmers enjoy it’s simplicity, ease of deployment, and performance when writing backend services. A key feature in any backend software service is networking communications. There are numerous application level protocols by which software can communicate over a network. Underneath the great majority of these protocols lie either
TCP in go
TCP’s support in Go is covered quite well in numerous articles and resources. Obviously, TCP is popular because it’s the core network layer under the HTTP protocol which proudly powers most of the internet. Let’s cover a couple of practical points about TCP in Go.
First it’s important to bring up Go’s net package , which is our master key for anything network or ethernet in Go. Inside the net package, there are a bunch of concrete types that support TCP. These types are TCPAddr, TCPConn, and TCPListener. If you are curious, take some time to explore them.
However, in the majority of cases, we don’t need to use these types directly unless we need to access advanced properties of the connection. Go’s net package supports some interfaces that not only expose TCP but also any stream oriented networking protocol like TCP, unix , or unixpacket. These interfaces are
UDP in Go
UDP’s Go support on the other hand is not well covered in blogs and forums like TCP. UDP is a very important protocol for modern software, and there are cases where UDP makes sense as our software’s networking protocol.
The net package supports some concrete types for UDP, chief among them are UDPConn, and UDPAddr. Most of the examples I found online use those types directly, however there is a more elegant way to write UDP software in Go.
As in TCP, there are some more abstract interfaces which we can use for UDP communications. The most important is PacketConn, which delivers functionality that covers packet-oriented protocols like UDP, IP , or Unixgram.
UDP vs TCP in golang: client implementation
Now, it’s time to dive into actual code. If we use interfaces, implementing a client should be the same on both the TCP and the UDP versions. Let’s see how.
TCP:
//Connect TCP conn, err := net.Dial("tcp", "host:port") if err != nil { return err } defer conn.Close() //simple Read buffer := make([]byte, 1024) conn.Read(buffer) //simple write conn.Write([]byte("Hello from client"))
net.Dial() returns the Conn interface, which in turn supports Read and Write methods. Conn supports the super popular io.Reader and io.Writer interfaces which are implemented by numerous packages in Go like bufio which allows buffered I/O reads and writes.
We notice that net.Dial() took a “tcp” string as an argument, that’s how we told it to initiate a tcp connection in particular. The second argument to net.Dial() is just our destination address.
Now how about UDP clients? guess!!
//Connect udp conn, err := net.Dial("udp", "host:port") if err != nil { return err } defer conn.Close() //simple Read buffer := make([]byte, 1024) conn.Read(buffer) //simple write conn.Write([]byte("Hello from client"))
Pretty straight forward ha? The only difference was the first argument to the net.Dial() function, we used “udp” this time to signify our desire to create a UDP connection.
UDP VS TCP IN GOLANG: Server IMPLEMENTATION
The implementation story around UDP vs TCP server code is different. In case of TCP, we need to use the Listener interface to listen and accept TCP connections. Let’s see a code snippet:
// listen to incoming tcp connections l, err := net.Listen("tcp", "host:port") if err != nil { return err } defer l.Close() // A common pattern is to start a loop to continously accept connections for { //accept connections using Listener.Accept() c, err := l.Accept() if err!= nil { return err } //It's common to handle accepted connection on different goroutines go handleConnection(c) }
The code is simple and straight forward, it only needs some basic understanding of TCP servers: You listen to incoming connections, you accept them when they come, then you handle the connection by reading and writing data on it. Let’s see the simplest form of how to do reads and writes:
func handleConnection(c net.Conn) { //some code... //Simple read from connection buffer := make([]byte, 1024) c.Read(buffer) //simple write to connection c.Write([]byte("Hello from server")) }
Now if you haven’t already noticed: the connection returned by Listener.Accept() is of interface type Conn which was the same type used by the TCP client. It supports io.Reader and io.Writer which makes it compatible with a wide variety of Go packages out there.
Implementing a UDP server in Go follows a different path than TCP, instead of the listener and the Conn interfaces, it uses the PacketConn interface. let’s see how.
// listen to incoming udp packets pc, err := net.ListenPacket("udp", "host:port") if err != nil { log.Fatal(err) } defer pc.Close() //simple read buffer := make([]byte, 1024) pc.ReadFrom(buffer) //simple write pc.WriteTo([]byte("Hello from client"), addr)
For UDP servers, we use net.ListenPacket() with string argument “udp” to announce readiness for incoming UDP interactions on the network to our server’s address. We can then read or write to UDP clients.
PacketConn however doesn’t support the popular io.reader and io.writer interface like type Conn. Instead it supports two special methods for reads and writes which are ReadFrom() and WriteTo(). WriteTo() needs to be provided the address to which we would like to send the data to, whereas ReadFrom() returns the address from which data was received.
This should serve as a practical dive into TCP vs UDP code implementation in Go. I hope you learned something new ?
Interested to learn more about Go? Please take a moment to check my Mastering Go Programming course. It provides a unique combination of covering deep internal aspects of the language, while also diving into very practical topics about using the language in production environments.