From C to Golang: My Journey of Programming Model Upgrade
In my personal programming journey, I’ve been fortunate to touch and deeply use several programming languages, including C, PHP, Node.js, and Golang. Each language has its unique advantages and challenges, and every transition has brought new understanding and skill improvement. Let’s take a look at the advantages and disadvantages of these programming languages, as well as why I chose to transition from one language to another.
C Language: Manual Memory Management
C was where I started. It provides direct control over hardware and the ability to manage memory manually. However, this capability also brings some challenges. For example, the complexity of handling strings and managing memory requires programmers to have a high level of expertise and experience.
#include<stdio.h>
int main() {
char str[10] = "Hello";
printf("%s\n", str);
return 0;
}
Comparison of String Handling
In C, strings are treated as arrays of characters, which means manually handling many aspects of strings, including memory allocation and release, string concatenation, and calculation of string length. This increases the complexity of programming and can lead to errors such as buffer overflow and memory leaks.
Here’s an example of handling strings in C:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main() {
char str1[10] = "Hello";
char str2[10] = "World";
char *str3 = (char *) malloc(20 * sizeof(char));
strcpy(str3, str1);
strcat(str3, str2);
printf("%s\n", str3);
free(str3); // don't forget to free the memory!
return 0;
}
In the above code, we need to manually allocate enough memory to store the concatenated string, and then use the strcpy and strcat functions to perform string concatenation. In the end, we need to remember to release the allocated memory, otherwise, it will lead to memory leaks.
In contrast, handling strings in JavaScript is much simpler. In JavaScript, strings are considered primitive types, you can use the ”+” operator to concatenate them, and there is no need to manually handle memory.
Here’s an example of handling strings in JavaScript:
let str1 = "Hello";
let str2 = "World";
let str3 = str1 + str2;
console.log(str3);
In this JavaScript code, we just need to use the ”+” operator to connect two strings. We don’t need to worry about memory management, which greatly reduces the complexity of programming and the possibility of errors.
PHP: The Main Choice for Web Development
I then turned to PHP because it makes web development extremely simple. PHP has a wealth of built-in features, as well as a huge open-source community and very mature CMS systems, including WordPress and Laravel. However, the downside of PHP is that it has no type system, it is single-threaded, and its code is executed synchronously.
<?php
$array = ['hello', 123, true];
foreach ($array as $item) {
echo $item . "\n";
}
?>
Node.js: An Excellent Choice for Asynchronous Programming
Next, I turned to Node.js because it offers non-blocking asynchronous operations and support for multithreading (through web workers). Additionally, Node.js has excellent support for JSON, and it can handle many complex tasks in web development very well.
The main advantage of Node.js lies in its asynchronous non-blocking I/O mechanism, which allows Node.js to maintain high performance when dealing with a large number of concurrent requests. This is mainly due to the event loop mechanism of Node.js.
Event Loop Mechanism
In Node.js, all I/O operations (such as reading and writing files, database queries, network requests, etc.) are asynchronous, and they trigger a callback function when completed. These callback functions are put into a queue, waiting for the event loop to process them.
When the main thread of Node.js is idle, the event loop removes a callback function from the queue and executes it. In this way, even if an operation takes some time to complete, Node.js can continue to handle other requests without waiting for this operation to complete.
Here is a code example of the event loop in Node.js:
const fs = require('fs');
fs.readFile('file.txt', 'utf8', function(err, data) {
if (err) throw err;
console.log(data);
});
console.log('Reading file...');
In this example, fs.readFile is an asynchronous operation, which calls the callback function when the file reading is completed. We can see that even though reading the file takes some time, console.log(‘Reading file…’) will execute immediately, this is the effect of the event loop.
Web Worker
Node.js also supports Web Workers, which allows us to run JavaScript code in background threads, achieving true parallel computation. This is very useful for CPU-intensive tasks as we can distribute the tasks to be processed across multiple CPU cores.
Here is a Node.js code example using Web Worker:
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (msg) => {
console.log(`Received message from worker: ${msg}`);
});
worker.postMessage('Hello, worker!');
} else {
parentPort.on('message', (msg) => {
console.log(`Received message from main thread: ${msg}`);
parentPort.postMessage('Hello, main thread!');
});
}
In this example, we create a new Worker and use postMessage and on(‘message’) to communicate between the main thread and the Worker. This allows us to execute tasks in parallel between the main thread and the Worker.
Golang: Concise, Efficient, with Native Concurrency Support
Finally, I chose Golang because it provides a powerful standard library, fast compilation and execution speed, and native concurrency support. Although its error handling may seem inelegant, Golang’s other advantages made me find it a very effective and reliable tool in practice.
Error Handling
In Go language, error handling is usually done by checking the error value returned by functions. This approach, while direct, is also considered inelegant. Here is a typical error handling example:
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("nonexistentfile.txt")
if err != nil {
fmt.Println(err)
return
}
// Do something with f
f.Close()
}
In the above code, we need to explicitly check each function call that may cause an error. The main problem with this approach is that if you forget to check for an error, it can lead to unpredictable program behavior.
One way to improve this error handling method is to use helper functions to encapsulate the error handling logic, as shown below:
package main
import (
"fmt"
"os"
)
func handleError(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func main() {
f, err := os.Open("nonexistentfile.txt")
handleError(err)
// Do something with f
f.Close()
}
In this optimized version, we define a handleError function that checks for errors and terminates the program when an error occurs. This way, we don’t have to manually check for errors after each function call.
Concurrent Programming
One of the core advantages of Go language is its native support for concurrent programming. In Go, you can use goroutines (lightweight threads in Go) and channels (for communication between goroutines) to implement concurrent programming concisely.
Below is a simple task implemented using goroutines and channels:
package main
import (
"fmt"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "started job", j)
time.Sleep(time.Second)
fmt.Println("worker", id, "finished job", j)
results <- j * 2
}
}
func main() {
const numJobs = 5
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
for a := 1; a <= numJobs; a++ {
<-results
}
}
In this example, we create three worker goroutines, they read tasks from the task channel in parallel, process tasks, and then write the results to the results channel. This is an example of Go language supporting concurrent programming.
Utilizing Multi-core CPU
The reason why Go language can take advantage of multi-core CPUs lies in its runtime system. The Go language runtime system can schedule goroutines on all CPU cores, so it can fully utilize the performance of multi-core CPUs.
In contrast, Node.js is single-threaded by default, even running on a multi-core CPU, it can only run on one core. Although Node.js can achieve parallel computation by creating child processes or using the worker_threads module, these are much more complex compared to Go’s concurrency model.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
fmt.Println(i)
}(i)
}
wg.Wait()
}
In summary, each programming language has its unique uses and advantages. My choices do not mean that one language is superior to another, but are based on my needs and preferences in a given situation. I hope my experience can provide some references for you to choose and learn programming languages.