search
HomeBackend DevelopmentGolangBuilding an API with Go, PostgreSQL, Google Cloud and CockroachDB

I built an API with Go and PostgreSQL, set up a CI/CD pipeline with Google Cloud Run, Cloud Build, Secret Manager and Artifact Registry, and connected the Cloud Run instance to CockroachDB.

The API is based on the game Crisis Core: Final Fantasy VII, to simulate “Materia Fusion”. This article’s intended audience is for developers who just want to know how to build and deploy the API. I have another article where I talk about everything I learnt while working on this project, what didn’t work, and understanding and translating the game’s materia fusion rules (link coming soon).

Links for easy reference

  • GitHub repo and README
  • Swagger (OpenAPI) documentation and testing
  • Public Postman collection
  • Source of domain model

API Objective

3 endpoints — health check (GET), list of all materia (GET), and simulate materia fusion (POST)

The Domain Model

Materia (both singular and plural) is a crystal orb that serves as a source of magic. There are 144 distinct materia in the game, and they’re broadly classified into 4 categories: “Magic”, “Command”, “Support” and “Independent”. However, for the purpose of figuring out the rules of materia fusion, it was easier to have 32 internal categories based on their fusion behaviour, and 8 grades within those categories (see reference).

A materia becomes ‘Mastered’ when it is used for a certain duration. The duration is not important here.

Most importantly, 2 materia can be fused to produce a new materia. The rules governing fusion are influenced by:

  • Whether either or both materia are mastered.
  • Which materia comes first (as in X Y is not necessarily equal to Y X).
  • Materia internal category.
  • Materia grade.

Building an API with Go, PostgreSQL, Google Cloud and CockroachDB

And there are a LOT of exceptions, with some rules having 3 levels of nested if-else logic. This eliminates the possibility of creating a simple table in the DB and persisting 1000 rules into it, or coming up with One Formula To Rule Them All.

In short, we need:

  1. A table materia with columns name(string), materia_type(ENUM) (the 32 internal categories), grade(integer), display_materia_type(ENUM) (the 4 categories used in the game), description(string) and id(integer) as an auto-incrementing primary key.
  2. A data structure to encapsulate the basic rules format MateriaTypeA MateriaTypeB = MateriaTypeC.
  3. Code to use the basic and complex rules to determine the output Materia in terms of its internal category and grade.

1. Setting Up The Local PostgreSQL DB

Ideally you can install the DB from the website itself. But the pgAdmin tool could not connect to the DB for some reason, so I used Homebrew.

Installation

brew install postgresql@17

This will install a whole bunch of CLI binary files to help use the DB.

Optional: add /opt/homebrew/opt/postgresql@17/bin to $PATH variable.

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb

Create the user and permissions

-- create an SQL user to be used by the Go server
CREATE USER go_client WITH PASSWORD 'xxxxxxxx';

-- The Go server doesn't ever need to add data to the DB. 
-- So let's give it just read permission.
CREATE ROLE readonly_role;
GRANT USAGE ON SCHEMA public TO readonly_role;

-- This command gives SELECT access to all future created tables. 
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role;

-- If you want to be more strict and give access only to tables that already exist, use this:
-- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;

GRANT readonly_role TO go_client;

Create the table

CREATE TYPE display_materia_type AS ENUM ('Magic', 'Command', 'Support', 'Independent');

CREATE TYPE materia_type AS ENUM ('Fire', 'Ice', 'Lightning', 'Restore', 'Full Cure', 'Status Defense', 'Defense', 'Absorb Magic', 'Status Magic', 'Fire & Status', 'Ice & Status', 'Lightning & Status', 'Gravity', 'Ultimate', 'Quick Attack', 'Quick Attack & Status', 'Blade Arts', 'Blade Arts & Status', 'Fire Blade', 'Ice Blade', 'Lightning Blade', 'Absorb Blade', 'Item', 'Punch', 'SP Turbo', 'HP Up', 'AP Up', 'ATK Up', 'VIT Up', 'MAG Up', 'SPR Up', 'Dash', 'Dualcast', 'DMW', 'Libra', 'MP Up', 'Anything');

CREATE TABLE materia (
    id integer NOT NULL,
    name character varying(50) NOT NULL,
    materia_type materia_type NOT NULL,
    grade integer NOT NULL,
    display_materia_type display_materia_type,
    description text
    CONSTRAINT materia_pkey PRIMARY KEY (id)
);

-- The primary key 'id' should auto-increment by 1 for every row entry.
CREATE SEQUENCE materia_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

ALTER SEQUENCE materia_id_seq OWNED BY materia.id;

ALTER TABLE ONLY materia ALTER COLUMN id SET DEFAULT nextval('materia_id_seq'::REGCLASS);

Add the data

Create an Excel sheet with table header and data, and export it as a CSV file. Then run the command:

COPY materia(name,materia_type,grade,display_materia_type,description) FROM
 '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
</path_to_csv_file>

2. Creating the Go Server

Create the boilerplate code using autostrada.dev. Add the options of api, postgresql, httprouter , env var config, tinted logging, git, live reload, makefile. We end up getting a file structure like this:

? codebase
├─ cmd
│  └─ api
│     ├─ errors.go
│     ├─ handlers.go
│     ├─ helpers.go
│     ├─ main.go
│     ├─ middleware.go
│     └─ server.go
├─ internal
│  ├─ database --- db.go
│  ├─ env --- env.go
│  ├─ request --- json.go
│  ├─ response --- json.go
│  └─ validator
│     ├─ helpers.go
│     └─ validators.go
├─ go.mod
├─ LICENSE
├─ Makefile
├─ README.md
└─ README.html

.env file

The boilerplate generator has created code to fetch environment variables and add them to the code, but we can make it easier to track and update the values.

Create /.env file. Add the following values:

HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1
</password>

Add the godotenv library:

go get github.com/joho/godotenv

Add the following to main.go:

// At the beginning of main():
err := godotenv.Load(".env") // Loads environment variables from .env file
if err != nil { // This will be true in prod, but that's fine.
  fmt.Println("Error loading .env file")
}


// Modify config struct:
type config struct {
  baseURL string
  db      struct {
    dsn string
  }
  httpPort                 int
  apiTimeout               int
  apiCallsAllowedPerSecond float64
}

// Modify run() to use the new values from .env:
cfg.httpPort = env.GetInt("HTTP_PORT")
cfg.db.dsn = env.GetString("DB_DSN")
cfg.apiTimeout = env.GetInt("API_TIMEOUT_SECONDS")
cfg.apiCallsAllowedPerSecond = float64(env.GetInt("API_CALLS_ALLOWED_PER_SECOND"))

// cfg.baseURL = env.GetString("BASE_URL") - not required

Middleware and Routes

The boilerplate already has a middleware to recover from panics. We will add 3 more: Content-Type checking, rate-limiting and API timeout protection.

Add tollbooth library:

go get github.com/didip/tollbooth

Update

func (app *application) contentTypeCheck(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  if r.Header.Get("Content-Type") != "application/json" {
   app.unsupportedMediaType(w, r)

   return
  }
  next.ServeHTTP(w, r)
 })
}


func (app *application) rateLimiter(next http.Handler) http.Handler {
 limiter := tollbooth.NewLimiter(app.config.apiCallsAllowedPerSecond, nil)
 limiter.SetIPLookups([]string{"X-Real-IP", "X-Forwarded-For", "RemoteAddr"})

 return tollbooth.LimitHandler(limiter, next)
}


func (app *application) apiTimeout(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  timeoutDuration := time.Duration(app.config.apiTimeout) * time.Second

  ctx, cancel := context.WithTimeout(r.Context(), timeoutDuration)
  defer cancel()

  r = r.WithContext(ctx)

  done := make(chan struct{})

  go func() {
   next.ServeHTTP(w, r)
   close(done)
  }()

  select {
  case 



<p>The middleware need to be added to the routes. They can be either added to all the routes, or to specific ones. In our case, Content-Type checking (that is, mandating the input headers to include Content-Type: application/json) is only needed for POST requests. So modify routes.go as follows:<br>
</p>

<pre class="brush:php;toolbar:false">func (app *application) routes() http.Handler {
 mux := httprouter.New()

 mux.NotFound = http.HandlerFunc(app.notFound)
 mux.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowed)

 // Serve the Swagger UI. Uncomment this line later
 // mux.Handler("GET", "/docs/*any", httpSwagger.WrapHandler)

 mux.HandlerFunc("GET", "/status", app.status)
 mux.HandlerFunc("GET", "/materia", app.getAllMateria)

 // Adding content-type check middleware to only the POST method
 mux.Handler("POST", "/fusion", app.contentTypeCheck(http.HandlerFunc(app.fuseMateria)))

 return app.chainMiddlewares(mux)
}

func (app *application) chainMiddlewares(next http.Handler) http.Handler {
 middlewares := []func(http.Handler) http.Handler{
  app.recoverPanic,
  app.apiTimeout,
  app.rateLimiter,
 }

 for _, middleware := range middlewares {
  next = middleware(next)
 }

 return next
}

Error handling

Add the following methods to /api/errors.go to help the middleware functions:

func (app *application) unsupportedMediaType(w http.ResponseWriter, r *http.Request) {
 message := fmt.Sprintf("The %s Content-Type is not supported", r.Header.Get("Content-Type"))
 app.errorMessage(w, r, http.StatusUnsupportedMediaType, message, nil)
}

func (app *application) gatewayTimeout(w http.ResponseWriter, r *http.Request) {
 message := "Request timed out"
 app.errorMessage(w, r, http.StatusGatewayTimeout, message, nil)
}

Request and Response structure files

/api/dtos.go :

package main

// MateriaDTO provides Materia details - Name, Description and Type (Magic / Command / Support / Independent)
type MateriaDTO struct {
 Name        string `json:"name" example:"Thunder"`
 Type        string `json:"type" example:"Magic"`
 Description string `json:"description" example:"Shoots lightning forward dealing thunder damage."`
}

// StatusDTO provides status of the server
type StatusDTO struct {
 Status string `json:"Status" example:"OK"`
}

// ErrorResponseDTO provides Error message
type ErrorResponseDTO struct {
 Error string `json:"Error" example:"The server encountered a problem and could not process your request"`
}

/api/requests.go :

brew install postgresql@17

Validator, from the generated code, will be used later to validate the input fields for fusion endpoint.

Data Structure for the combination rules

Create the file /internal/crisis-core-materia-fusion/constants.go

Add the following:

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb

full list of 32 MateriaTypes can be found here.

Create the file /internal/crisis-core-materia-fusion/models.go

Add the following:

-- create an SQL user to be used by the Go server
CREATE USER go_client WITH PASSWORD 'xxxxxxxx';

-- The Go server doesn't ever need to add data to the DB. 
-- So let's give it just read permission.
CREATE ROLE readonly_role;
GRANT USAGE ON SCHEMA public TO readonly_role;

-- This command gives SELECT access to all future created tables. 
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role;

-- If you want to be more strict and give access only to tables that already exist, use this:
-- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;

GRANT readonly_role TO go_client;

full list of rules can be found here.

Handler for materia in api/handlers.go

CREATE TYPE display_materia_type AS ENUM ('Magic', 'Command', 'Support', 'Independent');

CREATE TYPE materia_type AS ENUM ('Fire', 'Ice', 'Lightning', 'Restore', 'Full Cure', 'Status Defense', 'Defense', 'Absorb Magic', 'Status Magic', 'Fire & Status', 'Ice & Status', 'Lightning & Status', 'Gravity', 'Ultimate', 'Quick Attack', 'Quick Attack & Status', 'Blade Arts', 'Blade Arts & Status', 'Fire Blade', 'Ice Blade', 'Lightning Blade', 'Absorb Blade', 'Item', 'Punch', 'SP Turbo', 'HP Up', 'AP Up', 'ATK Up', 'VIT Up', 'MAG Up', 'SPR Up', 'Dash', 'Dualcast', 'DMW', 'Libra', 'MP Up', 'Anything');

CREATE TABLE materia (
    id integer NOT NULL,
    name character varying(50) NOT NULL,
    materia_type materia_type NOT NULL,
    grade integer NOT NULL,
    display_materia_type display_materia_type,
    description text
    CONSTRAINT materia_pkey PRIMARY KEY (id)
);

-- The primary key 'id' should auto-increment by 1 for every row entry.
CREATE SEQUENCE materia_id_seq
    AS integer
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

ALTER SEQUENCE materia_id_seq OWNED BY materia.id;

ALTER TABLE ONLY materia ALTER COLUMN id SET DEFAULT nextval('materia_id_seq'::REGCLASS);

In-server cache

We are using an in-server cache because:

  1. The data fetched from the DB never changes.
  2. The same data is used by both materia and fusion endpoints.

Update main.go:

COPY materia(name,materia_type,grade,display_materia_type,description) FROM
 '<path_to_csv_file>/materiadata.csv' DELIMITER ',' CSV HEADER;
</path_to_csv_file>

Update api/helpers.go:

? codebase
├─ cmd
│  └─ api
│     ├─ errors.go
│     ├─ handlers.go
│     ├─ helpers.go
│     ├─ main.go
│     ├─ middleware.go
│     └─ server.go
├─ internal
│  ├─ database --- db.go
│  ├─ env --- env.go
│  ├─ request --- json.go
│  ├─ response --- json.go
│  └─ validator
│     ├─ helpers.go
│     └─ validators.go
├─ go.mod
├─ LICENSE
├─ Makefile
├─ README.md
└─ README.html

Handler for fusion in api/handlers.go

HTTP_PORT=4444
DB_DSN=go_client:<password>@localhost:5432/materiafusiondb?sslmode=disable
API_TIMEOUT_SECONDS=5
API_CALLS_ALLOWED_PER_SECOND=1
</password>

Complete handler code can be found here.

Swagger UI and OpenAPI definition doc

Add the Swagger library:

go get github.com/joho/godotenv

In routes.go uncomment the Swagger line, and add the import:

// At the beginning of main():
err := godotenv.Load(".env") // Loads environment variables from .env file
if err != nil { // This will be true in prod, but that's fine.
  fmt.Println("Error loading .env file")
}


// Modify config struct:
type config struct {
  baseURL string
  db      struct {
    dsn string
  }
  httpPort                 int
  apiTimeout               int
  apiCallsAllowedPerSecond float64
}

// Modify run() to use the new values from .env:
cfg.httpPort = env.GetInt("HTTP_PORT")
cfg.db.dsn = env.GetString("DB_DSN")
cfg.apiTimeout = env.GetInt("API_TIMEOUT_SECONDS")
cfg.apiCallsAllowedPerSecond = float64(env.GetInt("API_CALLS_ALLOWED_PER_SECOND"))

// cfg.baseURL = env.GetString("BASE_URL") - not required

In the handler, DTO and model files, add comments for Swagger documentation. Refer this for all options.

In the terminal, run:

go get github.com/didip/tollbooth

This creates an api/docs folder, with the definition available for Go, JSON and YAML.

To test it, start the local server and open http://localhost:4444/docs.


Final folder structure:

func (app *application) contentTypeCheck(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  if r.Header.Get("Content-Type") != "application/json" {
   app.unsupportedMediaType(w, r)

   return
  }
  next.ServeHTTP(w, r)
 })
}


func (app *application) rateLimiter(next http.Handler) http.Handler {
 limiter := tollbooth.NewLimiter(app.config.apiCallsAllowedPerSecond, nil)
 limiter.SetIPLookups([]string{"X-Real-IP", "X-Forwarded-For", "RemoteAddr"})

 return tollbooth.LimitHandler(limiter, next)
}


func (app *application) apiTimeout(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  timeoutDuration := time.Duration(app.config.apiTimeout) * time.Second

  ctx, cancel := context.WithTimeout(r.Context(), timeoutDuration)
  defer cancel()

  r = r.WithContext(ctx)

  done := make(chan struct{})

  go func() {
   next.ServeHTTP(w, r)
   close(done)
  }()

  select {
  case 



<h2>
  
  
  3. Setting up the remote PostgreSQL instance in CockroachDB
</h2>

<ol>
<li>Use the steps from here.</li>
<li>After creating the certificate, create <rootfolder>/certs/root.crt in the project and add the certificate there. We will make a reference to this file later in the Google Run configuration.</rootfolder>
</li>
<li>
<strong>CAUTION!</strong> We are <strong>NOT</strong> pushing this folder to the remote repository. Add certs/ folder to .gitignore. We are creating the certificate in local only to test the connection, if you wish.</li>
<li>Now when you go to CockroachDB → Dashboard → Left Menu → Databases, you should be able to see the DB you created.</li>
</ol>

<h3>
  
  
  Migration
</h3>

<p>From your local DB instance, run:<br>
</p><pre class="brush:php;toolbar:false">brew install postgresql@17
  1. Go to CockroachDB → Left Menu → Migrations → Add Schema → Drag the SQL file you just got. All the steps will run, except the table data insertion. It will also show you a list of steps that were executed.
  2. At the time of writing this article, the PostgreSQL instance in CockroachDB doesn’t support statements like IMPORT INTO. So I had to create an INSERT statement in a local SQL file for 270 rows (which we can derive from the pg_dump output we just got).
  3. Log into the remote instance, and run the SQL file.

Logging in to the remote instance:

# create the DB
createdb materiafusiondb
# step into the DB to perform SQL commands
psql materiafusiondb

4. Deploy a Google Cloud Run instance

  1. Create a Dockerfile like this.
  2. Go to Google Cloud Run and create a new project for the API.
  3. Create Service → Continuously deploy from a repoSETUP WITH CLOUD BUILDRepository Provider = Github → Select your repo → Build Type = Dockerfile → Save.
  4. Authentication = Allow unathenitcated invocations.
  5. Most of the defaults should be fine as is.
  6. Scroll down to Containers → Container Port = 4444.
  7. Select Variables and Secrets tab, and add the same environment variables as we have in our local .env file.

Values:

  1. HTTP_PORT = 4444
  2. DB_DSN = ?sslmode=verify-full&sslrootcert=/app/certs/root.crt
  3. API_TIMEOUT_SECONDS = 5
  4. API_CALLS_ALLOWED_PER_SECOND = 1

Using Google Secret Manager for the certificate

The last piece of the puzzle.

  1. Search for Secret Manager → Create Secret → Name = ‘DB_CERT’ → Upload the .crt certificate of the CockroachDB.
  2. In Cloud Run → (your service) → Click Edit Continuous Deployment → Scroll down to Configuration → Open Editor.
  3. Add this as the first step:
-- create an SQL user to be used by the Go server
CREATE USER go_client WITH PASSWORD 'xxxxxxxx';

-- The Go server doesn't ever need to add data to the DB. 
-- So let's give it just read permission.
CREATE ROLE readonly_role;
GRANT USAGE ON SCHEMA public TO readonly_role;

-- This command gives SELECT access to all future created tables. 
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly_role;

-- If you want to be more strict and give access only to tables that already exist, use this:
-- GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_role;

GRANT readonly_role TO go_client;

This will make Cloud Build create the file certs/root.crt in our project before the build starts, so that the Dockerfile will have access to it even though we never pushed it to our Github repository.


And that’s it. Try pushing a commit and check if the build triggers. The Cloud Run dashboard will show the URL of your hosted Go server.


For questions related to “Why did you do X and not Y?” read this.

For anything else that you want to know or discuss, go here, or comment below.

The above is the detailed content of Building an API with Go, PostgreSQL, Google Cloud and CockroachDB. For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Choosing Between Golang and Python: The Right Fit for Your ProjectChoosing Between Golang and Python: The Right Fit for Your ProjectApr 19, 2025 am 12:21 AM

Golangisidealforperformance-criticalapplicationsandconcurrentprogramming,whilePythonexcelsindatascience,rapidprototyping,andversatility.1)Forhigh-performanceneeds,chooseGolangduetoitsefficiencyandconcurrencyfeatures.2)Fordata-drivenprojects,Pythonisp

Golang: Concurrency and Performance in ActionGolang: Concurrency and Performance in ActionApr 19, 2025 am 12:20 AM

Golang achieves efficient concurrency through goroutine and channel: 1.goroutine is a lightweight thread, started with the go keyword; 2.channel is used for secure communication between goroutines to avoid race conditions; 3. The usage example shows basic and advanced usage; 4. Common errors include deadlocks and data competition, which can be detected by gorun-race; 5. Performance optimization suggests reducing the use of channel, reasonably setting the number of goroutines, and using sync.Pool to manage memory.

Golang vs. Python: Which Language Should You Learn?Golang vs. Python: Which Language Should You Learn?Apr 19, 2025 am 12:20 AM

Golang is more suitable for system programming and high concurrency applications, while Python is more suitable for data science and rapid development. 1) Golang is developed by Google, statically typing, emphasizing simplicity and efficiency, and is suitable for high concurrency scenarios. 2) Python is created by Guidovan Rossum, dynamically typed, concise syntax, wide application, suitable for beginners and data processing.

Golang vs. Python: Performance and ScalabilityGolang vs. Python: Performance and ScalabilityApr 19, 2025 am 12:18 AM

Golang is better than Python in terms of performance and scalability. 1) Golang's compilation-type characteristics and efficient concurrency model make it perform well in high concurrency scenarios. 2) Python, as an interpreted language, executes slowly, but can optimize performance through tools such as Cython.

Golang vs. Other Languages: A ComparisonGolang vs. Other Languages: A ComparisonApr 19, 2025 am 12:11 AM

Go language has unique advantages in concurrent programming, performance, learning curve, etc.: 1. Concurrent programming is realized through goroutine and channel, which is lightweight and efficient. 2. The compilation speed is fast and the operation performance is close to that of C language. 3. The grammar is concise, the learning curve is smooth, and the ecosystem is rich.

Golang and Python: Understanding the DifferencesGolang and Python: Understanding the DifferencesApr 18, 2025 am 12:21 AM

The main differences between Golang and Python are concurrency models, type systems, performance and execution speed. 1. Golang uses the CSP model, which is suitable for high concurrent tasks; Python relies on multi-threading and GIL, which is suitable for I/O-intensive tasks. 2. Golang is a static type, and Python is a dynamic type. 3. Golang compiled language execution speed is fast, and Python interpreted language development is fast.

Golang vs. C  : Assessing the Speed DifferenceGolang vs. C : Assessing the Speed DifferenceApr 18, 2025 am 12:20 AM

Golang is usually slower than C, but Golang has more advantages in concurrent programming and development efficiency: 1) Golang's garbage collection and concurrency model makes it perform well in high concurrency scenarios; 2) C obtains higher performance through manual memory management and hardware optimization, but has higher development complexity.

Golang: A Key Language for Cloud Computing and DevOpsGolang: A Key Language for Cloud Computing and DevOpsApr 18, 2025 am 12:18 AM

Golang is widely used in cloud computing and DevOps, and its advantages lie in simplicity, efficiency and concurrent programming capabilities. 1) In cloud computing, Golang efficiently handles concurrent requests through goroutine and channel mechanisms. 2) In DevOps, Golang's fast compilation and cross-platform features make it the first choice for automation tools.

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

MantisBT

MantisBT

Mantis is an easy-to-deploy web-based defect tracking tool designed to aid in product defect tracking. It requires PHP, MySQL and a web server. Check out our demo and hosting services.

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

This project is in the process of being migrated to osdn.net/projects/mingw, you can continue to follow us there. MinGW: A native Windows port of the GNU Compiler Collection (GCC), freely distributable import libraries and header files for building native Windows applications; includes extensions to the MSVC runtime to support C99 functionality. All MinGW software can run on 64-bit Windows platforms.

PhpStorm Mac version

PhpStorm Mac version

The latest (2018.2.1) professional PHP integrated development tool

Atom editor mac version download

Atom editor mac version download

The most popular open source editor