Building a Postman-Like API Client: Go Fyne vs Java Swing

Published: 2026-06-22
Google AdSense 广告位 (待申请通过后开启)

Conclusion

This demonstration implemented the same Postman-like API testing tool using both Go Fyne and Java Swing.

From a functionality perspective, both frameworks successfully provided the core features required by an API client, including request methods, URL input, request body editing, asynchronous HTTP requests, and response display.

Java Swing benefits from decades of development and remains one of the most mature desktop GUI toolkits available. Its rich component ecosystem, stable behavior, and extensive documentation make it a strong choice for complex desktop applications.

Go Fyne takes a different approach. While its ecosystem is smaller and some Linux environments may require additional graphical dependencies, it offers a modern development experience, native binaries, and straightforward deployment once the build environment is configured.

For small utility tools, internal applications, and developers already working within the Go ecosystem, Fyne can be a practical option. For large-scale desktop applications that require advanced UI components and long-term ecosystem stability, Swing still has significant advantages.

Ultimately, neither framework is universally better. The choice depends on project requirements, deployment targets, team experience, and long-term maintenance considerations.

Implementing an API Client using Go Fyne

We can continue using the cross-platform Fyne framework to implement it. This tool needs to handle network requests, so we will use Go's extremely powerful standard library, net/http.

package main

import (
	"bytes"
	"io"
	"net/http"
	"strings"
	"time"

	"fyne.io/fyne/v2"
	"fyne.io/fyne/v2/app"
	"fyne.io/fyne/v2/container"
	"fyne.io/fyne/v2/widget"
)

func main() {
	myApp := app.New()
	myWindow := myApp.NewWindow("Go API Tester")

	// --- 1. Top Request Configuration Components ---
	// Request method dropdown (GET/POST)
	methodSelect := widget.NewSelect([]string{"GET", "POST"}, func(value string) {})
	methodSelect.SetSelected("GET") // Default to GET

	// Media type dropdown
	contentTypeSelect := widget.NewSelect([]string{"application/json", "multipart/form-data", "application/x-www-form-urlencoded"}, func(value string) {})
	contentTypeSelect.SetSelected("application/json")

	// URL input field
	urlEntry := widget.NewEntry()
	urlEntry.SetPlaceHolder("Enter Request Address...")
	urlEntry.SetText("https://raw.githubusercontent.com/pixel-jey/exchange/main/rates.json") // Default address from the image

	// --- 2. Left and Right Text Boxes ---
	// Left side: Request body input field
	reqBodyEntry := widget.NewMultiLineEntry()
	reqBodyEntry.SetPlaceHolder("Request Body (JSON or parameters)...")
	reqBodyEntry.SetText("{\n  \"userId\": \"1781577778344\"\n}")

	// Right side: Response result display field (set as read-only implicitly by usage)
	resCodeLabel := widget.NewLabel("HTTP Status Code: Not Sent")
	resBodyEntry := widget.NewMultiLineEntry()
	resBodyEntry.SetPlaceHolder("Result (Response)...")
	resBodyEntry.Wrapping = fyne.TextWrapWord

	// --- 3. Core Network Logic for Send Button ---
	sendButton := widget.NewButton("Send Request", func() {
		resBodyEntry.SetText("Requesting...")
		resCodeLabel.SetText("HTTP Status Code: Waiting...")

		go func() {
			method := methodSelect.Selected
			urlStr := strings.TrimSpace(urlEntry.Text)
			bodyStr := reqBodyEntry.Text

			// Create HTTP client and set a 10-second timeout
			client := &http.Client{Timeout: 10 * time.Second}

			// Assemble the request
			var req *http.Request
			var err error
			if method == "POST" && bodyStr != "" {
				req, err = http.NewRequest(method, urlStr, bytes.NewBufferString(bodyStr))
			} else {
				req, err = http.NewRequest(method, urlStr, nil)
			}

			if err != nil {
				resBodyEntry.SetText("Failed to build request: " + err.Error())
				return
			}

			// Attach Content-Type header if it is a POST request
			if method == "POST" {
				req.Header.Set("Content-Type", contentTypeSelect.Selected)
			}

			// Execute the request
			resp, err := client.Do(req)
			if err != nil {
				resBodyEntry.SetText("Failed to send request (Please check your network or URL): \n" + err.Error())
				return
			}
			defer resp.Body.Close()

			// Read response body
			respBody, err := io.ReadAll(resp.Body)
			if err != nil {
				resBodyEntry.SetText("Failed to read response: " + err.Error())
				return
			}

			// Update the UI
			resCodeLabel.SetText("HTTP Status Code: " + resp.Status)
			resBodyEntry.SetText(string(respBody))
		}()
	})

	// --- 4. UI Layout Assembly ---
	// Top configuration bar layout: Method selection (100px) + Type selection (180px) + URL field (Auto-stretch) + Send button
	topRow := container.NewBorder(nil, nil, container.NewHBox(methodSelect, contentTypeSelect), sendButton, urlEntry)

	// Left panel: Contains request body
	leftPanel := container.NewBorder(widget.NewLabel("Request Body:"), nil, nil, nil, reqBodyEntry)
	// Right panel: Contains status code label and response body
	rightPanel := container.NewBorder(resCodeLabel, nil, nil, nil, resBodyEntry)

	// Split the interface equally between left and right
	splitGrid := container.NewGridWithColumns(2, leftPanel, rightPanel)

	// Overall layout: Top bar fixed, middle and bottom fill the split interface
	mainLayout := container.NewBorder(topRow, nil, nil, nil, splitGrid)

	myWindow.SetContent(mainLayout)
	myWindow.Resize(fyne.NewSize(900, 550)) // Set a reasonable initial window size
	myWindow.ShowAndRun()
}

To start it, enter the following command in the terminal:

go run main.go

🔍 Why does it freeze? As mentioned when discussing file size, the Fyne framework relies on C (CGO) for graphics and OpenGL rendering. The initial compilation is extremely slow: When you first execute `go run main.go`, the Go compiler needs to call your Linux system's gcc compiler to compile all tens of thousands of lines of C graphics driver code for the Fyne framework. There's no progress bar: Go doesn't output any logs by default during compilation, so it looks like it's frozen at the cursor. Depending on your computer's CPU performance, this process typically takes anywhere from 30 seconds to 2 minutes.

Post-run effects and feature debugging: Instant startup: After clicking "Run," a native GUI window compiled entirely from Go will pop up, without any external dependencies. Default data: The URL field and the request body on the left have automatically filled in the test link shown in your image. Interface testing: Clicking the "Send" button in the upper right corner will cause the program to asynchronously request the JSON address in the background via Go's network goroutines. Result return: Upon successful request, a green (or system default color) status code (HTTP Status Code: 200 OK) will immediately appear on the right, and the JSON text data of the exchange rate will be elegantly displayed in the box below.

json

{
  "USD_THB": 32.86,
  "USD_CNY": 6.77,
  "USD_PHP": 60.78
}

Implementing an API Client using Java Swing

We can directly implement the "interface testing tool (mini Postman)" using Java Swing.

import javax.swing.*;
import java.awt.*;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

public class ApiTester {
    public static void main(String[] args) {
        // Create the main window
        JFrame frame = new JFrame("Java Swing API Tester");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(900, 550);

        // --- 1. Top Bar Components ---
        JPanel topPanel = new JPanel(new BorderLayout(5, 5));
        
        // Dropdown selection box
        String[] methods = {"GET", "POST"};
        JComboBox<String> methodSelect = new JComboBox<>(methods);
        
        // URL input field
        JTextField urlField = new JTextField("https://raw.githubusercontent.com/pixel-jey/exchange/main/rates.json");
        
        // Send button
        JButton sendButton = new JButton("Send Request");

        JPanel leftTop = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0));
        leftTop.add(methodSelect);

        topPanel.add(leftTop, BorderLayout.WEST);
        topPanel.add(urlField, BorderLayout.CENTER);
        topPanel.add(sendButton, BorderLayout.EAST);

        // --- 2. Middle Text Areas ---
        // Left side: Request body
        JTextArea reqBodyArea = new JTextArea("{\n  \"userId\": \"1781577778344\"\n}");
        JScrollPane leftScroll = new JScrollPane(reqBodyArea);
        leftScroll.setBorder(BorderFactory.createTitledBorder("Request Body"));

        // Right side: Response result
        JTextArea resBodyArea = new JTextArea();
        resBodyArea.setEditable(false); // Read-only
        resBodyArea.setLineWrap(true);   // Auto line wrap
        JScrollPane rightScroll = new JScrollPane(resBodyArea);
        rightScroll.setBorder(BorderFactory.createTitledBorder("Response (Result)"));

        // Split pane to divide left and right equally
        JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftScroll, rightScroll);
        splitPane.setDividerLocation(450);

        // --- 3. Core Network Request Logic ---
        sendButton.addActionListener(e -> {
            resBodyArea.setText("Requesting...");
            
            // Start a new thread for the network request to prevent the Swing UI from freezing
            new Thread(() -> {
                try {
                    String urlStr = urlField.getText().trim();
                    URL url = new URL(urlStr);
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setRequestMethod((String) methodSelect.getSelectedItem());
                    conn.setConnectTimeout(10000);
                    conn.setReadTimeout(10000);

                    // If it is a POST request, attempt to send the request body
                    if ("POST".equals(conn.getRequestMethod())) {
                        conn.setDoOutput(true);
                        conn.setRequestProperty("Content-Type", "application/json");
                        try (OutputStream os = conn.getOutputStream()) {
                            byte[] input = reqBodyArea.getText().getBytes(StandardCharsets.UTF_8);
                            os.write(input, 0, input.length);
                        }
                    }

                    // Read the response
                    int status = conn.getResponseCode();
                    BufferedReader in = new BufferedReader(new InputStreamReader(
                            status < 400 ? conn.getInputStream() : conn.getErrorStream(), StandardCharsets.UTF_8));
                    String inputLine;
                    StringBuilder content = new StringBuilder();
                    while ((inputLine = in.readLine()) != null) {
                        content.append(inputLine).append("\n");
                    }
                    in.close();
                    conn.disconnect();

                    // Update UI
                    SwingUtilities.invokeLater(() -> resBodyArea.setText("HTTP Status Code: " + status + "\n\n" + content.toString()));

                } catch (Exception ex) {
                    SwingUtilities.invokeLater(() -> resBodyArea.setText("Request failed: " + ex.getMessage()));
                }
            }).start();
        });

        // Assemble overall layout
        frame.getContentPane().add(topPanel, BorderLayout.NORTH);
        frame.getContentPane().add(splitPane, BorderLayout.CENTER);
        
        // Center the window on screen and display it
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

To start it, enter the following command in the terminal:

javac ApiTester.java
java ApiTester
Image-1
Image-1

Same Functionality

Implemented in Go, this tool offers significant advantages during debugging: Seamless Asynchronous Operation: After clicking send, the network request runs in a separate `go func()` goroutine. Even if a network timeout occurs, the entire software window won't freeze or experience loading screens, allowing the user to continue editing the text on the left. Extremely Low Resource Consumption: When running in the background, this small tool typically uses only 15MB to 30MB of memory, while similar tools written in Electron would start at 150MB.

You'll find that there's no lag, no hidden or explicit dependency issues; the perfect graphical window pops up almost instantly, within fractions of a second of pressing Enter. This demonstrates Java's unparalleled maturity in the GUI field.

Same Interface

In terms of the zero-configuration, out-of-the-box experience and the speed of the first build in a Linux environment, Go's Fyne graphics library is indeed completely defeated by Java Swing. The "freezing" pain point you encountered precisely hits the core weakness of the Go language in desktop development.

Swing is truly "dynamically linked": Java handles all graphics rendering and window management within the system's built-in JVM. Running Swing on Linux is instantaneous – a single command compiles and starts it. Go, however, striving for dependency-free operation, requires compiling tens of thousands of lines of C code for OpenGL on your local Linux machine using CGO, resulting in the prolonged "freezing" you see. Linux's dependency hell: Running Fyne on Linux systems (especially Ubuntu/Debian) often leads to hidden freezes or errors because you must manually install a bunch of graphics development libraries (such as libgl1-mesa-dev, xorg-dev, etc.) using apt beforehand. If these are missing, Go might even fail to compile.

Same Requests

🐹 Go Language Version (based on the standard library net/http) Thanks to its native high-concurrency design, Go makes implementing asynchronous requests extremely elegant, without needing to explicitly create heavy threads.

☕ In the Java language version (based on the standard library HttpURLConnection), without introducing third-party libraries (such as OkHttp or HttpClient), implementing the same asynchronous request in native Java requires explicit manipulation of threads and byte streams.

Performance Comparison

Deep Comparison of Core Technical Differences:

Comparison Dimension 🐹 Go Language (net/http) ☕ Java Language (HttpURLConnection)

Asynchronous Overhead

🥇 Extremely Low. go func() creates a Coroutine (Goroutine), which consumes only about 2KB of memory. Running tens of thousands of concurrent requests causes almost no pressure on CPU or RAM.

❌ High. new Thread() creates an OS-level Thread, which defaults to allocating 1MB of memory. Running thousands of concurrent requests can easily trigger an OOM (Out Of Memory) error.

Connection Pooling

🥇 Automatic Connection Pool. The standard library comes with a built-in Transport layer that enables HTTP Keep-Alive by default. It automatically reuses old connections for consecutive requests, making it incredibly fast.

❌ Clunky Management. The persistent connection reuse mechanism is weak. If not handled carefully, it can easily generate a large number of TIME_WAIT connections, leading to port exhaustion.

Error Stream Handling

🥇 Unified Handling. Whether the server returns a successful 200 OK or an error like 500 Internal Error, the response body is always read from resp.Body in the exact same way.

❌ Strictly Separated. You must check the status code first. Statuses below 400 require getInputStream(), while 400/500+ statuses force you to switch to getErrorStream(). Forgetting this throws an exception.

Code Boilerplate

🥇 Streamlined. It includes excellent built-in wrapper utilities, completely eliminating the need for nested streams like BufferedReader(new InputStreamReader(...)).

❌ Bloated. Due to legacy architecture, IO streams require multi-layered wrapping. Without relying on third-party libraries, the boilerplate code is heavily redundant.

Conclusion

In the specific field of desktop client development (GUI), Go's current ecosystem and maturity are indeed far behind Java.

During testing on Linux, Fyne required additional graphical dependencies and a longer initial setup process compared with Swing.

We can objectively see this gap:

1. The disparity in historical foundation and ecosystem: Java Swing was born in 1998, and JavaFX in 2008. Java has been deeply involved in the graphical interface field for nearly 30 years, and its underlying compatibility, font rendering, and multi-threaded UI refresh mechanisms have been polished to perfection by hundreds of thousands of developers worldwide. Go (born in 2009), on the other hand, was designed from day one for server backends, cloud computing, and microservices. Go officially does not provide any native GUI framework. Fyne, which you are currently using, is just a third-party library assembled by community enthusiasts using OpenGL, and in terms of component richness, complex layout, and system compatibility, it is indeed several generations behind Java.

2. Runtime Differences: Java boasts the industry's most powerful virtual machine (JVM). The JVM acts like an all-around "housekeeper," permanently residing in the system (or dynamically linked). Swing's various rendering instructions are directly translated to the operating system by the JVM, enabling zero-configuration, second-level compilation, and second-level startup. Go's pursuit of "single-file, dependency-free" is a godsend on servers, but becomes a burden on desktops. To avoid relying on the JVM, it must statically compile the graphics rendering engine, leading to the CGO compilation hell you'll encounter.

For this Postman-like API Client example, both implementations were able to achieve the same core functionality, demonstrating that modern desktop API tools can be built effectively using either Go Fyne or Java Swing.

Explore More

Technology Guides →

Building a Linux Command Search Tool with Java Swing

Java 21 Virtual Threads vs Java 17 Thread Pools in Swing Applications

Oracle vs IBM: Who Would Have Been the Better Steward of Java?

How JComponent and JTextComponent Simplify Swing Development

Southeast Asia Insights →