7

I am new to Go. I have read that encapsulation in Go is on the package level. I have a simple web controller use case. I have a struct which comes in as a JSON object and is Unmarshaled into the struct type.

type User struct{
    Name String `json:"name"`
    //Other Variables
}

Now a json can be unmarshaled into type User Struct by json.Unmarshal([]byte). However, this User struct is available to other packages too. How do I make sure that only methods related to User are accessible by other packages.

One solution I could think of :

type User struct{
    name String
}

type UserJSON struct{
    Name String `json:"name"`
}

func DecodeJSONToUser(rawJSON []byte) (User,error) {
    var userJSON UserJSON
    err := json.Unmarshal(rawJSON,&userJSON)
    //Do error handling
    return User{name:userJSON.Name},nil
}

Is there a GOish way to achieve this ?

rhumbaJi
  • 73
  • 1
  • 4
  • 1
    yes there is a method the struct fields that you don't want to be used by other packages don't export them. – Himanshu Mar 06 '18 at 08:58
  • @GrzegorzŻur I wish that other packages work on the User Struct, for instance calling User.GetName() instead of directly calling User.Name. – rhumbaJi Mar 06 '18 at 09:05
  • @Himanshu True, but think of this use case where every member of the struct User comes in the JSON and hence, json.Unmarshal would require all struct fields to be exported. Now, who stops something like User.Name = "some_other_name" ? – rhumbaJi Mar 06 '18 at 09:07
  • create a package assign the struct in that unmarshal your data and do not import that package in the main so that no other user can modify the struct except the package. Encapsulate your struct inside a package – Himanshu Mar 06 '18 at 09:15

1 Answers1

13

You can use a package local struct with a public field so that this struct will not be visible outside the package. Then you can make this struct satisfy some public interface and you have your perfect decoupling:

package user

import "encoding/json"

type User interface {
    Name() string
}

type user struct {
    Username string `json:"name"`
}

func (u *user) Name() string {
    return "Mr. " + u.Username
}

func ParseUserData(data []byte) (User, error) {
    user := &user{}
    if err := json.Unmarshal(data, user); err != nil {
        return nil, err
    }
    return user, nil
}

And the corresponding test:

package user_test

import (
    "testing"

    "github.com/teris-io/user"
)

func TestParseUserData(t *testing.T) {
    data := []byte("{\"name\": \"Uncle Sam\"}")
    expected := "Mr. Uncle Sam"
    if usr, err := user.ParseUserData(data); err != nil {
        t.Fatal(err.Error())
    } else if usr.Name() != expected {
        t.Fatalf("expected %s, found %s", expected, usr.Name())
    }
}

➜ user git:(master) ✗ go test github.com/teris-io/user

ok github.com/teris-io/user 0.001s

You can also convert your package local object to some public object after unmarshaling.

Note: one of the comments mentions how pity it is that due to name clashes (field user.Name on the struct, and method User.Name on the interface) the interface needs to have a different method name. This is not necessary and the code above has been amended correspondingly: the field on the internal structure can have a different name from that in JSON, the corresponding annotation defines the mapping.

Oleg Sklyar
  • 9,834
  • 6
  • 39
  • 62
  • Why didn't you just suggest an edit for @Thomas' answer? The difference of both solutions is very subtle. – vasart Mar 06 '18 at 09:45
  • subtle differences sometimes make a crucial difference – Oleg Sklyar Mar 06 '18 at 10:04
  • Love this! One thing which bothers me a bit is the fact, that the getter `GetName()` should be called `Name()`. But in this case, you cannot because you need a public field inside `user` struct because of the tag. – Maki Vlach Mar 06 '18 at 15:56
  • 1
    @MilanVlach That is not quite that dramatic, changed the code to accommodate this requirement as well (see the note) – Oleg Sklyar Mar 06 '18 at 17:01