In this article, we'll be creating a simple Go application that acts as a REST service.
The application will have one end-point /api/contact
will run on port 8080
, and will be installed as a systemd
service on Ubuntu 20.
Let's start.
What is Go π§Έ ?
Go is a simple and efficient programming language created by Google. It is known for its clean syntax, strong concurrency support, and excellent performance.
Learn more about Go: https://go.dev
Preparing the Go environment
I'm using Ubuntu 20 for this tutorial, If you need to install go on another platform you can refer to the official documentation: https://go.dev/dl/
On Ubuntu installing Go is as simple as running
apt update && apt install golang
and then adding the go binaries to the system path by updating my ~/.bashrc
with these two lines.
export GOPATH=$HOME/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
To explain, the lines added above are shell commands commonly used in Go development environments to set up the necessary environment variables.
The first line, export GOPATH=$HOME/go
, sets the value of the GOPATH
environment variable to the path $HOME/go
. The GOPATH
is an important variable in Go that specifies the root directory for Go projects and their dependencies.
In this case, $HOME/go
is the path to the go
directory within the user's home directory ($HOME
). This is where Go packages and binaries will be stored.
The second line, export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
, modifies the PATH
environment variable to include the Go binary directories. It appends :/usr/local/go/bin:$GOPATH/bin
to the existing PATH
.
/usr/local/go/bin
is the path where the Go compiler (go
) and other Go tools are typically installed.$GOPATH/bin
is the path where Go binaries (executables) are installed when using thego install
command.
By adding these directories to the PATH
, the system will be able to locate and execute Go-related commands and binaries from anywhere in the shell. This allows for convenient usage of Go tools and running Go programs without specifying their full paths.
Project structure
Our project structure will be like this:
email-handler.go
: Contains the request handler logic for the email service.email-sender.go
: Implements the business logic for sending emails.models.go
: Defines the data models used within the email service.main.go
: Serves as the entry point of the application.go.mod
: Specifies the project's module and its dependencies.email-template.html
: A template for the mail that we'll send.
go-email-service
βββ email-handler.go
βββ email-sender.go
βββ models.go
βββ main.go
βββ email-template.html
βββ config.yaml
βββ go.mod
In go.mod
we'll define the module name and the required Go version for the project.
We will need the yaml
package to read SMTP server configuration from the config file
module main
go 1.20
require gopkg.in/yaml.v2 v2.4.0 // indirect
In the file main.go
we will load SMTP
server configuration from yaml
file and then set up an HTTP server with an endpoint for sending emails and start the server on port 8080
.
config.yaml
smtpHost: example-smtpserver.emailprovider.com
smtpPort: 465
smtpEmail: example-sender@emailprovider.com
smtpPassword: example-sender-password
main.go
package main
import (
"log"
"net/http"
"os"
"gopkg.in/yaml.v2"
)
// Global SMTP server configuration variable
var smtpConfig SMTPConfig
// Load SMTP server configuration from the YAML file
func loadSMTPConfig(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return err
}
err = yaml.Unmarshal(data, &smtpConfig)
if err != nil {
return err
}
return nil
}
func main() {
err := loadSMTPConfig("config.yaml")
if err != nil {
log.Fatalf("Failed to load SMTP configuration: %v", err)
}
// Define the endpoint for sending the email
http.HandleFunc("/api/book", SendEmailHandler)
// Start the HTTP server
log.Println("Server listening on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
For email-hander.go
we can create our handler function SendEmailHandler
receives HTTP requests, parses a JSON request body, composes email messages using HTML template, and sends it concurrently using goroutines
. Finally, it sends a response back to the client indicating successful email delivery. (more about goroutines
here )
package main
import (
"encoding/json"
"fmt"
"bytes"
"html/template"
"log"
"net/http"
"sync"
)
// Handler function for sending email
func SendEmailHandler(w http.ResponseWriter, r *http.Request) {
log.Println("Received request /api/contact")
// Parse the JSON request body
var requestBody RequestBody
err := json.NewDecoder(r.Body).Decode(&requestBody)
if err != nil {
http.Error(w, "Failed to parse request body", http.StatusBadRequest)
return
}
defer r.Body.Close()
// Send the email concurrently
var wg sync.WaitGroup
wg.Add(1)
// Compose the email message
subject := fmt.Sprintf("Contact received for %s", requestBody.Name)
body, err := getEmailBody("email-template.html", requestBody)
if err != nil {
log.Println("Failed to read email template:", err)
http.Error(w, "Failed to read email template", http.StatusInternalServerError)
return
}
go SendEmail(subject, body, requestBody.RecipientEmail, &wg)
// Send a response back to the client
w.WriteHeader(http.StatusOK)
w.Write([]byte("Email sent successfully"))
}
func getEmailBody(templateFile string, data interface{}) (string, error) {
tmpl, err := template.ParseFiles(templateFile)
if err != nil {
return "", err
}
var result bytes.Buffer
err = tmpl.Execute(&result, data)
if err != nil {
return "", err
}
return result.String(), nil
}
The email-sender.go
defines a Go function SendEmail
sends an email using an SMTP server. It establishes a secure connection with the SMTP server, authenticates using the provided credentials, and sends the email message.
package main
import (
"bytes"
"crypto/tls"
"fmt"
"log"
"net/smtp"
"sync"
)
func SendEmail(subject string, body string, to string, wg *sync.WaitGroup) bool {
defer wg.Done()
message := []byte("From: Contact <" + smtpConfig.Sender + ">\r\n" +
"To: " + to + "\r\n" +
"Subject: " + subject + "\r\n" +
"MIME-Version: 1.0\r\n" +
"Content-Type: text/html; charset=utf-8\r\n" +
"\r\n" +
body + "\r\n")
// Create authentication credentials
auth := smtp.PlainAuth("", smtpConfig.Sender, smtpConfig.Password, smtpConfig.SMTPHost)
// Create the TLS configuration
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtpConfig.SMTPHost,
}
// Connect to the SMTP server
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", smtpConfig.SMTPHost, smtpConfig.SMTPPort), tlsConfig)
if err != nil {
log.Printf("Failed to connect to the SMTP server: %v", err)
return false
}
// Create the SMTP client
client, err := smtp.NewClient(conn, smtpConfig.SMTPHost)
if err != nil {
log.Printf("Failed to create SMTP client: %v", err)
return false
}
// Authenticate with the SMTP server
if err := client.Auth(auth); err != nil {
log.Printf("SMTP authentication failed: %v", err)
return false
}
// Set the sender and recipient
if err := client.Mail(smtpConfig.Sender); err != nil {
log.Printf("Failed to set sender: %v", err)
return false
}
if err := client.Rcpt(to); err != nil {
log.Printf("Failed to set recipient: %v", err)
return false
}
// Send the email message
w, err := client.Data()
if err != nil {
log.Printf("Failed to open data writer: %v", err)
return false
}
buf := bytes.NewBuffer(make([]byte, 0, 1024))
buf.Write(message)
_, err = buf.WriteTo(w)
if err != nil {
log.Printf("Failed to write email message: %v", err)
return false
}
err = w.Close()
if err != nil {
log.Printf("Failed to close data writer: %v", err)
return false
}
// Close the connection to the SMTP server
client.Quit()
log.Println("Email sent successfully!")
return true
}
models.go
will contain data structures used in the project like smtp config and http request body
package main
type RequestBody struct {
RecipientEmail string `json:"email"`
PhoneNumber string `json:"phone"`
Name string `json:"name"`
}
type SMTPConfig struct {
SMTPHost string `yaml:"smtpHost"`
SMTPPort int `yaml:"smtpPort"`
Sender string `yaml:"smtpEmail"`
Password string `yaml:"smtpPassword"`
}
Build and run
To build the project, first, we fetch the dependencies (make sure you are in the root folder of the project)
go get .
Then we run
go run .
If you only want to build the project without running it
go build .
An executable file will be generated. On Windows, it will be a main.exe
file (you can double-click it to run) and on Linux or MacOS it will be an executable file main
After running the server and sending a request using curl or Postman
you will get an output like this in the console
2023/05/29 12:30:57 Server listening on http://localhost:8080
2023/05/29 12:31:08 Received request /api/contact
2023/05/29 12:31:10 Email sent successfully!
And you will receive an email with the content from the template
Running as a service using Systemd on Ubuntu
To run your app as a systemd
service on Ubuntu 20
:
Create a
systemd
service unit file:sudo vi /etc/systemd/system/go-email.service
Update the file content with this:
[Unit] Description=Go Email Sender After=network.target [Service] User=username ExecStart=/path/to/go-email-service-folder/main WorkingDirectory=/path/to/go-email-service-folder Restart=always [Install] WantedBy=multi-user.target
Replace
username
with your username and/path/to/go-email-service-folder
with the actual path to the application.
Enable and start the service:
This will enable the service to start automatically on system boot and start it immediately.
sudo systemctl enable go-email sudo systemctl start go-email
Source code
As always the source code can be found on GitHub β€οΈ:
https://github.com/camelcodes/go-email-sender
Works on my machine π
A similar email-sending service is used in our Coding Assistance service website https://patchmycode.com to perform seamless backend performance.
Check out PatchMyCode if you need expert help with coding assistance or software programming courses at a good price.