Todos os produtos
Search
Central de documentação

API Gateway:Desenvolver plugins de gateway em Go

Última atualização: Aug 26, 2026

Desenvolva e depure localmente plugins de gateway personalizados em Go para estender a funcionalidade do API gateway e atender a requisitos de negócios complexos.

Importante

O Higress migrou do TinyGo 0.29 + Go 1.20 para compilação Wasm nativa no Go 1.24. O Go 1.24 oferece suporte nativo à compilação de arquivos .wasm.

Para migrar do TinyGo para o Go 1.24, mova a lógica de inicialização do plugin da função main para a função init e atualize as dependências no arquivo go.mod. Confira um exemplo abaixo.

Ajustes de compatibilidade para migrar plugins baseados em TinyGo:

1. Se você chamar um service externo durante o processamento de cabeçalhos e retornar type.ActionPause, substitua por types.HeaderStopAllIterationAndWatermark. O exemplo "Chamar um service HTTP externo", apresentado mais adiante, demonstra esse procedimento.

2. Caso tenha utilizado go-re2 (necessário porque o TinyGo tinha suporte incompleto ao pacote regexp), substitua-o pelo pacote padrão regexp do Go.

Pré-requisitos

Instale o Go 1.24 ou superior na sua máquina de desenvolvimento.

Golang

Instale o Go 1.24 ou superior seguindo o guia de instalação oficial.

Nota

Plugins desenvolvidos com Go 1.24 exigem o Cloud-native API Gateway versão 2.1.5 ou superior. Para versões anteriores do gateway, consulte Desenvolver plugins WASM usando a linguagem Go.

Windows

  • Baixe o arquivo de instalação.

  • Execute o instalador. Por padrão, o Go é instalado em Program Files ou Program Files (x86).

  • Pressione Win+R, insira cmd e clique em OK para abrir o prompt de comando. Execute go version para verificar a instalação.

macOS

  • Baixe o pacote de instalação.

  • Clique duas vezes no pacote para instalar. O Go é instalado em /usr/local/go por padrão.

  • Abra um terminal e execute go version para verificar a instalação.

Linux

  • Baixe o pacote de instalação.

  • Execute os comandos a seguir para instalar o Go:

    • Instale o Go.

      rm -rf /usr/local/go && tar -C /usr/local -xzf go1.24.4.linux-amd64.tar.gz
    • Configure a variável de ambiente.

      export PATH=$PATH:/usr/local/go/bin
    • Execute go version para verificar a instalação.

Escrever o plugin

Inicializar o projeto

  1. Crie um diretório para o projeto e inicialize um módulo Go:

       mkdir wasm-demo-go && cd wasm-demo-go
       go mod init wasm-demo-go
  2. Baixe as dependências do sdk do 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

    Se estiver na China continental, configure um proxy antes:

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

Escrever o arquivo main.go

O exemplo a seguir adiciona um cabeçalho de requisição hello: world a todas as requisições recebidas. Quando mockEnable está definido como true na configuração do plugin, ele retorna hello world diretamente, sem encaminhar a requisição ao 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(
    // Plugin name
    "my-plugin",
    // Custom configuration parser
     wrapper.ParseConfigBy(parseConfig),
    // Hook into the request header processing phase
    wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
  )
}

// Custom plugin configuration
type MyConfig struct {
  mockEnable bool
}

// Parse the JSON configuration into the config struct.
// The gateway console converts YAML to JSON automatically.
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
}

Pontos importantes:

  • A função main() deve estar vazia. Coloque toda a lógica de inicialização em init().

  • wrapper.SetCtx registra o nome do plugin, o analisador de configuração e os hooks de processamento.

  • Retorne types.HeaderContinue (equivalente a types.ActionContinue) para passar a requisição ao próximo filtro.

Referência do sdk

Métodos utilitários

O sdk do plugin fornece os seguintes métodos proxywasm para manipulação de requisições e respostas:

Processamento de cabeçalhos de requisição (efetivo durante a fase de cabeçalho da requisição)

Método

Finalidade

GetHttpRequestHeaders

Obtém todos os cabeçalhos da requisição

ReplaceHttpRequestHeaders

Substitui todos os cabeçalhos da requisição

GetHttpRequestHeader

Obtém um cabeçalho específico da requisição

RemoveHttpRequestHeader

Remove um cabeçalho específico da requisição

ReplaceHttpRequestHeader

Substitui um cabeçalho específico da requisição

AddHttpRequestHeader

Adiciona um cabeçalho à requisição

Processamento do corpo da requisição (efetivo durante a fase de corpo da requisição)

Método

Finalidade

GetHttpRequestBody

Obtém o corpo da requisição

AppendHttpRequestBody

Acrescenta dados ao final do corpo da requisição

PrependHttpRequestBody

Insere dados no início do corpo da requisição

ReplaceHttpRequestBody

Substitui todo o corpo da requisição

Processamento de cabeçalhos de resposta (efetivo durante a fase de cabeçalho da resposta)

Método

Finalidade

GetHttpResponseHeaders

Obtém todos os cabeçalhos de resposta do backend

ReplaceHttpResponseHeaders

Substitui todos os cabeçalhos de resposta

GetHttpResponseHeader

Obtém um cabeçalho específico da resposta

RemoveHttpResponseHeader

Remove um cabeçalho específico da resposta

ReplaceHttpResponseHeader

Substitui um cabeçalho específico da resposta

AddHttpResponseHeader

Adiciona um cabeçalho à resposta

Processamento do corpo da resposta (efetivo durante a fase de corpo da resposta)

Método

Finalidade

GetHttpResponseBody

Obtém o corpo da resposta

AppendHttpResponseBody

Acrescenta dados ao final do corpo da resposta

PrependHttpResponseBody

Insere dados no início do corpo da resposta

ReplaceHttpResponseBody

Substitui todo o corpo da resposta

Chamadas HTTP e controle de fluxo

Método

Finalidade

DispatchHttpCall

Envia uma requisição HTTP para um service externo

GetHttpCallResponseHeaders

Obtém os cabeçalhos de resposta de uma requisição DispatchHttpCall

GetHttpCallResponseBody

Obtém o corpo da resposta de uma requisição DispatchHttpCall

GetHttpCallResponseTrailers

Obtém os trailers de resposta de uma requisição DispatchHttpCall

SendHttpResponse

Retorna uma resposta HTTP diretamente ao cliente

ResumeHttpRequest

Retoma um fluxo de processamento de requisição pausado

ResumeHttpResponse

Retoma um fluxo de processamento de resposta pausado

Importante

Não chame ResumeHttpRequest ou ResumeHttpResponse quando o processamento não estiver pausado. Após a chamada de SendHttpResponse, qualquer requisição ou resposta pausada é retomada automaticamente. Chamar ResumeHttpRequest ou ResumeHttpResponse novamente causa comportamento indefinido.

Códigos de status de cabeçalho

Cada hook de processamento retorna um código de status de cabeçalho que controla o fluxo da requisição. Escolha o status apropriado dependendo se o seu plugin precisa pausar, armazenar em buffer ou transmitir dados:

Status

Comportamento

HeaderContinue

O filtro atual foi concluído. Passa a requisição para o próximo filtro. Equivalente a types.ActionContinue.

HeaderStopIteration

Retém o cabeçalho, mas continua lendo os dados do corpo. Use esta opção para modificar cabeçalhos de requisição durante a fase de processamento do corpo. Um corpo é obrigatório — se não houver corpo, a requisição fica bloqueada indefinidamente. Verifique com HasRequestBody().

HeaderContinueAndEndStream

Passa o cabeçalho para o próximo filtro com end_stream = false, permitindo que o filtro atual anexe mais dados ao corpo.

HeaderStopAllIterationAndBuffer

Interrompe toda iteração e armazena em buffer cabeçalhos, corpo e trailers. Se o buffer exceder o limite, o gateway retorna 413 durante a fase de requisição ou 500 durante a fase de resposta. Retome com proxywasm.ResumeHttpRequest(), proxywasm.ResumeHttpResponse() ou proxywasm.SendHttpResponseWithDetail().

HeaderStopAllIterationAndWatermark

Igual a HeaderStopAllIterationAndBuffer, mas aciona limitação no nível da conexão quando o buffer excede o limite, em vez de retornar um erro. Equivalente a types.ActionPause na ABI 0.2.1.

Nota

Para exemplos práticos de uso de HeaderStopIteration e HeaderStopAllIterationAndWatermark, consulte os plugins Higress ai-transformer e ai-quota.

Compilar o arquivo Wasm

Compile o plugin em um binário Wasm:

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

Esse processo gera um arquivo main.wasm. Utilize esse arquivo para depuração local ou faça upload dele pelo marketplace do gateway cloud-native para implantar um plugin personalizado.

Para implantar via Higress usando a CustomResourceDefinition (CRD) WasmPlugin ou a interface do console, empacote o arquivo Wasm em uma imagem OCI ou docker. Para mais detalhes, consulte Plugins personalizados.

Depuração local

Pré-requisitos

Instale o docker.

Configurar o ambiente de teste

Certifique-se de que o arquivo main.wasm exista no diretório do seu projeto e crie os dois arquivos a seguir.

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
    # Enable debug-level logging for Wasm. Use info level in production.
    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
    # Comment out the following line to test on v6 networks
    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

Inicie o ambiente:

docker compose up

Verificar

Testar o cabeçalho adicionado. Envie uma requisição através do gateway (porta 10000) e confirme se o cabeçalho Hello: world aparece:

curl http://127.0.0.1:10000/get

Resposta esperada:

{
  "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"
}

A presença do cabeçalho Hello: world confirma que o plugin está ativo.

Para comparação, uma requisição direta ao httpbin (porta 12345) não inclui esse cabeçalho:

curl http://127.0.0.1:12345/get

Testar uma alteração de configuração. Edite o arquivo envoy.yaml e defina mockEnable como true:

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

Reinicie o ambiente e envie a mesma requisição:

curl http://127.0.0.1:10000/get

Resposta esperada:

hello world

A resposta simulada confirma que as alterações de configuração entraram em vigor corretamente.

Mais exemplos

Plugin sem configuração

Para um plugin que não exige configuração, defina uma struct de configuração vazia e omita 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
}

Chamar um service HTTP externo

Os plugins suportam chamadas HTTP para services Nacos, services Kubernetes e services de endereço fixo ou DNS configurados no console do gateway. A biblioteca padrão net/http não está disponível no runtime Wasm. Em vez disso, utilize o cliente HTTP encapsulado do sdk.

O exemplo a seguir analisa a configuração do service durante a inicialização e depois chama o service durante o processamento da requisição. Ele extrai um token dos cabeçalhos de resposta e o injeta na requisição original.

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 {
  // HTTP client for external calls
  client      wrapper.HttpClient
  // Path to request on the external service
  requestPath string
  // Response header key to extract and inject into the original request
  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")
  }
  // Full FQDN with service type suffix.
  // Examples: 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") {
      // Default port for static IP services
      servicePort = 80
    }
  }
  config.client = wrapper.NewClusterClient(wrapper.FQDNCluster{
    FQDN: serviceName,
    Port: servicePort,
        })
}

func onHttpRequestHeaders(ctx wrapper.HttpContext, config MyConfig, log logs.Log) types.Action {
  // Send an async HTTP GET request. Default timeout is 500 ms.
  err := config.client.Get(config.requestPath, nil,
           // Callback runs when the response arrives
           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)
             // Extract the token from the response and add it to the original request
             token := responseHeaders.Get(config.tokenHeader)
             if token != "" {
               proxywasm.AddHttpRequestHeader(config.tokenHeader, token)
             }
             // Resume the paused request so it can be forwarded to the backend
             proxywasm.ResumeHttpRequest()
    })

  if err != nil {
    // If the service call fails, let the request proceed and log the error
    log.Errorf("Error occured while calling http, it seems cannot find the service cluster.")
    return types.ActionContinue
  } else {
    // Pause the request until the async callback completes
    return types.HeaderStopAllIterationAndWatermark
  }
}

Chamar Redis a partir de um plugin

O exemplo a seguir implementa um plugin de limitação de taxa com suporte a Redis. Ele rastreia requisições por minuto (QPM) e retorna HTTP 429 quando o limite é excedido.

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 {
  // Full FQDN with service type suffix.
  // Examples: 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 in milliseconds
  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)
  // If err != nil, the gateway likely cannot reach the Redis backend.
  // Verify that the Redis service has not been deleted.
  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 {
    // If the Redis call fails, let the request proceed and log the error
    log.Errorf("Error occured while calling redis, it seems cannot find the redis cluster.")
    return types.HeaderContinue
  } else {
    // Pause the request until the Redis callback completes
    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
}