1. Opening Brace Can’t Be Placed on a Separate Line
In most other languages that use braces you get to choose where you place them. Go is different. You can thank automatic semicolon injection for this behavior.
Fails:
package main
import "fmt"
func main()
{ //error, can't have the opening brace on a separate line
fmt.Println("hello there!")
}
Compile Error:
/tmp/sandbox826898458/main.go:6: syntax error: unexpected semicolon or newline before {
Works:
import "fmt"
func main() {
fmt.Println("works!")
}
2. Unused Variables
If you have an unused variable your code will fail to compile. There’s an exception though. You must use variables you declare inside functions, but it’s OK if you have unused global variables. It’s also OK to have unused function arguments.
If you assign a new value to the unused variable your code will still fail to compile. You need to use the variable value somehow to make the compiler happy.
Fails:
package main
var gvar int //not an error
func main() {
var one int //error, unused variable
two := 2 //error, unused variable
var three int //error, even though it's assigned 3 on the next line
three = 3
func(unused string) {
fmt.Println("Unused arg. No compile error")
}("what?")
}
Compile Errors:
/tmp/sandbox473116179/main.go:6: one declared and not used
/tmp/sandbox473116179/main.go:7: two declared and not used
/tmp/sandbox473116179/main.go:8: three declared and not used
Works:
func main() {
var one int
_ = one
two := 2
fmt.Println(two)
var three int
three = 3
one = three
var four int
four = four
}
3. Unused Imports
Your code will fail to compile if you import a package without using any of its exported functions, interfaces, structures, or variables.
If you really need the imported package you can use the blank identifier, _
, as its package name to avoid this compilation failure. The blank identifier is used to import packages for their side effects.
Fails:
package main
import (
"fmt"
"log"
"time"
)
func main() {
}
Compile Errors:
/tmp/sandbox627475386/main.go:4: imported and not used: “fmt”
/tmp/sandbox627475386/main.go:5: imported and not used: “log”
/tmp/sandbox627475386/main.go:6: imported and not used: “time”
Works:
package main
import (
_ "fmt"
"log"
"time"
)
var _ = log.Println
func main() {
_ = time.Now
}
4. Strings Can’t Be “nil”
This is a gotcha for developers who are used to assigning “nil” identifiers to string variables.
Fails:
package main
func main() {
var x string = nil //error
if x == nil { //error
x = "default"
}
}
Compile Errors:
/tmp/sandbox630560459/main.go:4: cannot use nil as type string in assignment
/tmp/sandbox630560459/main.go:6: invalid operation: x == nil (mismatched types string and nil)
Works:
package main
func main() {
var x string //defaults to "" (zero value)
if x == "" {
x = "default"
}
}
5. Unexpected Values in Slice and Array “range” Clauses
This can happen if you are used to the “for-in” or “foreach” statements in other languages. The “range” clause in Go is different. It generates two values: the first value is the item index while the second value is the item data.
Bad:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for v := range x {
fmt.Println(v) //prints 0, 1, 2
}
}
Good:
package main
import "fmt"
func main() {
x := []string{"a","b","c"}
for _, v := range x {
fmt.Println(v) //prints a, b, c
}
}
6. log.Fatal and log.Panic Do More Than Log
Logging libraries often provide different log levels. Unlike those logging libraries, the log package in Go does more than log if you call its Fatal*()
and Panic*()
functions. When your app calls those functions Go will also terminate your app.
package main
import "log"
func main() {
log.Fatalln("Fatal Level: log entry") //app exits here
log.Println("Normal Level: log entry")
}
7. Closing HTTP Response Body
When you make requests using the standard http library you get a http response variable. If you don’t read the response body you still need to close it. Note that you must do it for empty responses too. It’s very easy to forget especially for new Go developers.
Some new Go developers do try to close the response body, but they do it in the wrong place.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
defer resp.Body.Close()//not ok
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
This code works for successful requests, but if the http request fails the resp
variable might be nil
, which will cause a runtime panic.
The most common why to close the response body is by using a defer
call after the http response error check.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if err != nil {
fmt.Println(err)
return
}
defer resp.Body.Close()//ok, most of the time 🙂
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
Most of the time when your http request fails the resp
variable will be nil
and the err
variable will be non-nil
. However, when you get a redirection failure both variables will be non-nil
. This means you can still end up with a leak.
You can fix this leak by adding a call to close non-nil
response bodies in the http response error handling block. Another option is to use one defer
call to close response bodies for all failed and successful requests.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
resp, err := http.Get("https://api.ipify.org?format=json")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(string(body))
}
8. Closing HTTP Connections
Some HTTP servers keep network connections open for a while (based on the HTTP 1.1 spec and the server “keep-alive” configurations). By default, the standard http library will close the network connections only when the target HTTP server asks for it. This means your app may run out of sockets/file descriptors under certain conditions.
You can ask the http library to close the connection after your request is done by setting the Close
field in the request variable to true
.
Another option is to add a Connection
request header and set it to close
. The target HTTP server should respond with a Connection: close
header too. When the http library sees this response header it will also close the connection.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
req, err := http.NewRequest("GET","http://golang.org",nil)
if err != nil {
fmt.Println(err)
return
}
req.Close = true
//or do this:
//req.Header.Add("Connection", "close")
resp, err := http.DefaultClient.Do(req)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
You can also disable http connection reuse globally. You’ll need to create a custom http transport configuration for it.
package main
import (
"fmt"
"net/http"
"io/ioutil"
)
func main() {
tr := &http.Transport{DisableKeepAlives: true}
client := &http.Client{Transport: tr}
resp, err := client.Get("http://golang.org")
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(string(body)))
}
If you send a lot of requests to the same HTTP server it’s ok to keep the network connection open. However, if your app sends one or two requests to many different HTTP servers in a short period of time it’s a good idea to close the network connections right after your app receives the responses. Increasing the open file limit might be a good idea too.