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:
- A Linux server.
- A non-root
sudo
user. - The Golang package.
1. Create a project
Directory
SSH to your server to complete the following steps.
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
Switch to the new
project
directory.$ cd project
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.
Open a new
validator.go
file withnano
.$ nano validator.go
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 }
Save and close the file when done.
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.
Next, you've got a
validate
function that accepts two arguments.func validate(params map[string]interface{}, validationRules map[string]string) (string) { ... }
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
) andvalidationRules
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", }
In the end, the
validate
function returns a string describing all errors and the exact reasons why the validation rules have failed.In the
validate
function, the first thing you're doing is to loop through thevalidationRules
map using therange
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
orrequired|alphanumeric
).... for field, rules := range validationRules { ... } ...
Next, you're getting a collection of rules for each field, separated by the vertical bar
|
, and you're using the statementstrings.Split
to get the individual rule. Again, you're using therange
statement to loop through the rules.... for _, rule := range strings.Split(rules, "|") { ... } ...
Next, you're extracting the
fieldValue
and a booleanok
to check if the field you're validating exists in theparams
map. This helps you to know whether mandatory fields defined with therequired
validation rule are set.fieldValue, ok := params[field]
Then, you're using the
if ...{...}
statement for each rule to validate its value only if theok
variable istrue
(That's if the field exists in theparams
map). You're usingregexp.MustCompile("^[a-zA-Z0-9 ]*$")
to validate alphanumeric variables. Then, you're usingstrconv.ParseInt(...)
andstrconv.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.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.
Create the
main.go
file.$ nano main.go
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") } }
Save and close the file.
In this file, you've created a simple web server using the
net/http
package. Your Golang server listens for incoming HTTP requests on port8082
and passes them to thehttpRequestHandler
function.Then, you're looping through the HTTP
req.Form
variables and putting them in aparams
map of type[string]interface{}
. Towards the end of the file, you're creating some validation rules. Then, you're calling the functionvalidate(params, validationRules)
from thevalidator.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.
Run the application. The following command has a blocking function, and you shouldn't enter any other commands after executing it.
$ go run ./
Connect to your Linux server on a new terminal window.
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.
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...
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.
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: