Testing Emitter func with channel return value in go - testing

I am having a hard time getting my test for my emitter function which passes results through a channel for a data pipeline. This function will be triggered periodically and will pull records from the database. I compiled an stripped done version for this question the real code would more complex but would follow the same pattern. For testing I mocked the access to the database because I want to test the behavoir of the Emitter function.
I guess code is more than words:
This is the method I want to test:
//EmittRecord pull record from database
func EmittRecord(svc Service, count int) <-chan *Result {
out := make(chan *Result)
go func() {
defer close(out)
for i := 0; i < count; i++ {
r, err := svc.Next()
if err != nil {
out <- &Result{Error: err}
continue
}
out <- &Result{Payload: &Payload{
Field1: r.Field1,
Field2: r.Field2,
}, Error: nil}
}
}()
return out
}
I have a couple of types with an interface:
//Record is a Record from db
type Record struct {
Field1 string
Field2 string
}
//Payload is a record for the data pipeline
type Payload struct {
Field1 string
Field2 string
}
//Result is a type for the data pipeline
type Result struct {
Payload *Payload
Error error
}
//Service is an abstraction to access the database
type Service interface {
Next() (*Record, error)
}
This is my service Mock for testing:
//MockService is a struct to support testing for mocking the database
type MockService struct {
NextMock func() (*Record, error)
}
//Next is an Implementation of the Service interface for the mock
func (m *MockService) Next() (*Record, error) {
if m.NextMock != nil {
return m.NextMock()
}
panic("Please set NextMock!")
}
And finally this is my test method which does not work. It does not hit the done case and das not hit the 1*time.Second timeout case either ... the test just times out. I guess I am missing something here.
func TestEmitter(t *testing.T) {
tt := []struct {
name string
svc runner.Service
expectedResult runner.Result
}{
{name: "Database returns error",
svc: &runner.MockService{
NextMock: func() (*runner.Record, error) {
return nil, fmt.Errorf("YIKES")
},
},
expectedResult: runner.Result{Payload: nil, Error: fmt.Errorf("RRRR")},
},
{name: "Database returns record",
svc: &runner.MockService{
NextMock: func() (*runner.Record, error) {
return &runner.Record{
Field1: "hello",
Field2: "world",
}, nil
},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
done := make(chan bool)
defer close(done)
var output <-chan *runner.Result
go func() {
output = runner.EmittRecord(tc.svc, 1)
done <- true
}()
found := <-output
<-done
select {
case <-done:
case <-time.After(1 * time.Second):
panic("timeout")
}
if found.Error.Error() != tc.expectedResult.Error.Error() {
t.Errorf("FAIL: %s, expected: %s; but got %s", tc.name, tc.expectedResult.Error.Error(), found.Error.Error())
} else if reflect.DeepEqual(found.Payload, tc.expectedResult.Payload) {
t.Errorf("FAIL: %s, expected: %+v; got %+v", tc.name, tc.expectedResult.Payload, found.Payload)
}
})
}
}
It would be great, if someone could give me an advice what I missing here and maybe some input how to verify the count of the EmittRecord function right now it is only set to 1
Thanks in advance
//Edited: the expectedResult as per Comment by #Lansana

Are you sure you have your expected results in the tests set to the proper value?
In the first slice in the test, you expect a fmt.Errorf("RRRR"), yet the mock returns a fmt.Errorf("YIKES").
And then later in the actual test conditionals, you do this:
if found.Error.Error() != "Hello" {
t.Errorf("FAIL: %s, expected: %s; but got %s", tc.name, tc.expectedResult.Error.Error(), found.Error.Error())
}
You are checking "Hello". Shouldn't you be checking if it's an error with the message "YIKES"?
I think your logic is good, but your test is just not properly written. Check my Go Playground example here and run the code. You will see there is no output or panics when you run it. This is because the code passes my test conditions in main.
You are adding more complexity to your test by more channels, and if those extra channels are invalid then you may have some false positives that make you think your business logic is bad. In this case, it actually seems to be working as it should.
Here is the highlight of the code from my playground example . (the part that tests your logic):
func main() {
svc1 := &MockService{
NextMock: func() (*Record, error) {
return nil, errors.New("foo")
},
}
for item := range EmittRecord(svc1, 5) {
if item.Payload != nil {
panic("item.Payload should be nil")
}
if item.Error == nil {
panic("item.Error should be an error")
}
}
svc2 := &MockService{
NextMock: func() (*Record, error) {
return &Record{Field1: "Hello ", Field2: "World"}, nil
},
}
for item := range EmittRecord(svc2, 5) {
if item.Payload == nil {
panic("item.Payload should have a value")
}
if item.Payload.Field1 + item.Payload.Field2 != "Hello World" {
panic("item.Payload.Field1 and item.Payload.Field2 are invalid!")
}
if item.Error != nil {
panic("item.Error should be nil")
}
}
}
The output from the above code is nothing. No panics. Thus, it succeeded.
Try simplifying your test to a working state, and then add more complexity from there. :)

Related

How to enumerate constants of a certain type

I'd like to ensure with a test, that for each APIErrorCode constant defined as below, the map APIErrorCodeMessages contains an entry. How can I enumerate all constants of a certain type in Go?
// APIErrorCode represents the API error code
type APIErrorCode int
const (
// APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
APIErrorCodeAuthentication APIErrorCode = 1000
// APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
APIErrorCodeInternalError APIErrorCode = 1001
)
// APIErrorCodeMessages holds all error messages for APIErrorCodes
var APIErrorCodeMessages = map[APIErrorCode]string{
APIErrorCodeInternalError: "Internal Error",
}
I've looked into reflect and go/importer and tried tools/cmd/stringer without success.
Basic concept
The reflect package does not provide access to exported identifiers, as there is no guarantee they will be linked to the executable binary (and thus available at runtime); more on this: Splitting client/server code; and How to remove unused code at compile time?
This is a source-code level checking. What I would do is write a test that checks if the number of error code constants matches the map length. The solution below will only check the map length. An improved version (see below) may also check if the keys in the map match the values of the constant declarations too.
You may use the go/parser to parse the Go file containing the error code constants, which gives you an ast.File describing the file, containing the constant declarations. You just need to walk through it, and count the error code constant declarations.
Let's say your original file is named "errcodes.go", write a test file named "errcodes_test.go".
This is how the test function could look like:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
errCodeCount := 0
// Range through declarations:
for _, dd := range f.Decls {
if gd, ok := dd.(*ast.GenDecl); ok {
// Find constant declrations:
if gd.Tok == token.CONST {
for _, sp := range gd.Specs {
if valSp, ok := sp.(*ast.ValueSpec); ok {
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
}
}
}
if exp, got := errCodeCount, len(APIErrorCodeMessages); exp != got {
t.Errorf("Expected %d err codes, got: %d", exp, got)
}
}
Running go test will result in:
--- FAIL: TestMap (0.00s)
errcodes_test.go:39: Expected 2 err codes, got: 1
The test properly reveals that there are 2 constant error code declarations, but the APIErrorCodeMessages map contains only 1 entry.
If we now "complete" the map:
var APIErrorCodeMessages = map[APIErrorCode]string{
APIErrorCodeInternalError: "Internal Error",
APIErrorCodeAuthentication: "asdf",
}
And run go test again:
PASS
Note: it's a matter of style, but the big loop may be written this way to decrease nesting level:
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
for _, name := range valSp.Names {
// Count those that start with "APIErrorCode"
if strings.HasPrefix(name.Name, "APIErrorCode") {
errCodeCount++
}
}
}
}
Full, improved detection
This time we will check the exact type of the constants, not their names. We will also gather all the constant values, and in the end we will check each if that exact constant value is in the map. If something is missing, we will print the exact values of the missing codes.
So here it is:
func TestMap(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "errcodes.go", nil, 0)
if err != nil {
t.Errorf("Failed to parse file: %v", err)
return
}
var keys []APIErrorCode
// Range through declarations:
for _, dd := range f.Decls {
gd, ok := dd.(*ast.GenDecl)
if !ok {
continue
}
// Find constant declrations:
if gd.Tok != token.CONST {
continue
}
for _, sp := range gd.Specs {
// Filter by APIErrorCode type:
valSp, ok := sp.(*ast.ValueSpec)
if !ok {
continue
}
if id, ok2 := valSp.Type.(*ast.Ident); !ok2 ||
id.Name != "APIErrorCode" {
continue
}
// And gather the constant values in keys:
for _, value := range valSp.Values {
bslit, ok := value.(*ast.BasicLit)
if !ok {
continue
}
keyValue, err := strconv.Atoi(bslit.Value)
if err != nil {
t.Errorf("Could not parse value from %v: %v",
bslit.Value, err)
}
keys = append(keys, APIErrorCode(keyValue))
}
}
}
for _, key := range keys {
if _, found := APIErrorCodeMessages[key]; !found {
t.Errorf("Could not found key in map: %v", key)
}
}
}
Running go test with an "incomplete" APIErrorCodeMessages map, we get the following output:
--- FAIL: TestMap (0.00s)
errcodes_test.go:58: Could not found key in map: 1000
Short of static code analysis, which generates your tests, you can't.
You'll just need to maintain a list of known types somewhere. The most obvious place is probably in your test:
func TestAPICodes(t *testing.T) {
for _, code := range []APIErrorCode{APIErrorCodeAuthentication, ...} {
// Do your test here
}
}
If you want the list defined closer to the code definitions, you could also put it in your main package:
// APIErrorCode represents the API error code
type APIErrorCode int
const (
// APIErrorCodeAuthentication represents an authentication error and corresponds with HTTP 401
APIErrorCodeAuthentication APIErrorCode = 1000
// APIErrorCodeInternalError represents an unknown internal error and corresponds with HTTP 500
APIErrorCodeInternalError APIErrorCode = 1001
)
var allCodes = []APIErrorCode{APIErrorCodeAuthentication, ...}
Or, if you're confident that your APIErrorCodeMessages map will be kept up-to-date, then you already have the solution. Just loop over that map in your test:
func TestAPICodes(t *testing.T) {
for code := range APIErrorCodeMessages {
// Do your tests...
}
}

Mocking functions in Golang to test my http routes

I'm totally confused figuring out how I can mock a function, without using any additional packages like golang/mock. I'm just trying to learn how to do so but can't find many decent online resources.
Essentially, I followed this excellent article that explains how to use an interface to mock things.
As so, I've re-written the function I wanted to test. The function just inserts some data into datastore. My tests for that are ok - I can mock the function directly.
The issue I'm having is mocking it 'within' an http route I'm trying to test. Am using the Gin framework.
My router (simplified) looks like this:
func SetupRouter() *gin.Engine {
r := gin.Default()
r.Use(gin.Logger())
r.Use(gin.Recovery())
v1 := r.Group("v1")
v1.PATCH("operations/:id", controllers.UpdateOperation)
}
Which calls the UpdateOperation function:
func UpdateOperation(c *gin.Context) {
id := c.Param("id")
r := m.Response{}
str := m.OperationInfoer{}
err := m.FindAndCompleteOperation(str, id, r.Report)
if err == nil {
c.JSON(200, gin.H{
"message": "Operation completed",
})
}
}
So, I need to mock the FindAndCompleteOperation() function.
The main (simplified) functions looks like this:
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {
ctx := context.Background()
q := datastore.NewQuery("Operation").
Filter("Unique_Id =", id).
Limit(1)
var ops []Operation
if ts, err := db.Datastore.GetAll(ctx, q, &ops); err == nil {
{
if len(ops) > 0 {
ops[0].Id = ts[0].ID()
ops[0].Complete = true
// Do stuff
_, err := db.Datastore.Put(ctx, key, &o)
if err == nil {
log.Print("OPERATION COMPLETED")
}
}
}
}
err := errors.New("Not found")
return err
}
func FindAndCompleteOperation(ri OperationInfoer, id string, report Report) error {
return ri.FindAndCompleteOp(id, report)
}
type OperationInfoer struct{}
To test the route that updates the operation, I have something like so:
FIt("Return 200, updates operation", func() {
testRouter := SetupRouter()
param := make(url.Values)
param["access_token"] = []string{public_token}
report := m.Report{}
report.Success = true
report.Output = "my output"
jsonStr, _ := json.Marshal(report)
req, _ := http.NewRequest("PATCH", "/v1/operations/123?"+param.Encode(), bytes.NewBuffer(jsonStr))
resp := httptest.NewRecorder()
testRouter.ServeHTTP(resp, req)
Expect(resp.Code).To(Equal(200))
o := FakeResponse{}
json.NewDecoder(resp.Body).Decode(&o)
Expect(o.Message).To(Equal("Operation completed"))
})
Originally, I tried to cheat a bit and just tried something like this:
m.FindAndCompleteOperation = func(string, m.Report) error {
return nil
}
But that affects all the other tests etc.
I'm hoping someone can explain simply what the best way to mock the FindAndCompleteOperation function so I can test the routes, without relying on datastore etc.
I have another relevant, more informative answer to a similar question here, but here's an answer for your specific scenario:
Update your SetupRouter() function to take a function that can either be the real FindAndCompleteOperation function or a stub function:
Playground
package main
import "github.com/gin-gonic/gin"
// m.Response.Report
type Report struct {
// ...
}
// m.OperationInfoer
type OperationInfoer struct {
// ...
}
type findAndComplete func(s OperationInfoer, id string, report Report) error
func FindAndCompleteOperation(OperationInfoer, string, Report) error {
// ...
return nil
}
func SetupRouter(f findAndComplete) *gin.Engine {
r := gin.Default()
r.Group("v1").PATCH("/:id", func(c *gin.Context) {
if f(OperationInfoer{}, c.Param("id"), Report{}) == nil {
c.JSON(200, gin.H{"message": "Operation completed"})
}
})
return r
}
func main() {
r := SetupRouter(FindAndCompleteOperation)
if err := r.Run(":8080"); err != nil {
panic(err)
}
}
Test/mocking example
package main
import (
"encoding/json"
"net/http/httptest"
"strings"
"testing"
)
func TestUpdateRoute(t *testing.T) {
// build findAndComplete stub
var callCount int
var lastInfoer OperationInfoer
var lastID string
var lastReport Report
stub := func(s OperationInfoer, id string, report Report) error {
callCount++
lastInfoer = s
lastID = id
lastReport = report
return nil // or `fmt.Errorf("Err msg")` if you want to test fault path
}
// invoke endpoint
w := httptest.NewRecorder()
r := httptest.NewRequest(
"PATCH",
"/v1/id_value",
strings.NewReader(""),
)
SetupRouter(stub).ServeHTTP(w, r)
// check that the stub was invoked correctly
if callCount != 1 {
t.Fatal("Wanted 1 call; got", callCount)
}
if lastInfoer != (OperationInfoer{}) {
t.Fatalf("Wanted %v; got %v", OperationInfoer{}, lastInfoer)
}
if lastID != "id_value" {
t.Fatalf("Wanted 'id_value'; got '%s'", lastID)
}
if lastReport != (Report{}) {
t.Fatalf("Wanted %v; got %v", Report{}, lastReport)
}
// check that the correct response was returned
if w.Code != 200 {
t.Fatal("Wanted HTTP 200; got HTTP", w.Code)
}
var body map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatal("Unexpected error:", err)
}
if body["message"] != "Operation completed" {
t.Fatal("Wanted 'Operation completed'; got %s", body["message"])
}
}
You can't mock if you use globals that can't be mocked in an handler. Either your globals are mockable (i.e. declared as variables of interface type) or you need to use dependency injection.
func (oi OperationInfoer) FindAndCompleteOp(id string, report Report) error {...}
looks like a method of a struct, so you should be able to inject that struct into an handler, at the very least.
type OperationInfoer interface {
FindAndCompleteOp(id string, report Report) error
}
type ConcreteOperationInfoer struct { /* actual implementation */ }
func UpdateOperation(oi OperationInfoer) func(c *gin.Context) {
return func (c *gin.Context){
// the code
}
}
then mocking becomes a breeze in your tests :
UpdateOperation(mockOperationInfoer)(ginContext)
You can use a struct instead of closures
type UpdateOperationHandler struct {
Oi OperationInfoer
}
func (h UpdateOperationHandler ) UpdateOperation (c *gin.Context) {
h.Oi.FindAndCompleteOp(/* code */ )
}

Golang test with channels does not exit

The following Golang test never exits. I suspect it has something to do with a channel deadlock but being a go-noob, I am not very certain.
const userName = "xxxxxxxxxxxx"
func TestSynchroninze(t *testing.T) {
c, err := channel.New(github.ChannelName, authToken)
if err != nil {
t.Fatalf("Could not create channel: %s", err)
return
}
state := channel.NewState(nil)
ctx := context.Background()
ctx = context.WithValue(ctx, "userId", userName)
user := api.User{}
output, errs := c.Synchronize(state, ctx)
if err = <-errs; err != nil {
t.Fatalf("Error performing synchronize: %s", err)
return
}
for o := range output {
switch oo := o.Data.(type) {
case api.User:
user = oo
glog.Infof("we have a USER %s\n", user)
default:
t.Errorf("Encountered unexpected data type: %T", oo)
}
}
}
Here are the methods being tested.
type github struct {
client *api.Client
}
func newImplementation(t auth.UserToken) implementation.Implementation {
return &github{client: api.NewClient(t)}
}
// -------------------------------------------------------------------------------------
const (
kLastUserFetch = "lastUserFetch"
)
type synchronizeFunc func(implementation.MutableState, chan *implementation.Output, context.Context) error
// -------------------------------------------------------------------------------------
func (g *github) Synchronize(state implementation.MutableState, ctx context.Context) (<-chan *implementation.Output, <-chan error) {
output := make(chan *implementation.Output)
errors := make(chan error, 1) // buffer allows preflight errors
// Close output channels once we're done
defer func() {
go func() {
// wg.Wait()
close(errors)
close(output)
}()
}()
err := g.fetchUser(state, output, ctx)
if err != nil {
errors <- err
}
return output, errors
}
func (g *github) fetchUser(state implementation.MutableState, output chan *implementation.Output, ctx context.Context) error {
var err error
var user = api.User{}
userId, _ := ctx.Value("userId").(string)
user, err = g.client.GetUser(userId, ctx.Done())
if err == nil {
glog.Info("No error in fetchUser")
output <- &implementation.Output{Data: user}
state.SetTime(kLastUserFetch, time.Now())
}
return err
}
func (c *Client) GetUser(id string, quit <-chan struct{}) (user User, err error) {
// Execute request
var data []byte
data, err = c.get("users/"+id, nil, quit)
glog.Infof("USER DATA %s", data)
// Parse response
if err == nil && len(data) > 0 {
err = json.Unmarshal(data, &user)
data, _ = json.Marshal(user)
}
return
}
Here is what I see in the console (most of the user details removed)
I1228 13:25:05.291010 21313 client.go:177] GET https://api.github.com/users/xxxxxxxx
I1228 13:25:06.010085 21313 client.go:36] USER DATA {"login":"xxxxxxxx","id":00000000,"avatar_url":"https://avatars.githubusercontent.com/u/0000000?v=3",...}
I1228 13:25:06.010357 21313 github.go:90] No error in fetchUser
==========EDIT=============
Here is the relevant portion of the api package.
package api
type Client struct {
authToken auth.UserToken
http *http.Client
}
func NewClient(authToken auth.UserToken) *Client {
return &Client{
authToken: authToken,
http: auth.NewClient(authToken),
}
}
// -------------------------------------------------------------------------------------
type User struct {
Id int `json:"id,omitempty"`
Username string `json:"login,omitempty"`
Email string `json:"email,omitempty"`
FullName string `json:"name,omitempty"`
ProfilePicture string `json:"avatar_url,omitempty"`
Bio string `json:"bio,omitempty"`
Website string `json:"blog,omitempty"`
Company string `json:"company,omitempty"`
}
And the channel package
package channel
type Channel struct {
implementation.Descriptor
imp implementation.Implementation
}
// New returns a channel implementation with a given name and auth token.
func New(name string, token auth.UserToken) (*Channel, error) {
if desc, ok := implementation.Lookup(name); ok {
if imp := implementation.New(name, token); imp != nil {
return &Channel{Descriptor: desc, imp: imp}, nil
}
}
return nil, ErrInvalidChannel
}
and the implementation package...
package implementation
import "golang.org/x/net/context"
// -------------------------------------------------------------------------------------
// Implementation is the interface implemented by subpackages.
type Implementation interface {
// Synchronize performs a synchronization using the given state. A context parameters
// is provided to provide cancellation as well as implementation-specific behaviors.
//
// If a fatal error occurs (see package error definitions), the state can be discarded
// to prevent the persistence of an invalid state.
Synchronize(state MutableState, ctx context.Context) (<-chan *Output, <-chan error)
// FetchDetails gets details for a given timeline item. Any changes to the TimelineItem
// (including the Meta value) will be persisted.
FetchDetails(item *TimelineItem, ctx context.Context) (interface{}, error)
}
======Edit #2=======
This is the original Synchronize method. I removed some details in my testing to try and simplify the problem. By removing a go func call, I believe I introduced a new problem which could be confusing things.
Here is the original Synchronize method. There are some things with Wait Groups and a function array containing a single function because this method will eventually be synchronizing multiple functions.
func (g *github) Synchronize(state implementation.MutableState, ctx context.Context) (<-chan *implementation.Output, <-chan error) {
wg := sync.WaitGroup{}
output := make(chan *implementation.Output)
errors := make(chan error, 1) // buffer allows preflight errors
// Close output channels once we're done
defer func() {
go func() {
wg.Wait()
close(errors)
close(output)
}()
}()
// Perform fetch functions in separate routines
funcs := []synchronizeFunc{
g.fetchUser,
}
for _, f := range funcs {
wg.Add(1)
go func(f synchronizeFunc) {
defer wg.Done()
if err := f(state, output, ctx); err != nil {
errors <- err
}
}(f)
}
glog.Info("after go sync...")
return output, errors
}
I think the two problems are in
output <- &implementation.Output{Data: user}
the channel does not have a buffer. It will block until some other goroutine reads from it. But in your code is the same goroutine so it will block.
and second:
// Close output channels once we're done
defer func() {
go func() {
// wg.Wait()
close(errors)
close(output)
}()
}()
you launch a go routine when the routine exits. The goroutine is scheduled, the function returns but it never calls the goroutine.
I would suggest to unify all that logic in one:
func (g *github) Synchronize(state implementation.MutableState, ctx context.Context) (<-chan *implementation.Output, <-chan error) {
output := make(chan *implementation.Output)
errors := make(chan error, 1) // buffer allows preflight errors
go func() {
defer close(output)
defer close(errors)
err := g.fetchUser(state, output, ctx)
if err != nil {
errors <- err
}
}()
return output, errors
}

Portable way to detect different kinds of network error

I would like to identify what kind of error occurred in the network level. The only way I found was checking the error messages with a regular expression, but now I discovered that this messages can be in different languages (depending on the OS configuration), making it difficult to detect by regular expressions. Is there a better way to do it?
package main
import (
"github.com/miekg/dns"
"net"
"regexp"
)
func main() {
var c dns.Client
m := new(dns.Msg)
m.SetQuestion("3com.br.", dns.TypeSOA)
_, _, err := c.Exchange(m, "ns1.3com.com.:53")
checkErr(err)
m.SetQuestion("example.com.", dns.TypeSOA)
_, _, err = c.Exchange(m, "idontexist.br.:53")
checkErr(err)
m.SetQuestion("acasadocartaocuritiba.blog.br.", dns.TypeSOA)
_, _, err = c.Exchange(m, "ns7.storedns22.in.:53")
checkErr(err)
}
func checkErr(err error) {
if err == nil {
println("Ok")
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
println("Timeout")
} else if match, _ := regexp.MatchString(".*lookup.*", err.Error()); match {
println("Unknown host")
} else if match, _ := regexp.MatchString(".*connection refused.*", err.Error()); match {
println("Connection refused")
} else {
println("Other error")
}
}
Result:
$ go run neterrors.go
Timeout
Unknown host
Connection refused
I discover the problem when testing the system in a Windows OS with Portuguese as default language.
[EDIT]
I found a way to do it using the OpError. Here is the checkErr function again with the new approach. If someone has a better solution I will be very glad to known it!
func checkErr(err error) {
if err == nil {
println("Ok")
} else if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
println("Timeout")
} else if opError, ok := err.(*net.OpError); ok {
if opError.Op == "dial" {
println("Unknown host")
} else if opError.Op == "read" {
println("Connection refused")
}
}
}
[EDIT2]
Updated after seong answer.
func checkErr(err error) {
if err == nil {
println("Ok")
return
} else if netError, ok := err.(net.Error); ok && netError.Timeout() {
println("Timeout")
return
}
switch t := err.(type) {
case *net.OpError:
if t.Op == "dial" {
println("Unknown host")
} else if t.Op == "read" {
println("Connection refused")
}
case syscall.Errno:
if t == syscall.ECONNREFUSED {
println("Connection refused")
}
}
}
The net package works closely with your OS. For OS errors, the Go std library uses the pkg syscall. Have a look here: http://golang.org/pkg/syscall/
The net package can also return syscall.Errno type errors.
For a simpler code in you checkErr function, you could consider using a type switch (http://golang.org/doc/effective_go.html#type_switch).

Custom error handling in Go

I've been reading this blog post but I'm still not convinced I know exactly what to do to get custom errors that I can return from my functions and handle outside of them.
This is what I'm currently doing:
func doSomething() int {
x := 0
// Do something with x.
...
if somethingBadHappened {
return -1
}
if somethingElseBadHappened {
return -2
}
return x
}
This is what I'd like to be doing:
func doSomething() int, ? {
...
if somethingBadHappened {
return ?, err
}
if somethingElseBadHappened {
return ?, err2
}
return x, nil
}
But I'm not exactly sure how, and what to replace those question marks with.
You don't really need to return an int if you don't want to. You can do something like:
func doSomething() error {
...
if somethingBadHappened {
return errors.New("something bad happened")
}
if somethingElseBadHappened {
return errors.New("something else bad happened")
}
return nil
}
or if you want to return ints
func doSomething() (int, error) {
...
if somethingBadHappened {
return -1, errors.New("something bad happened")
}
if somethingElseBadHappened {
return -2, errors.New("something else bad happened")
}
return x, nil
}
be sure to import "errors" at the top.
If you want to test whether you got an error, you can do
x, err := doSomething()
if err != nil {
log.Println(err)
}
I would turn
func doSomething() int, ? {
...
if somethingBadHappened {
return ?, err
}
if somethingElseBadHappened {
return ?, err2
}
return x, nil
}
into
func doSomething() (r int, err error) {
...
if somethingBadHappened {
err = err1 // Whatever satisfies the `error` interface
return
}
if somethingElseBadHappened {
err = err2 // dtto
return
}
return x, nil
}
IOW, at the call site it is idiomatic* to ignore, never use or rely on, any other returned value if err != nil, so just don't care if r above has had been assigned some intermediate value or not.
(*) In the first approximatiom, i.e. if not stated otherwise. E.g. an io.Reader explicitly declares it can return both err == io.EOF and valid data at the same time:
When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, it returns the number of bytes read. It may return the (non-nil) error from the same call or return the error (and n == 0) from a subsequent call. An instance of this general case is that a Reader returning a non-zero number of bytes at the end of the input stream may return either err == EOF or err == nil. The next Read should return 0, EOF regardless.