基于Netlify的Go云函数开发

Go基础云函数开发示例

目标:创建一个名为 hello 的 API 接口,它能够:

  1. 被通过 .../.netlify/functions/hello 访问。
  2. 可以接收一个可选的 name 查询参数,例如 ?name=Go
  3. 返回一个 JSON 格式的问候语。

第一阶段:创建一个最简单的 “Hello, World” 风格的 Go 云函数并部署到 Netlify。熟悉从本地开发、测试到线上部署的完整工作流。


开发环境准备

在开始之前,请确保您的电脑上已经安装了以下工具:

  1. Go 语言环境:这里使用的版本是 v0.147.3
  2. Netlify 账号Signup
  3. Netlify CLI:用于本地测试。安装命令:pnpm install netlify-cli -g
  4. Git 和 GitHub 账号:用于代码版本控制和部署。

初始化项目

首先,我们在本地创建一个项目目录并初始化 Go 模块。

  1. 打开您的终端或命令行工具。

  2. 创建项目目录并进入:

    bash
    mkdir my-netlify-go-app
    cd my-netlify-go-app
  3. 初始化 Go 模块。这会创建一个 go.mod 文件,用于管理依赖。

    bash
    go mod init my-netlify-go-app
  4. 创建 Netlify Functions 所需的目录结构:

    bash
    mkdir -p netlify/functions/hello
    • netlify/functions/ 是 Netlify 默认存放所有云函数的目录。
    • hello/ 是我们第一个函数的名字。

项目结构现在是:

text
/my-netlify-go-app
|-- go.mod
|-- netlify/
    |-- functions/
        |-- hello/

编写Go云函数代码

现在,我们来编写函数的具体逻辑。

  1. netlify/functions/hello/ 目录下创建一个 main.go 文件。

  2. 将以下代码复制并粘贴到 main.go 文件中:

    go
    // netlify/functions/hello/main.go
    package main
    
    import (
    	"encoding/json"
    	"fmt"
    	"github.com/aws/aws-lambda-go/events"
    	"github.com/aws/aws-lambda-go/lambda"
    )
    
    // Response is a custom struct for our JSON response
    type Response struct {
    	Message string `json:"message"`
    }
    
    // handler is the main function logic
    func handler(request events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {
    	// 1. 从查询参数中获取 'name'
    	name := request.QueryStringParameters["name"]
    
    	// 2. 如果 'name' 为空,则提供一个默认值
    	if name == "" {
    		name = "World"
    	}
    
    	// 3. 构建我们的响应消息
    	message := fmt.Sprintf("Hello, %s!", name)
    
    	// 4. 将消息封装到我们的自定义 Response 结构体中
    	responseObject := Response{
    		Message: message,
    	}
    
    	// 5. 将 Response 结构体序列化为 JSON 字符串
    	responseBody, err := json.Marshal(responseObject)
    	if err != nil {
    		return nil, err
    	}
    
    	// 6. 返回一个标准的 API Gateway 响应
    	return &events.APIGatewayProxyResponse{
    		StatusCode: 200, // OK
    		Headers: map[string]string{
    			"Content-Type": "application/json",
    		},
    		Body: string(responseBody),
    	}, nil
    }
    
    func main() {
    	// 将我们的 handler 函数包装成一个 Lambda 服务
    	lambda.Start(handler)
    }
  3. 安装依赖。aws-lambda-go 是让我们的 Go 代码能与 AWS Lambda (Netlify Functions 的底层) 兼容的关键库。

    bash
    go get github.com/aws/aws-lambda-go/events
    go get github.com/aws/aws-lambda-go/lambda

    运行后,go.modgo.sum 文件会自动更新。


Netlify构建配置

我们需要告诉 Netlify 如何编译我们的 Go 代码。

  1. 在项目根目录(与 go.mod 同级)创建一个 netlify.toml 文件。

  2. 将以下配置粘贴到 netlify.toml 文件中:

    toml
    # netlify.toml
    
    [build]
      # 构建命令:编译我们的 Go 函数
      # -o 指定输出文件路径和名称。Netlify 会在这个目录里寻找可执行文件。
      command = "go build -o netlify/functions/hello/main netlify/functions/hello/main.go"
    
      # 函数目录
      functions = "netlify/functions/"
    
    [functions]
      # 明确指定 Go 函数的运行时
      # 这有助于 Netlify 优化构建过程
      included_files = ["**/*.go", "go.mod", "go.sum"]

本地测试(Netlify CLI)

在部署到线上之前,我们先用 Netlify CLI 在本地进行测试。

  1. 在项目根目录运行:

    bash
    netlify dev
  2. Netlify CLI 会启动一个本地开发服务器。你会看到类似下面的输出:

    text
    ◈ Server listening on http://localhost:8888
    ...
    ◈ Functions server is listening on 53011
  3. 现在,打开你的浏览器或 Postman,访问以下 URL:

如果本地测试成功,说明我们的代码和配置都是正确的!

确认这个基本流程成功跑通后,就可以进入下一步:在 Go 函数中集成 Supabase 或Neon。


上线—部署到Netlify

最后一步,我们将它部署到互联网上。

  1. 初始化 Git 并推送到 GitHub

    bash
    git init
    git add .
    git commit -m "feat: initial commit with hello function"
    • 在 GitHub 上创建一个新的空仓库(例如 my-netlify-go-app)。
    • 按照 GitHub 提供的指令,将你的本地仓库与远程仓库关联并推送:
      bash
      git remote add origin https://github.com/YOUR_USERNAME/YOUR_REPO_NAME.git
      git branch -M main
      git push -u origin main
  2. 在 Netlify 上创建站点

    • 登录 Netlify,点击 “Add new site” -> “Import an existing project”。
    • 选择 GitHub,并授权 Netlify 访问你的仓库。
    • 从列表中选择你刚刚创建的 my-netlify-go-app 仓库。
    • Netlify 会自动读取你的 netlify.toml 文件,所以构建设置通常无需修改。直接点击 “Deploy site”。
  3. 等待部署完成: Netlify 会开始拉取代码、执行 go build 命令并部署。你可以在 “Deploys” 页面看到部署日志。

  4. 验证线上接口

    • 部署成功后,Netlify 会给你一个独一无二的网址,例如 https://some-random-name.netlify.app
    • 你的 Go 云函数 API 就可以通过 https://your-site-name.netlify.app/.netlify/functions/hello 访问了。
    • 像本地测试一样,在浏览器中访问它,看看是否能得到正确的结果。

Go云函数与Neon数据库集成

目标: 沿用 my-netlify-go-app 项目,在其中添加一个名为 get-products 的新 API 接口。这个接口将连接到您的 Neon 数据库,查询一个产品列表,并以 JSON 格式返回。

创建一个新的 API 端点:.../.netlify/functions/get-products。当访问它时,它会:

  1. 安全地从环境变量中读取 Neon 数据库的连接信息。
  2. 连接到 Neon 数据库。
  3. 从一个 products 表中查询所有数据。
  4. 将查询结果以 JSON 数组的形式返回。

准备Neon数据库

根据Netlify中云函数部署的 区域(一般都是us-east-2 (Ohio)),在Neon中创建一个处于同一区域的数据库。(不在同一个区域会导致云函数执行时间变长)

查看Netlify项目云函数部署区域的地址:https://app.netlify.com/projects/my-netlify-app/configuration/deploys#functions-region ,注意将 my-netlify-app 替换为自己项目的名字


接下来还需要在数据库中创建一些示例数据,以便我们的函数有内容可以查询。

  1. 登录到您的 Neon 控制台

  2. 选择您的项目,然后导航到 SQL Editor (SQL 编辑器)。

  3. 复制以下 SQL 代码,粘贴到编辑器中,然后点击 “Run” 执行。

    sql
    -- 创建一个名为 'products' 的表
    CREATE TABLE products (
        id SERIAL PRIMARY KEY,
        name VARCHAR(255) NOT NULL,
        price NUMERIC(10, 2) NOT NULL,
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    );
    
    -- 插入一些示例数据
    INSERT INTO products (name, price) VALUES
    ('Go Gopher Plush', 19.99),
    ('Netlify Sticker Pack', 4.99),
    ('Neon Serverless Mug', 12.50);

    执行成功后,您的数据库中就有了一个包含 3 条记录的 products 表。


在 Netlify 中设置环境变量

最关键的安全步骤: 绝不能将数据库连接密码硬编码在代码里,而是通过Netlify的环境变量来管理

  1. 在您的 Neon 项目控制台的 Dashboard 页面,找到 Connection Details (连接详情) 部分。
  2. 从连接信息中,复制 Postgres connection string (它看起来像 postgres://user:password@host/dbname)。
  3. 打开您的 Netlify 站点后台 (app.netlify.com)。
  4. 导航到 Projects > 指定的项目(如:my-netlify-go-app) > Project configuration > Environment variables
  5. 点击 “Add a variable”,选择 “Add a single variable”。
  6. 键 (Key) 输入:NEON_DATABASE_URL
  7. 值 (Value) 粘贴:您刚刚从 Neon 复制的完整连接字符串。
  8. 点击 “Create variable” 保存。

现在 Go 云函数在部署后就可以通过 os.Getenv("NEON_DATABASE_URL") 来安全地访问这个值了。


更新 Go 项目

  1. 安装 Go Postgres 驱动: 我们需要 pgx 库,它是目前最高性能、功能最丰富的 Go Postgres 驱动。在您的项目根目录 my-netlify-go-app/ 中运行:

    bash
    go get github.com/jackc/pgx/v5
  2. 创建新函数的目录

    bash
    mkdir -p netlify/functions/get-products
  3. 编写 Go 函数代码 (get-products/main.go): 在 netlify/functions/get-products/ 目录下创建 main.go 文件,并粘贴以下代码:

    go
    // netlify/functions/get-products/main.go
    package main
    
    import (
    	"context"
    	"encoding/json"
    	"fmt"
    	"os"
    
    	"github.com/aws/aws-lambda-go/events"
    	"github.com/aws/aws-lambda-go/lambda"
    	"github.com/jackc/pgx/v5"
    )
    
    // Product 结构体,用于映射数据库中的 products 表
    type Product struct {
    	ID    int     `json:"id"`
    	Name  string  `json:"name"`
    	Price float64 `json:"price"`
    }
    
    func handler(ctx context.Context) (*events.APIGatewayProxyResponse, error) {
    	// 1. 从环境变量中安全地读取数据库连接字符串
    	connString := os.Getenv("NEON_DATABASE_URL")
    	if connString == "" {
    		return &events.APIGatewayProxyResponse{StatusCode: 500, Body: "NEON_DATABASE_URL not set"}, nil
    	}
    
    	// 2. 连接到 Neon 数据库
    	conn, err := pgx.Connect(ctx, connString)
    	if err != nil {
    		return &events.APIGatewayProxyResponse{StatusCode: 500, Body: fmt.Sprintf("Unable to connect to database: %v", err)}, nil
    	}
    	defer conn.Close(ctx) // 确保函数结束时关闭连接
    
    	// 3. 执行 SQL 查询
    	rows, err := conn.Query(ctx, "SELECT id, name, price FROM products ORDER BY id;")
    	if err != nil {
    		return &events.APIGatewayProxyResponse{StatusCode: 500, Body: fmt.Sprintf("Query failed: %v", err)}, nil
    	}
    	defer rows.Close() // 确保处理完结果集后关闭它
    
    	// 4. 处理查询结果
    	var products []Product
    	for rows.Next() {
    		var p Product
    		err := rows.Scan(&p.ID, &p.Name, &p.Price)
    		if err != nil {
    			return &events.APIGatewayProxyResponse{StatusCode: 500, Body: fmt.Sprintf("Failed to scan row: %v", err)}, nil
    		}
    		products = append(products, p)
    	}
    
    	// 5. 将结果序列化为 JSON
    	responseBody, err := json.Marshal(products)
    	if err != nil {
    		return &events.APIGatewayProxyResponse{StatusCode: 500, Body: fmt.Sprintf("Failed to marshal response: %v", err)}, nil
    	}
    
    	// 6. 返回成功的响应
    	return &events.APIGatewayProxyResponse{
    		StatusCode: 200,
    		Headers:    map[string]string{"Content-Type": "application/json"},
    		Body:       string(responseBody),
    	}, nil
    }
    
    func main() {
    	lambda.Start(handler)
    }

更新netlify配置

之前的 netlify.toml 文件只构建了一个 hello 函数。现在我们需要一个能构建所有函数的通用命令。

打开 netlify.toml 文件,将其中的 command 修改为以下内容:

toml
# netlify.toml

[build]
  # 在项目根目录执行,为每个函数目录构建一个 main 可执行文件。
  command = "for dir in netlify/functions/*; do function_name=$(basename $dir); go build -o \"netlify/functions/${function_name}/main\" \"./netlify/functions/${function_name}/\"; done"

  functions = "netlify/functions/"

# ... 其他配置 ...

这个命令会自动找到 netlify/functions 下的所有子目录(helloget-products),并为它们分别执行 go build。这是一个可扩展的、一劳永逸的配置。


再次本地测试

为了在本地测试,netlify dev 命令需要能访问到数据库连接字符串。

  1. 在项目根目录 my-netlify-go-app/ 创建一个名为 .env 的文件。
  2. .env 文件中添加以下内容,将值替换为您自己的连接字符串
    text
    NEON_DATABASE_URL="postgres://user:password@host/dbname"
  3. 重要:将 .env 文件添加到 .gitignore 中,防止将密码提交到 Git 仓库!
    text
    # .gitignore
    .env
  4. 现在,运行本地开发服务器:
    bash
    netlify dev
  5. 访问新接口的本地 URL:http://localhost:8888/.netlify/functions/get-products
  6. 您应该能看到浏览器返回了您在数据库中创建的三个产品的 JSON 数据。

本地测试成功后,就可以部署上线了。


部署到线上

  1. 将所有更改提交到 Git:
    bash
    git add .
    git commit -m "feat: add get-products function connecting to Neon"
    git push
  2. Netlify 会自动检测到您的代码推送,并使用 netlify.toml 中新的构建命令来部署您的两个函数。
  3. 部署成功后,访问您的线上 URL https://your-site-name.netlify.app/.netlify/functions/get-products 进行最终验证。

保活Neon的定时函数

目标:创建一个名为 keep-alive 的 Netlify 云函数,它没有公开的 API 接口,而是由 Netlify 的服务器根据预设的时间表自动在后台触发。它的唯一目的就是定期向 Neon 数据库发送一个极轻量的查询,防止数据库因闲置而进入休眠状态。


创建新函数的目录

my-netlify-go-app 项目中,为新函数创建一个目录。

  1. 打开终端,进入项目根目录。
  2. 执行命令:
    bash
    mkdir -p netlify/functions/keep-alive

项目结构现在看起来像这样:

text
/my-netlify-go-app
|-- ... (go.mod, netlify.toml, etc.)
|-- netlify/
    |-- functions/
        |-- hello/
        |-- get-products/
        |-- keep-alive/  <-- 新创建的目录

编写保活函数代码

这个函数的代码与 get-products 非常相似,但更简单,因为它不需要处理 HTTP 请求和响应。

  1. netlify/functions/keep-alive/ 目录下创建一个 main.go 文件。

  2. 将以下代码复制并粘贴到 main.go 文件中:

    go
    // netlify/functions/keep-alive/main.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    	"os"
    
    	"github.com/aws/aws-lambda-go/lambda"
    	"github.com/jackc/pgx/v5"
    )
    
    // handleRequest是函数的核心逻辑。
    // 注意:它的签名比API函数更简单,因为它不处理HTTP事件。
    func handleRequest(ctx context.Context) error {
    	log.Println("Running keep-alive function to ping Neon database...")
    
    	// 从环境变量安全地读取数据库连接字符串
    	connString := os.Getenv("NEON_DATABASE_URL")
    	if connString == "" {
    		log.Println("Error: NEON_DATABASE_URL environment variable not set.")
    		return fmt.Errorf("NEON_DATABASE_URL not set")
    	}
    
    	// 连接数据库
    	conn, err := pgx.Connect(ctx, connString)
    	if err != nil {
    		log.Printf("Error: Unable to connect to database: %v\n", err)
    		return err
    	}
    	defer conn.Close(ctx)
    
    	// 执行一个最轻量的查询,目的仅仅是“触摸”一下数据库
    	var result int
    	err = conn.QueryRow(ctx, "SELECT 1;").Scan(&result)
    	if err != nil {
    		log.Printf("Error: Keep-alive query failed: %v\n", err)
    		return err
    	}
    
    	// 成功的日志对于验证至关重要
    	log.Printf("Keep-alive ping to Neon successful. Query result: %d", result)
    	return nil
    }
    
    func main() {
    	// 启动 Lambda 处理程序
    	lambda.Start(handleRequest)
    }

代码要点

  • 日志 (Logging):我们使用了 log 包来打印信息。因为这个函数没有 API 响应,查看日志是验证它是否成功运行的唯一方式
  • 轻量查询SELECT 1; 是最理想的保活查询。它几乎不消耗任何数据库资源,但足以让数据库认为自己是“活跃”的。
  • 简单的 Handler:函数签名是 func(context.Context) error,因为它由调度器触发,而不是 HTTP 请求。

配置定时调度

这是将普通函数变为“定时函数”的关键一步。

  1. 打开项目根目录下的 netlify.toml 文件。

  2. 不需要修改 [build] 部分的 command,之前设置的 for 循环命令会自动构建这个新的 keep-alive 函数。

  3. 在文件的末尾,添加以下新的配置块:

    toml
    # netlify.toml
    
    # 现有的 [build] 配置 ...
    
    [functions."keep-alive"]
    # 调度计划:
    # - Cron 表达式: "*/5 1-15 * * *"
    # - UTC 时间: 每天的 01:00 到 15:59 之间,每5分钟执行一次。
    # - 对应东八区时间 (UTC+8): 每天的 09:00 到 23:59 之间,每5分钟执行一次。
    schedule = "*/5 1-15 * * *"

配置解释

  • [functions."keep-alive"]: 这个特殊的 TOML 语法表示我们正在为名为 keep-alive 的函数进行特定配置。
  • Netlify 的定时函数调度器(以及几乎所有的国际通用 Cron 系统)都使用 UTC (协调世界时) 来解析 Cron 表达式
    • 需要的时间 (东八区, UTC+8): 每天的 09:00 到 23:59 转换到 UTC 时间:
      • 开始时间:东八区 09:00 = 09:00 - 8小时 = UTC 01:00
      • 结束时间:东八区 23:59 = 23:59 - 8小时 = UTC 15:59
    • 得到的 Cron 表达式是: */5 1-15 * * *

部署与验证

  1. 部署: 将所有更改提交到您的 Git 仓库:

    bash
    git add .
    git commit -m "feat: add keep-alive scheduled function for Neon"
    git push

    Netlify 将自动开始部署。

  2. 验证 (最重要的一步): 由于这个函数没有 URL 可以访问,我们需要通过查看日志来验证它是否在按计划运行。

    • 部署成功后,等待几分钟(最多等 4-5 分钟,让第一个调度周期命中)。
    • 登录到您的 Netlify 站点后台。
    • 点击顶部的 Functions 标签页。
    • 在函数列表中,点击您新创建的 keep-alive 函数。
    • 在函数详情页面,查看下方的 Function log (函数日志) 部分。

    您应该能看到类似下面的日志条目,并且每隔大约 4 分钟就会出现一条新的

    text
    ... INFO: Running keep-alive function to ping Neon database...
    ... INFO: Keep-alive ping to Neon successful. Query result: 1

一旦这个 keep-alive 定时函数开始稳定运行,就可以避免 Neon 数据库5分钟不活动后进入休眠状态