All Products
Search
Document Center

API Gateway:Kembangkan plugin gerbang dalam Go

Last Updated:Aug 27, 2026

Kembangkan dan debug secara lokal plugin gerbang kustom dalam Go untuk memperluas fungsionalitas API Gateway guna memenuhi kebutuhan bisnis yang kompleks.

Penting

Higress telah bermigrasi dari TinyGo 0.29 + Go 1.20 ke kompilasi Wasm native di Go 1.24. Go 1.24 mendukung secara native kompilasi file .wasm.

Untuk bermigrasi dari TinyGo ke Go 1.24, pindahkan logika inisialisasi plugin Anda dari fungsi main ke fungsi init dan perbarui dependensi go.mod Anda. Contoh disediakan di bawah ini.

Penyesuaian kompatibilitas untuk migrasi plugin berbasis TinyGo:

1. Jika Anda memanggil layanan eksternal selama pemrosesan header dan mengembalikan type.ActionPause, gantilah dengan types.HeaderStopAllIterationAndWatermark. Contoh "Panggil layanan eksternal dari plugin" di bawah ini menunjukkan hal tersebut.

2. Jika Anda menggunakan go-re2 (diperlukan karena dukungan regexp TinyGo tidak lengkap), gantilah dengan paket regexp standar Go.

Prasyarat

Instal Go 1.24 atau versi yang lebih baru pada mesin pengembangan Anda.

Golang

Instal Go 1.24 atau versi yang lebih baru dari panduan instalasi resmi.

Catatan

Plugin Go 1.24 memerlukan Cloud-native API Gateway 2.1.5 atau versi yang lebih baru. Versi gerbang yang lebih lama harus mengikuti Kembangkan plugin WASM menggunakan bahasa Go.

Windows

  • Unduh file instalasi.

  • Jalankan installer. Go terinstal ke Program Files atau Program Files (x86) secara default.

  • Tekan Win+R, masukkan cmd, lalu klik OK untuk membuka command prompt. Jalankan go version untuk memverifikasi instalasi.

macOS

  • Unduh paket instalasi.

  • Klik ganda paket tersebut untuk menginstal. Go terinstal ke /usr/local/go secara default.

  • Buka terminal dan jalankan go version untuk memverifikasi instalasi.

Linux

  • Unduh paket instalasi.

  • Jalankan perintah-perintah berikut untuk menginstal Go:

    • Instal Go.

      rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz
    • Konfigurasikan variabel lingkungan.

      export PATH=$PATH:/usr/local/go/bin
    • Jalankan go version untuk memverifikasi instalasi.

Tulis plugin

Inisialisasi proyek

  1. Buat direktori proyek dan inisialisasi modul Go:

       mkdir wasm-demo-go && cd wasm-demo-go
       go mod init wasm-demo-go
  2. Unduh dependensi SDK plugin:

       go get github.com/higress-group/proxy-wasm-go-sdk@go-1.24
       go get github.com/higress-group/wasm-go@main
       go get github.com/tidwall/gjson

    Jika Anda berada di daratan Tiongkok, atur proxy terlebih dahulu:

       go env -w GOPROXY=https://proxy.golang.com.cn,direct

Tulis file main.go

Contoh berikut menambahkan header permintaan hello: world ke setiap permintaan masuk. Ketika mockEnable bernilai true dalam konfigurasi plugin, plugin akan langsung mengembalikan hello world alih-alih meneruskan ke backend.

package main

import (
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
  "github.com/tidwall/gjson"
)

func main() {}

func init() {
  wrapper.SetCtx(
    // Nama plugin
    "my-plugin",
    // Parser konfigurasi kustom
     wrapper.ParseConfigBy(parseConfig),
    // Hook ke fase pemrosesan header permintaan
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

// Konfigurasi plugin kustom
type MyConfig struct {
  mockEnable bool
}

// Uraikan konfigurasi JSON ke dalam struct config.
// Konsol gerbang secara otomatis mengonversi YAML ke JSON.
func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
  config.mockEnable = json.Get("mockEnable").Bool()
  return nil
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  proxywasm.AddHttpRequestHeader("hello", "world")
  if config.mockEnable {
    proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
  }
  return types.HeaderContinue
}

Poin-poin penting:

  • Fungsi main() harus kosong. Tempatkan seluruh logika inisialisasi di init().

  • wrapper.SetCtx mendaftarkan nama plugin, parser konfigurasi, dan hook pemrosesan.

  • Kembalikan types.HeaderContinue (setara dengan types.ActionContinue) untuk meneruskan permintaan ke filter berikutnya.

Referensi SDK

Metode utilitas

SDK plugin menyediakan metode proxywasm berikut untuk manipulasi permintaan dan respons:

Pemrosesan header permintaan (berlaku selama fase header permintaan)

Metode

Tujuan

GetHttpRequestHeaders

Ambil semua header permintaan

ReplaceHttpRequestHeaders

Ganti semua header permintaan

GetHttpRequestHeader

Ambil header permintaan tertentu

RemoveHttpRequestHeader

Hapus header permintaan tertentu

ReplaceHttpRequestHeader

Ganti header permintaan tertentu

AddHttpRequestHeader

Tambahkan header permintaan

Pemrosesan badan permintaan (berlaku selama fase badan permintaan)

Metode

Tujuan

GetHttpRequestBody

Ambil badan permintaan

AppendHttpRequestBody

Tambahkan data ke akhir badan permintaan

PrependHttpRequestBody

Tambahkan data ke awal badan permintaan

ReplaceHttpRequestBody

Ganti seluruh badan permintaan

Pemrosesan header respons (berlaku selama fase header respons)

Metode

Tujuan

GetHttpResponseHeaders

Ambil semua header respons dari backend

ReplaceHttpResponseHeaders

Ganti semua header respons

GetHttpResponseHeader

Ambil header respons tertentu

RemoveHttpResponseHeader

Hapus header respons tertentu

ReplaceHttpResponseHeader

Ganti header respons tertentu

AddHttpResponseHeader

Tambahkan header respons

Pemrosesan badan respons (berlaku selama fase badan respons)

Metode

Tujuan

GetHttpResponseBody

Ambil badan respons

AppendHttpResponseBody

Tambahkan data ke akhir badan respons

PrependHttpResponseBody

Tambahkan data ke awal badan respons

ReplaceHttpResponseBody

Ganti seluruh badan respons

Panggilan HTTP dan kontrol aliran

Metode

Tujuan

DispatchHttpCall

Kirim permintaan HTTP ke layanan eksternal

GetHttpCallResponseHeaders

Ambil header respons dari permintaan DispatchHttpCall

GetHttpCallResponseBody

Ambil badan respons dari permintaan DispatchHttpCall

GetHttpCallResponseTrailers

Ambil trailer respons dari permintaan DispatchHttpCall

SendHttpResponse

Kembalikan respons HTTP langsung ke klien

ResumeHttpRequest

Lanjutkan aliran pemrosesan permintaan yang dijeda

ResumeHttpResponse

Lanjutkan aliran pemrosesan respons yang dijeda

Penting

Jangan panggil ResumeHttpRequest atau ResumeHttpResponse ketika pemrosesan tidak dijeda. Setelah SendHttpResponse dipanggil, permintaan atau respons yang dijeda akan dilanjutkan secara otomatis. Memanggil ResumeHttpRequest atau ResumeHttpResponse lagi menyebabkan perilaku yang tidak terdefinisi.

Kode status header

Setiap hook pemrosesan mengembalikan kode status header yang mengontrol aliran permintaan. Pilih status yang sesuai berdasarkan apakah plugin Anda perlu menjeda, mem-buffer, atau melakukan streaming data:

Status

Perilaku

HeaderContinue

Filter saat ini selesai. Teruskan permintaan ke filter berikutnya. Setara dengan types.ActionContinue.

HeaderStopIteration

Tahan header tetapi lanjutkan membaca data badan. Gunakan ini untuk memodifikasi header permintaan selama fase pemrosesan badan. Badan diperlukan — jika tidak ada badan, permintaan akan diblokir tanpa batas. Periksa dengan HasRequestBody().

HeaderContinueAndEndStream

Teruskan header ke filter berikutnya dengan end_stream = false, memungkinkan filter saat ini menambahkan lebih banyak data badan.

HeaderStopAllIterationAndBuffer

Hentikan semua iterasi dan buffer header, badan, serta trailer. Jika buffer melebihi batas, gerbang mengembalikan 413 selama fase permintaan atau 500 selama fase respons. Lanjutkan dengan proxywasm.ResumeHttpRequest(), proxywasm.ResumeHttpResponse(), atau proxywasm.SendHttpResponseWithDetail().

HeaderStopAllIterationAndWatermark

Sama seperti HeaderStopAllIterationAndBuffer, tetapi memicu pembatasan kecepatan tingkat koneksi ketika buffer melebihi batas alih-alih mengembalikan error. Setara dengan types.ActionPause dalam ABI 0.2.1.

Catatan

Untuk contoh nyata penggunaan HeaderStopIteration dan HeaderStopAllIterationAndWatermark, lihat plugin Higress ai-transformer dan ai-quota.

Kompilasi file Wasm

Kompilasi plugin menjadi biner Wasm:

go mod tidy
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o main.wasm ./

Perintah ini menghasilkan file main.wasm. Gunakan file ini untuk debugging lokal atau unggah melalui pasar gerbang cloud-native untuk men-deploy plugin kustom.

Untuk men-deploy melalui Higress menggunakan CustomResourceDefinition (CRD) WasmPlugin atau antarmuka Konsol, kemas file Wasm ke dalam citra OCI atau Docker. Untuk detailnya, lihat Plugin kustom.

Debugging lokal

Prasyarat

Instal Docker.

Siapkan lingkungan pengujian

Pastikan main.wasm ada di direktori proyek Anda, lalu buat dua file berikut.

docker-compose.yaml

version: '3.7'
services:
  envoy:
    image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.1.5
    entrypoint: /usr/local/bin/envoy
    # Aktifkan logging level debug untuk Wasm. Gunakan level info di produksi.
    command: -c /etc/envoy/envoy.yaml --component-log-level wasm:debug
    depends_on:
    - httpbin
    networks:
    - wasmtest
    ports:
    - "10000:10000"
    volumes:
    - ./envoy.yaml:/etc/envoy/envoy.yaml
    - ./main.wasm:/etc/envoy/main.wasm

  httpbin:
    image: kennethreitz/httpbin:latest
    networks:
    - wasmtest
    ports:
    - "12345:80"

networks:
  wasmtest: {}

envoy.yaml

admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 10000
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          scheme_header_transformation:
            scheme_to_overwrite: https
          stat_prefix: ingress_http
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              - match:
                  prefix: "/"
                route:
                  cluster: httpbin
          http_filters:
          - name: wasmdemo
            typed_config:
              "@type": type.googleapis.com/udpa.type.v1.TypedStruct
              type_url: type.googleapis.com/envoy.extensions.filters.http.wasm.v3.Wasm
              value:
                config:
                  name: wasmdemo
                  vm_config:
                    runtime: envoy.wasm.runtime.v8
                    code:
                      local:
                        filename: /etc/envoy/main.wasm
                  configuration:
                    "@type": "type.googleapis.com/google.protobuf.StringValue"
                    value: |
                      {
                        "mockEnable": false
                      }
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
  clusters:
  - name: httpbin
    connect_timeout: 30s
    type: LOGICAL_DNS
    # Beri komentar baris berikut untuk menguji di jaringan v6
    dns_lookup_family: V4_ONLY
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: httpbin
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: httpbin
                port_value: 80

Jalankan lingkungan:

docker compose up

Verifikasi

Uji header yang ditambahkan. Kirim permintaan melalui gerbang (port 10000) dan pastikan header Hello: world muncul:

curl http://127.0.0.1:10000/get

Respons yang diharapkan:

{
  "args": {},
  "headers": {
    "Accept": "*/*",
    "Hello": "world",
    "Host": "127.0.0.1:10000",
    "Original-Host": "127.0.0.1:10000",
    "Req-Start-Time": "1681269273896",
    "User-Agent": "curl/7.79.1",
    "X-Envoy-Expected-Rq-Timeout-Ms": "15000"
  },
  "origin": "172.18.0.3",
  "url": "https://127.0.0.1:10000/get"
}

Header Hello: world mengonfirmasi bahwa plugin aktif.

Sebagai perbandingan, permintaan langsung ke httpbin (port 12345) tidak menyertakan header ini:

curl http://127.0.0.1:12345/get

Uji perubahan konfigurasi. Edit envoy.yaml dan atur mockEnable menjadi true:

configuration:
    "@type": "type.googleapis.com/google.protobuf.StringValue"
    value: |
      {
        "mockEnable": true
      }

Mulai ulang lingkungan dan kirim permintaan yang sama:

curl http://127.0.0.1:10000/get

Respons yang diharapkan:

hello world

Respons mock ini mengonfirmasi bahwa perubahan konfigurasi berlaku dengan benar.

Contoh lainnya

Plugin tanpa konfigurasi

Untuk plugin yang tidak memerlukan konfigurasi, definisikan struct config kosong dan hilangkan ParseConfigBy:

package main

import (
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
)

func main() {}

func init() {
  wrapper.SetCtx(
    "hello-world",
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

type MyConfig struct {}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  proxywasm.SendHttpResponse(200, nil, []byte("hello world"), -1)
  return types.HeaderContinue
}

Panggil layanan HTTP eksternal

Plugin mendukung panggilan HTTP ke layanan Nacos, layanan Kubernetes, serta layanan alamat tetap atau DNS yang dikonfigurasi di Konsol gerbang. Pustaka net/http standar tidak tersedia di runtime Wasm. Gunakan klien HTTP terenkapsulasi dari SDK sebagai gantinya.

Contoh berikut mengurai konfigurasi layanan selama startup, lalu memanggil layanan tersebut selama pemrosesan permintaan. Plugin mengekstrak token dari header respons dan menyuntikkannya ke permintaan asli.

package main

import (
  "errors"
  "net/http"
  "strings"
  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
  "github.com/tidwall/gjson"
)

func main() {}

func init() {
  wrapper.SetCtx(
    "http-call",
    wrapper.ParseConfigBy(parseConfig),
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

type MyConfig struct {
  // Klien HTTP untuk panggilan eksternal
  client      wrapper.HttpClient
  // Path yang diminta pada layanan eksternal
  requestPath string
  // Kunci header respons untuk diekstrak dan disuntikkan ke permintaan asli
  tokenHeader string
}

func parseConfig(json gjson.Result, config *MyConfig, log logs.Log) error {
  config.tokenHeader = json.Get("tokenHeader").String()
  if config.tokenHeader == "" {
    return errors.New("missing tokenHeader in config")
  }
  config.requestPath = json.Get("requestPath").String()
  if config.requestPath == "" {
    return errors.New("missing requestPath in config")
  }
  // FQDN lengkap dengan akhiran tipe layanan.
  // Contoh: my-svc.dns, my-svc.static,
  //   service-provider.DEFAULT-GROUP.public.nacos,
  //   httpbin.my-ns.svc.cluster.local
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      // Port default untuk layanan IP statis
      servicePort = 80
    }
  }
  config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
        })
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  // Kirim permintaan HTTP GET async. Timeout default adalah 500 ms.
  err := config.client.Get(config.requestPath, nil,
           // Callback dijalankan saat respons tiba
           func(statusCode int, responseHeaders http.Header, responseBody []byte) {
             if statusCode != http.StatusOK {
               log.Errorf("http call failed, status: %d", statusCode)
               proxywasm.SendHttpResponse(http.StatusInternalServerError, nil,
                 []byte("http call failed"), -1)
               return
             }
             log.Infof("get status: %d, response body: %s", statusCode, responseBody)
             // Ekstrak token dari respons dan tambahkan ke permintaan asli
             token := responseHeaders.Get(config.tokenHeader)
             if token != "" {
               proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
             }
             // Lanjutkan permintaan yang dijeda agar dapat diteruskan ke backend
             proxywasm.ResumeHttpRequest()
    })

  if err != nil {
    // Jika panggilan layanan gagal, biarkan permintaan dilanjutkan dan catat error
    log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
    return types.ActionContinue
  } else {
    // Jeda permintaan hingga callback async selesai
    return types.HeaderStopAllIterationAndWatermark
  }
}

Panggil Redis dari plugin

Contoh berikut mengimplementasikan plugin pembatasan laju berbasis Redis. Plugin ini melacak permintaan per menit (QPM) dan mengembalikan HTTP 429 ketika batas terlampaui.

package main

import (
  "strconv"
  "time"

  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
  "github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
  "github.com/tidwall/gjson"
  "github.com/tidwall/resp"

  "github.com/higress-group/wasm-go/pkg/wrapper"
  logs "github.com/higress-group/wasm-go/pkg/log"
)

func main() {}

func init() {
  wrapper.SetCtx(
    "redis-demo",
    wrapper.ParseConfigBy(parseConfig),
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
    wrapper.ProcessResponseHeadersBy(onHttpResponseHeaders),
  )
}

type RedisCallConfig struct {
  client wrapper.RedisClient
  qpm    int
}

func parseConfig(json gjson.Result, config *RedisCallConfig, log logs.Log) error {
  // FQDN lengkap dengan akhiran tipe layanan.
  // Contoh: my-redis.dns, redis.my-ns.svc.cluster.local
  serviceName := json.Get("serviceName").String()
  servicePort := json.Get("servicePort").Int()
  if servicePort == 0 {
    if strings.HasSuffix(serviceName, ".static") {
      servicePort = 80
    } else {
      servicePort = 6379
    }
  }
  username := json.Get("username").String()
  password := json.Get("password").String()
  // Timeout dalam milidetik
  timeout := json.Get("timeout").Int()
  if timeout == 0 {
    timeout = 1000
  }
  qpm := json.Get("qpm").Int()
  config.qpm = int(qpm)
  config.client = wrapper.NewRedisClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
  })
  return config.client.Init(username, password, timeout)
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
  now := time.Now()
  minuteAligned := now.Truncate(time.Minute)
  timeStamp := strconv.FormatInt(minuteAligned.Unix(), 10)
  // Jika err != nil, kemungkinan besar gerbang tidak dapat menjangkau backend Redis.
  // Pastikan layanan Redis belum dihapus.
  err := config.client.Incr(timeStamp, func(response resp.Value) {
    if response.Error() != nil {
      log.Errorf("call redis error: %v", response.Error())
      proxywasm.ResumeHttpRequest()
    } else {
      ctx.SetContext("timeStamp", timeStamp)
      ctx.SetContext("callTimeLeft", strconv.Itoa(config.qpm-response.Integer()))
      if response.Integer() == 1 {
        err := config.client.Expire(timeStamp, 60, func(response resp.Value) {
          if response.Error() != nil {
            log.Errorf("call redis error: %v", response.Error())
          }
          proxywasm.ResumeHttpRequest()
        })
        if err != nil {
          log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
          proxywasm.ResumeHttpRequest()
        }
      } else {
        if response.Integer() > config.qpm {
          proxywasm.SendHttpResponse(429, [][2]string{{"timeStamp", timeStamp}, {"callTimeLeft", "0"}}, []byte("Too many requests\n"), -1)
        } else {
          proxywasm.ResumeHttpRequest()
        }
      }
    }
  })
  if err != nil {
    // Jika panggilan Redis gagal, biarkan permintaan dilanjutkan dan catat error
    log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
    return types.HeaderContinue
  } else {
    // Jeda permintaan hingga callback Redis selesai
    return types.HeaderStopAllIterationAndWatermark
  }
}

func onHttpResponseHeaders(ctx wrapper.HttpContext, config RedisCallConfig, log logs.Log) types.Action {
  if ctx.GetContext("timeStamp") != nil {
    proxywasm.AddHttpResponseHeader("timeStamp", ctx.GetContext("timeStamp").(string))
  }
  if ctx.GetContext("callTimeLeft") != nil {
    proxywasm.AddHttpResponseHeader("callTimeLeft", ctx.GetContext("callTimeLeft").(string))
  }
  return types.HeaderContinue
}