Golang Custom Type Implementation to Big Framework

Use Case and Implementation of Custom Type

David Yappeter
4 min readFeb 11, 2023
source: https://go.dev/blog/gopher

In Go Programming Language, the data type is separated into several kinds of classes:

  • Basic Type: int , float64 , byte , rune , string , boolean , complex , etc.
  • Composite Type (Non-Reference): Arrays , Structs
  • Composite Type (Reference): Slices , Maps , Channel , Pointer , Function/Method
  • Interface: interface

But besides all of the built-in data types, we can create our data type by simply using type .
For Example:

type CustomString string // custom string type 

type MyStruct struct {
s CustomString
}

With this feature, we can implement the receiver method to our custom data type and implements some built-in interface like encoding/json or even database/sql .

Repository: github.com/david-yappeter/golang-custom-type-example

Hands-On

For this example, we will use github.com/gin-gonic/gin , an HTTP Web Framework for Golang.

First, initialize the project.

$ cd /path/to/go/src
$ mkdir project_name
$ cd project_name
$ go mod init your_module_name
$ go get github.com/gin-gonic/gin
$ touch main.go

main.go

I will explain it part by part.

gin use encoding/json to parse the content of an HTTP request and encoding/json itself have json.Marshaler and json.Unmarshaler that the implementation can be overwritten.

type Marshaler interface {
MarshalJSON() ([]byte, error)
}

type Unmarshaler interface {
UnmarshalJSON([]byte) error
}

First, we will create DateTime a struct. This struct will help us parse and format it forth and back between time.Time and string .


type DateTime struct {
time time.Time
}

// RFC3339 = "2006-01-02T15:04:05Z07:00"
func (dt DateTime) format() string {
return time.RFC3339
}

/*
This receiver function overwrite `fmt.Stringer` which use to print the output
type Stringer interface {
String() string
}
*/
func (dt DateTime) String() string {
return dt.time.Format(dt.format())
}

/*
This part implements `json.Marshaler`
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
*/
func (dt DateTime) MarshalJSON() ([]byte, error) {
return json.Marshal(dt.String())
}

/*
This part implements `json.Unmarshaler`
type Unmarshaler interface {
UnmarshalJSON([]byte) error
}
*/
func (dt *DateTime) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
panic(BadRequestError("not a valid string"))
}
if s == "" {
panic(BadRequestError("must not be empty"))
}
t, err := time.Parse(dt.format(), s)
if err != nil {
panic(BadRequestError("format must be YYYY-MM-DDTHH:mm:ssZ"))
}

dt.time = t

return nil
}

By creating the receiver function for DateTime which implements the json.Marshaler and json.Unmarshaler , it will automatically handle the JSON parsing and formatting at the API level.

And then, we will make the gin.Engine router.

var (
router *gin.Engine
routerOnce sync.Once
)

func getRouter() *gin.Engine {
routerOnce.Do(func() {
router = gin.New()

// panic handler
router.Use(func(ctx *gin.Context) {
defer func() {
if r := recover(); r != nil {
switch v := r.(type) {
case BadRequestError:
ctx.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
"error": v,
})
return
case error:
fmt.Println("log error: ", v)
default:
ctx.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": "internal server error",
})
}
}
}()

ctx.Next()
})

// simple routing
router.POST("/date-time", func(ctx *gin.Context) {
var request RequestContentDateTime
err := ctx.ShouldBind(&request)
if err != nil {
panic(err)
}

ctx.JSON(http.StatusOK, request)
})

})

return router
}

We initialize the router by using sync.Once to ensure it only initialize once, and then operating gin.New() with panic handler implementation for error and adding the route for testing.

We will create makeTestRequest a function for testing purpose of the router.


func makeTestRequest(method string, url string, body map[string]interface{}) *httptest.ResponseRecorder {
jsoned, err := json.Marshal(body)
if err != nil {
panic(err)
}

request, err := http.NewRequest(method, url, bytes.NewBuffer(jsoned))
if err != nil {
panic(err)
}
request.Header.Add("Content-Type", "application/json")

response := httptest.NewRecorder()

router := getRouter()

router.ServeHTTP(response, request)

return response
}

Finally, to test things out, we will call the makeTestRequest on main() function. This is the test result


var response *httptest.ResponseRecorder

// DateTime
response = makeTestRequest(http.MethodPost, "/date-time", map[string]interface{}{
"time_at": "2020-01-01T02:02:05+07:00",
})
fmt.Printf("%+v\n", response.Body.String()) // [200] {"time_at":"2020-01-01T02:02:05+07:00"}

response = makeTestRequest(http.MethodPost, "/date-time", map[string]interface{}{
"time_at": "",
})
fmt.Printf("%+v\n", response.Body.String()) // [400] {"error":"must not be empty"}
response = makeTestRequest(http.MethodPost, "/date-time", map[string]interface{}{
"time_at": true,
})
fmt.Printf("%+v\n", response.Body.String()) // [400] {"error":"not a valid string"}
response = makeTestRequest(http.MethodPost, "/date-time", map[string]interface{}{
"time_at": "wrong-format",
})
fmt.Printf("%+v\n", response.Body.String()) // [400] {"error":"format must be YYYY-MM-DDTHH:mm:ssZ"}

There is another example ArrayString on the repository or the entire code above. This is just a small example of the usage.

Where to go next?

Another implementation you can try to do is using database/sql by using their interfaces:

type Scanner interface {
Scan(b interface{}) error
}

type Valuer interface {
Value() (driver.Value,error)
// Value() (Value,error) on godev documentation
}

Scanner will be run when you try to fetch data and scan into a variable
Valuer will be run when you try to mutate the data from the database

That’s all about Golang Custom Type Implementation to Big Framework; I hope you all have a great day :).

--

--