How to Create a Central Input Data Validator in Golang

Updated on November 21, 2023
How to Create a Central Input Data Validator in Golang header image

Introduction

When developing any Golang project that relies on input data from untrusted sources, you must validate data before further processing. The only way you can achieve this functionality is by using the Golang if and case control structures. In simple terms, you've to analyze user-supplied data and choose whether to accept or reject it. Of course, this makes your stored data more accurate and eliminates any chances of logical errors.

While validating data with control structures sounds great, a poor approach can bloat your production source code. For instance, assume you're collecting data from 50 different input forms tied to separate Golang source files. In that case, you'll have to code extended validation logic in each file. In most cases, you'll be copy-pasting the logic between the different files. This approach is not convenient since it makes your code unmaintainable - in case of any slight change in your business logic, you'll have to edit each file independently.

Luckily, there is a better method that you can use, which is short, cleaner, and easier to maintain. In this guide, you'll create and implement a central input data validation logic that you can reuse in your Golang projects.

Prerequisites

To complete this tutorial, you require the following:

1. Create a project Directory

SSH to your server to complete the following steps.

  1. Create a project directory to separate your Golang source code files from the rest of the Linux files, making troubleshooting more straightforward in the future if you have problems.

     $ mkdir project
  2. Switch to the new project directory.

     $ cd project
  3. You'll now add all Golang project files under this directory.

2. Create the validator.go File

You'll create a single validator.go file that stores all your validation logic in this step. Irrespective of how big your project is, you can reuse the function in this file in any part of your application that requires input validation.

  1. Open a new validator.go file with nano.

     $ nano validator.go
  2. Paste the following information into the file.

     package main
    
     import (         
         "strings"
         "regexp"
         "strconv"
         "fmt"
     )
    
     func validate(params map[string]interface{}, validationRules map[string]string) (string) {
    
         error := ""
    
         for field, rules := range validationRules { 
    
             for _, rule := range strings.Split(rules, "|") {
    
                 fieldValue, ok := params[field]
    
                 if rule == "required" && ok == false {
                     error = error + " " + field + " is required.\r\n";
                 } 
    
                 if ok == true {
    
                     if rule == "alphanumeric" {
    
                         re := regexp.MustCompile("^[a-zA-Z0-9 ]*$")
    
                         if re.MatchString(fmt.Sprint(fieldValue)) == false {
                             error = error + "The value of '" +  field + "' is not a valid alphanumeric value.\r\n";
                         }
                     }
    
                     if rule == "integer" {
    
                         if _, err := strconv.ParseInt(fmt.Sprint(fieldValue), 10, 64); err != nil {
                             error = error + "The value of '" + field + "' is not a valid integer value.\r\n";
                         }
    
                     }
    
                     if rule == "float" {
    
                         if _, err := strconv.ParseFloat(fmt.Sprint(fieldValue), 10); err != nil {
                             error = error + "The value of '" + field + "' is not a valid float value.\r\n";
                         }
    
                     }
    
                 }
    
             }
    
         }
    
         return error    
     }
  3. Save and close the file when done.

  4. In the validator.go file, you're importing the following packages:

    • strings: You're using this package to split some validation rule separated by the vertical bar |.
    • regexp: This is a package that deals with regular expression. In this guide, you'll validate some fields by matching a particular pattern.
    • strconv: This package allows you to convert strings to other data types in the validation logic.
  5. Next, you've got a validate function that accepts two arguments.

     func validate(params map[string]interface{}, validationRules map[string]string) (string) {
         ...
     }
  6. The first argument(params) contains a key-value pair or a map of fields and their values as received from end-users connecting to your application. The [string]interface{} map allows you to accommodate different data types in your application, such as integer, float, strings, and more. Then, you've got the second argument, validationRules. This is also a map containing the fields and their respective validation rules. Below is a code snippet showing how the user-supplied data (params) and validationRules look in a real-life scenario.

     //A sample of data coming from web forms or API calls
    
     params := map[string]interface{}{
         "age" : 78,
         "customer_name": "JOHN DOE",
     }
    
    
     //A sample of validation rules 
    
     validationRules := map[string]string{
         "age" : "required|integer",
         "customer_name": "required|alphanumeric",
     } 
  7. In the end, the validate function returns a string describing all errors and the exact reasons why the validation rules have failed.

  8. In the validate function, the first thing you're doing is to loop through the validationRules map using the range statement as shown below. This allows you to get the name of each field that requires validation(For instance, age or customer_name) and the validation rules that you want to run against the fields (For instance, required|integer or required|alphanumeric).

     ...
    
     for field, rules := range validationRules { 
        ...
     }
    
     ...
  9. Next, you're getting a collection of rules for each field, separated by the vertical bar |, and you're using the statement strings.Split to get the individual rule. Again, you're using the range statement to loop through the rules.

     ...
    
     for _, rule := range strings.Split(rules, "|") {
         ...
     }
    
     ...
  10. Next, you're extracting the fieldValue and a boolean ok to check if the field you're validating exists in the params map. This helps you to know whether mandatory fields defined with the required validation rule are set.

    fieldValue, ok := params[field]
  11. Then, you're using the if ...{...} statement for each rule to validate its value only if the ok variable is true(That's if the field exists in the params map). You're using regexp.MustCompile("^[a-zA-Z0-9 ]*$") to validate alphanumeric variables. Then, you're using strconv.ParseInt(...) and strconv.ParseFloat(..) to validate integer and float values respectively. You may extend the rules and validate other values such as emails, addresses, phone numbers, domain names, and more.

  12. Your validation logic is now ready, and you can use it in your project.

3. Create the main.go File

Your Golang project must have a main.go file. This is the entry point that contains the main function. For this guide, you'll create a simple web server that accepts input data.

  1. Create the main.go file.

     $ nano main.go
  2. Paste the following information into the file.

     package main
    
     import (
         "net/http"
         "fmt"
     )
    
     func main() {
         http.HandleFunc("/", httpRequestHandler)
         http.ListenAndServe(":8082", nil)
     }
    
     func httpRequestHandler(w http.ResponseWriter, req *http.Request) {     
    
         req.ParseForm()
    
         params := map[string]interface{}{}
    
         for field, value := range req.Form {   
             params[field] = value[0]
         }
    
         validationRules := map[string]string{
             "first_name": "required|alphanumeric",
             "last_name":  "required|alphanumeric",
             "age" :       "required|integer",
             "amount" :    "required|float",                 
         } 
    
         error := validate(params, validationRules)
    
         if error != "" {               
             fmt.Fprintf(w, error)    
         } else {
             fmt.Fprintf(w, "Validation logic passed. Do your business logic...\r\n")
         } 
    
     }
  3. Save and close the file.

  4. In this file, you've created a simple web server using the net/http package. Your Golang server listens for incoming HTTP requests on port 8082 and passes them to the httpRequestHandler function.

  5. Then, you're looping through the HTTP req.Form variables and putting them in a params map of type [string]interface{}. Towards the end of the file, you're creating some validation rules. Then, you're calling the function validate(params, validationRules) from the validator.go file that you created earlier. If there are errors reported by your validation logic, you're printing them out. Otherwise, you can proceed with any business logic. For instance, you can save data permanently to a database.

4. Test the Golang Central Validation Logic

You'll now send sample data to your application to see how the validation logic works.

  1. Run the application. The following command has a blocking function, and you shouldn't enter any other commands after executing it.

     $ go run ./
  2. Connect to your Linux server on a new terminal window.

  3. Then, attempt sending an empty payload to the application's endpoint http://localhost:8082.

     $ curl -X POST http://localhost:8082 -H "Content-Type: application/x-www-form-urlencoded" -d ""

    The validation logic should report about the mandatory fields that it accepts, as shown below.

      amount is required.
      first_name is required.
      last_name is required.
      age is required.
  4. Next, send valid data to the application's endpoint and see how the input data validation logic behaves this time around.

     $ curl -X POST http://localhost:8082 -H "Content-Type: application/x-www-form-urlencoded" -d "first_name=JOHN&last_name=DOE&age=38&amount=186.58"

    The input data has now passed all validation tests.

     Validation logic passed. Do your business logic...
  5. Next, attempt sending invalid data values to the application's endpoint.

     $ curl -X POST http://localhost:8082 -H "Content-Type: application/x-www-form-urlencoded" -d "first_name=#k&last_name=p*x&age=no&amount=rr"

    Output.

     The value of 'first_name' is not a valid alphanumeric value.
     The value of 'last_name' is not a valid alphanumeric value.
     The value of 'age' is not a valid integer value.
     The value of 'amount' is not a valid float value.
  6. Your validation logic is now working as expected.

Conclusion

In this guide, you've created a central input validation logic for your Golang project. You can now reuse the validate(params, validationRules) function in any file that needs input validation without repeating the long control structures.

Visit the following resources to read more Golang guides: