DEV Community

Cover image for How to create a native macOS app on Go (Golang) and React.js with full code protection — Part 1
Vic Shóstak
Vic Shóstak

Posted on • Updated on • Originally published at Medium

How to create a native macOS app on Go (Golang) and React.js with full code protection — Part 1

Introduction

Welcome to the next article in the “How to” series! This time, we will analyze the creation of a native desktop application for Apple macOS, write a little code on Go (Golang) and React.js, which will have copy protection.

And we will try to do it without magic! 😉

Article Parts

Part 1: Third-party Go package manager (you're here)
🕓 Part 2: New Go-way with Go Modules

From author: this article (originally) was written about half-year ago, therefore I use dep package manager for Go. I'm preparing Part 2 of this article for modern coding with Go Modules.

Objectives of the Article

  1. Show one of the easiest ways to create a native desktop application for macOS on Golang.
  2. Show the option to protect the code of your application from being modified by third parties (for example, during commercial distribution).

Work Environment

  • Go v1.12.5
  • Node.js v12.3.1

Operating System

  • Apple macOS 10.14.5 Mojave (darwin/amd64)

Package and Dependency Manager

  • dep v0.5.3 (Go)
  • npm v6.9.0 (Node.js)

Used Packages

Go

  • net/http - standard package for creating a web server (godoc)
  • gobuffalo/packr - package for packaging all the necessary sources into one executable binary file (GitHub)
  • zserge/webview - cross-platform package for creating a native operating system window with a built-in browser (GitHub)

Node.js

  • facebook/create-react-app - frontend for macOS app (GitHub)
  • axios/axios - for easier writing of AJAX requests (GitHub)

article theoretical base

Theoretical Base

To better understand what is happening, I suggest you examine the work of some of the packages on which we will rely and use.

net/http

A package that provides an implementation of the HTTP client and server. Included in the standard Go delivery and does not require separate installation and configuration.

It is interesting to us, as it is very easy to understand, has good documentation and has the function http.FileServer().

For more details, see official documentation.

http.FileServer()

This function is the key and gives the web server full access to the specified folder and all its files. That is, the http.FileServer() function allows you to mount a folder to any specified address (route) of the web server.

For example, mount the root folder ./static/images/photos so that it is available at http://localhost:8000/photos:

http.Handle("/photos", http.FileServer("./static/images/photos"))
Enter fullscreen mode Exit fullscreen mode

gobuffalo/packr

Package with a talking title. It is he who will allow us to pack all the necessary files into one binary file.

Please note: we are working with the packr v1 branch.

Suppose we have the following project directory structure:

$ tree .

.
├── main.go
└── templates
    ├── admin
    │   └── index.html
    └── index.html
Enter fullscreen mode Exit fullscreen mode

The file ./main.go contains:

package main

import (
    "fmt"
    "log"

    "github.com/gobuffalo/packr"
)

func main() {
    // Folder with templates that are needed
    // to add to binary file
    box := packr.NewBox("./templates")

    // Search file inside folder
    s, err := box.FindString("amdin/index.html")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(s)
}
Enter fullscreen mode Exit fullscreen mode

Now let’s compile the project into an executable binary file. At the same time, the packr package will pack the entire contents of the ./templates folder into it:

$ packr build ./main.go
Enter fullscreen mode Exit fullscreen mode

If you want to create a binary file for an OS or architecture other than the one you are working with now, then call packr like this (for GNU/Linux, x64):

$ GOOS=linux GOARCH=amd64 packr build ./main.go
Enter fullscreen mode Exit fullscreen mode

zserge/webview

A tiny cross-platform web-browsing package used to create modern graphical interfaces.

Please note: the article describes how to work with v0.1.0.

The file ./main.go contains:

package main

import "github.com/zserge/webview"

func main() {
    // Open Google into a desktop webview window,
    // 1024x768 px, without resize
    webview.Open("Google", "https://google.com", 1024, 768, false)
}
Enter fullscreen mode Exit fullscreen mode

the project structure

The Project Structure

$ tree .

.
├── vendor
├── ui
│   ├── build
│   ├── node_modules
│   ├── public
│   ├── src
│   ├── package-lock.json
│   └── package.json
├── helloworld.app
├── Gopkg.lock
├── Gopkg.lock
├── Makefile
└── main.go
Enter fullscreen mode Exit fullscreen mode

Description of Main files and folders

  • vendor — all packages installed using dep will be stored here
  • ui — folder with React.js application (frontend)
  • ui/build — folder with production-version of React app after the build
  • ui/src — folder with the source code of the React app
  • ui/package.json — dependency file npm
  • helloworld.app — macOS application (specially prepared folder)
  • Gopkg.toml — dependency file dep
  • Makefile — make script for easy way to build app
  • main.go — Golang application source code (backend)

Write the Code

Enough theory. As he said, without exaggeration, one of the great programmers of our time, Linus Torvalds:

Talk is cheap. Show me the code.

Linus Torvalds

Let’s follow this advice and write some code.

I will not analyze each line of code separately, as I consider it redundant and counter-productive. All code listings are provided with detailed comments.

Looking for "full code" Example?

No problem! 👌 I created repository on my GitHub especially for you:

GitHub logo koddr / example-go-react-macos-app-1

Example native macOS app on Go (Golang) and React.js

Just git clone and make.

Memo for Beginners/Copy-paste Developers

Great, when there is a full code listing at the end of the article, right? You can immediately, without reading the text, copy all the program code and see its execution... At this point, I would like to appeal to all readers who do not want to spend time on theory:

Do not mindlessly copy code from the Internet!

This will not help you (in understanding the code and subject of the article), nor the author (in explaining/helping in the comments).

App Frontend

React.js is a powerful, but at the same time, easy-to-learn JavaScript-library for creating user interfaces, which is perfect for us to implement the frontend part of the application.

Please note: For this article, we will not use anything but the standard React.js page.

Like everything in modern frontend, we start with the installation of React.js and all the necessary auxiliary libraries.

  • Create a folder for app and go into it.
  • According to the structure of finished app, install React.js in ./ui directory:
$ npx create-react-app ui
Enter fullscreen mode Exit fullscreen mode
  • Go to folder and check that everything works:
$ cd ui && npm start && open http://localhost:3000
Enter fullscreen mode Exit fullscreen mode
  • Stop dev server (press Ctrl+C) and install axios library:
$ npm i --save axios
Enter fullscreen mode Exit fullscreen mode
  • OK! 👍 Source code of ./ui/src/App.js file:
// Import React and React Hooks
import React, { useState, useEffect } from "react";

// Import axios
import axios from "axios";

// Import logo and CSS
import logo from "./logo.svg";
import "./App.css";

function App() {
  // Define storage for data
  const [state, setState] = useState([]);

  // Retrieving data from an AJAX request.
  // Remember that the function passed to useEffect will run,
  // after render is fixed on the screen.
  // See https://reactjs.org/docs/hooks-reference.html#useeffect
  useEffect(() => {
    axios
      .get("/hello") // GET request to URL /hello
      .then(resp => setState(resp.data)) // save response to state
      .catch(err => console.log(err)); // catch error
  });

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>Hello, {state.text}!</p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

App Backend

  • Install the necessary Go packages:
$ dep ensure -add github.com/gobuffalo/packr
$ dep ensure -add github.com/zserge/webview
Enter fullscreen mode Exit fullscreen mode
  • Also, we need the packr utility, which should be available for calling from the console in $GOPATH/bin/packr:
$ go get -u github.com/gobuffalo/packr/packr
Enter fullscreen mode Exit fullscreen mode
  • Source code of ./main.go file:
package main

import (
    "encoding/json"
    "net/http"

    "github.com/gobuffalo/packr"
    "github.com/zserge/webview"
)

// Message : struct for message
type Message struct {
    Text string `json:"text"`
}

func main() {
    // Bind folder path for packaging with Packr
    folder := packr.NewBox("./ui/build")

    // Handle to ./static/build folder on root path
    http.Handle("/", http.FileServer(folder))

    // Handle to showMessage func on /hello path
    http.HandleFunc("/hello", showMessage)

    // Run server at port 8000 as goroutine
    // for non-block working
    go http.ListenAndServe(":8000", nil)

    // Let's open window app with:
    //  - name: Golang App
    //  - address: http://localhost:8000
    //  - sizes: 800x600 px
    //  - resizable: true
    webview.Open("Golang App", "http://localhost:8000", 800, 600, true)
}

func showMessage(w http.ResponseWriter, r *http.Request) {
    // Create Message JSON data
    message := Message{"World"}

    // Return JSON encoding to output
    output, err := json.Marshal(message)

    // Catch error, if it happens
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Set header Content-Type
    w.Header().Set("Content-Type", "application/json")

    // Write output
    w.Write(output)
}
Enter fullscreen mode Exit fullscreen mode

Build a native macOS App 🏗

  • Creating the directory structure of macOS app:
$ mkdir -p helloworld.app/Contents/MacOS
Enter fullscreen mode Exit fullscreen mode
  • Compile ./main.go to app folder:
$ go build -o helloworld.app/Contents/MacOS/helloworld
Enter fullscreen mode Exit fullscreen mode
  • Run application:
$ open helloworld.app
Enter fullscreen mode Exit fullscreen mode
  • Result:

demo time

Cross compilation for Windows and GNU/Linux

The theoretical block and the code given in the article are relevant for developing similar applications for other operating systems. In this case, the code remains unchanged.

Write the code once — run everywhere!

This is made possible by the cross-system nature.

  • GNU/Linux — executable binary file
  • Microsoft Windows — executable file .exe
  • Apple macOS — a binary file located inside the .app structure

We will look at this in the following articles.

Stay tuned, comment and write only good code!

write code

Securing material

You are at the end of the article. Now you know a lot more than 8 minutes ago.

Take my congratulations! 🎉

Separate 10–15 minutes and the read text restored in memory and the studied code from articles. Next, try to answer the questions and do the exercises in order to better consolidate the material.

Yes, you can pry, but only if you could not remember.

Questions

  1. What is the function of the standard go package net/http used to mount folders to the specified address (route)?
  2. What does the Marshal function do from the standard Go package encoding/json?
  3. What parameters need to be changed in the source code of the Full HD application?
  4. If you want to start a web server without goroutine?
  5. What is the command packr build ./main.go?

Exercises

  • Write tests for showMessage() function (./main.go).
  • Write tests for frontend App (./ui/src/App.js).
  • Rewrite the code of the AJAX request (in the frontend application) without using the axios library. Hint: use the features Fetch API.
  • Add more JSON data to the frontend output in the showMessage() function. Example: add a new attribute Emoji to theMessage structure and output it (with your favorite smiley) after the Text attribute.
  • Try to improve the appearance of your application. Hint: use the Material UI visual component library (GitHub).

Photo by

[Title] Jantine Doornbos https://unsplash.com/photos/HvYy5SEefC8
[1] Tianyi Ma https://unsplash.com/photos/WiONHd_zYI4
[2] Fabian Grohs https://unsplash.com/photos/XMFZqrGyV-Q
[3] Priscilla Du Preez https://unsplash.com/photos/XkKCui44iM0
[Demo] Vic Shóstak (article author)

P.S.

If you want more articles (like this) on this blog, then post a comment below and subscribe to me. Thanks! 😻

And of course, you can help me make developers' lives even better! Just connect to one of my projects as a contributor. It's easy!

My projects that need your help (and stars) 👇

  • 🔥 gowebly: A next-generation CLI tool for easily build amazing web applications with Go on the backend, using htmx & hyperscript and the most popular atomic/utility-first CSS frameworks on the frontend.
  • create-go-app: Create a new production-ready project with Go backend, frontend and deploy automation by running one CLI command.
  • 🏃 yatr: Yet Another Task Runner allows you to organize and automate your routine operations that you normally do in Makefile (or else) for each project.
  • 📚 gosl: The Go Snippet Library provides snippets collection for working with routine operations in your Go programs with a super user-friendly API and the most efficient performance.
  • 🏄‍♂️ csv2api: The parser reads the CSV file with the raw data, filters the records, identifies fields to be changed, and sends a request to update the data to the specified endpoint of your REST API.
  • 🚴 json2csv: The parser can read given folder with JSON files, filtering and qualifying input data with intent & stop words dictionaries and save results to CSV files by given chunk size.

Top comments (11)

Collapse
 
christopy profile image
Christopher Ribeiro

In the first example, the variable t actually should be renamed to box:

t := packr.NewBox("./templates")

Because its used here as box:

s, err := box.FindString("amdin/index.html")

Just to help others don't fall in the same error as me :)

Collapse
 
koddr profile image
Vic Shóstak

Thx! Fix it.

Collapse
 
yougotwill profile image
Will G

Great article! Loved the writing structure with questions and exercises at the end.

Collapse
 
koddr profile image
Vic Shóstak

Thanks! It's so nice to be appreciated! I plan to add this ending part to all of my articles. Be aware of that 😉

Collapse
 
yougotwill profile image
Will G

Challenge accepted! 😆

Thread Thread
 
koddr profile image
Vic Shóstak

Haha 🎯 okay!

Collapse
 
cdnsteve profile image
Steven Leggett • Edited

As an FYI, the Github repo for Packr seems to be advising folks to use Pkger at github.com/markbates/pkger instead.

Collapse
 
koddr profile image
Vic Shóstak

Yep, I know and love it. This is a Part 2 of this article ;)

Collapse
 
flyinglarson profile image
Lars Wenk

thanks for that :) i like that tutorial

Collapse
 
koddr profile image
Vic Shóstak

You're welcome! Hope it helps, before I write Part 2 :D

Collapse
 
sagarwal profile image
Sagar Agarwal

Quite helpful article !
In macOS, once I get the executable in .app, how can I open it in Xcode ?
I need it for a few things like code signing & integrating Sparkle in my app.