编写 Cloud Functions 函数

Cloud Functions 函数可用 Node.jsPythonGoJava.NETRubyPHP 编程语言编写,并可在各种语言版本的运行时中执行。Cloud Functions 函数执行环境因您选择的运行时而异。如需进一步了解每种运行时环境的详情,请参阅运行时概览页面:

自行试用

如果您是 Google Cloud 新手,请创建一个帐号来评估 Cloud Functions 在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。

免费试用 Cloud Functions

Cloud Functions 函数的类型

Cloud Functions 函数有两种不同类型:HTTP 函数和事件驱动的函数。事件驱动的函数可以是后台函数CloudEvent 函数,具体取决于这些函数是针对哪种 Cloud Functions 运行时编写的。

HTTP 函数

您可以通过标准 HTTP 请求调用 HTTP 函数。这些 HTTP 请求会等待响应并支持处理常见 HTTP 请求方法(如 GET、PUT、POST、DELETE 和 OPTIONS)。当您使用 Cloud Functions 函数时,系统会自动为您预配 TLS 证书,因此您可以通过安全连接调用所有 HTTP 函数。

如需了解详情,请参阅编写 HTTP 函数

示例

Node.js

const escapeHtml = require('escape-html');

/**
 * HTTP Cloud Function.
 *
 * @param {Object} req Cloud Function request context.
 *                     More info: https://expressjs.com/en/api.html#req
 * @param {Object} res Cloud Function response context.
 *                     More info: https://expressjs.com/en/api.html#res
 */
exports.helloHttp = (req, res) => {
  res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`);
};

Python

from flask import escape

def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <https://flask.palletsprojects.com/en/1.1.x/api/#incoming-request-data>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response>.
    """
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'World'
    return 'Hello {}!'.format(escape(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"encoding/json"
	"fmt"
	"html"
	"net/http"
)

// HelloHTTP is an HTTP Cloud Function with a request parameter.
func HelloHTTP(w http.ResponseWriter, r *http.Request) {
	var d struct {
		Name string `json:"name"`
	}
	if err := json.NewDecoder(r.Body).Decode(&d); err != nil {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	if d.Name == "" {
		fmt.Fprint(w, "Hello, World!")
		return
	}
	fmt.Fprintf(w, "Hello, %s!", html.EscapeString(d.Name))
}

Java


import com.google.cloud.functions.HttpFunction;
import com.google.cloud.functions.HttpRequest;
import com.google.cloud.functions.HttpResponse;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Logger;

public class HelloHttp implements HttpFunction {
  private static final Logger logger = Logger.getLogger(HelloHttp.class.getName());

  private static final Gson gson = new Gson();

  @Override
  public void service(HttpRequest request, HttpResponse response)
      throws IOException {
    // Check URL parameters for "name" field
    // "world" is the default value
    String name = request.getFirstQueryParameter("name").orElse("world");

    // Parse JSON request and check for "name" field
    try {
      JsonElement requestParsed = gson.fromJson(request.getReader(), JsonElement.class);
      JsonObject requestJson = null;

      if (requestParsed != null && requestParsed.isJsonObject()) {
        requestJson = requestParsed.getAsJsonObject();
      }

      if (requestJson != null && requestJson.has("name")) {
        name = requestJson.get("name").getAsString();
      }
    } catch (JsonParseException e) {
      logger.severe("Error parsing JSON: " + e.getMessage());
    }

    var writer = new PrintWriter(response.getWriter());
    writer.printf("Hello %s!", name);
  }
}

C#

using Google.Cloud.Functions.Framework;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

namespace HelloHttp
{
    public class Function : IHttpFunction
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public async Task HandleAsync(HttpContext context)
        {
            HttpRequest request = context.Request;
            // Check URL parameters for "name" field
            // "world" is the default value
            string name = ((string) request.Query["name"]) ?? "world";

            // If there's a body, parse it as JSON and check for "name" field.
            using TextReader reader = new StreamReader(request.Body);
            string text = await reader.ReadToEndAsync();
            if (text.Length > 0)
            {
                try
                {
                    JsonElement json = JsonSerializer.Deserialize<JsonElement>(text);
                    if (json.TryGetProperty("name", out JsonElement nameElement) &&
                        nameElement.ValueKind == JsonValueKind.String)
                    {
                        name = nameElement.GetString();
                    }
                }
                catch (JsonException parseException)
                {
                    _logger.LogError(parseException, "Error parsing JSON request");
                }
            }

            await context.Response.WriteAsync($"Hello {name}!");
        }
    }
}

Ruby

require "functions_framework"
require "cgi"
require "json"

FunctionsFramework.http "hello_http" do |request|
  # The request parameter is a Rack::Request object.
  # See https://www.rubydoc.info/gems/rack/Rack/Request
  name = request.params["name"] ||
         (JSON.parse(request.body.read)["name"] rescue nil) ||
         "World"
  # Return the response body as a string.
  # You can also return a Rack::Response object, a Rack response array, or
  # a hash which will be JSON-encoded into a response.
  "Hello #{CGI.escape_html name}!"
end

PHP

<?php

use Google\CloudFunctions\FunctionsFramework;
use Psr\Http\Message\ServerRequestInterface;

// Register the function with Functions Framework.
// This enables omitting the `FUNCTIONS_SIGNATURE_TYPE=http` environment
// variable when deploying. The `FUNCTION_TARGET` environment variable should
// match the first parameter.
FunctionsFramework::http('helloHttp', 'helloHttp');

function helloHttp(ServerRequestInterface $request): string
{
    $name = 'World';
    $body = $request->getBody()->getContents();
    if (!empty($body)) {
        $json = json_decode($body, true);
        if (json_last_error() != JSON_ERROR_NONE) {
            throw new RuntimeException(sprintf(
                'Could not parse body: %s',
                json_last_error_msg()
            ));
        }
        $name = $json['name'] ?? $name;
    }
    $queryString = $request->getQueryParams();
    $name = $queryString['name'] ?? $name;

    return sprintf('Hello, %s!', htmlspecialchars($name));
}

事件驱动的函数

Cloud Functions 使用事件驱动的函数来处理来自 Cloud 基础架构的事件,例如 Pub/Sub 主题中的消息或 Cloud Storage 存储分区中的变化。

Cloud Functions 支持事件驱动函数的两种子类型:

如下所述,您使用的子类型将受到函数所针对的运行时的约束。

后台函数

Node.jsPythonGoJava Cloud Functions 运行时编写的事件驱动函数称为后台函数。如需了解详情,请参阅编写后台函数

示例

Node.js

/**
 * Background Cloud Function to be triggered by Pub/Sub.
 * This function is exported by index.js, and executed when
 * the trigger topic receives a message.
 *
 * @param {object} message The Pub/Sub message.
 * @param {object} context The event metadata.
 */
exports.helloPubSub = (message, context) => {
  const name = message.data
    ? Buffer.from(message.data, 'base64').toString()
    : 'World';

  console.log(`Hello, ${name}!`);
};

Python

def hello_pubsub(event, context):
    """Background Cloud Function to be triggered by Pub/Sub.
    Args:
         event (dict):  The dictionary with data specific to this type of
                        event. The `@type` field maps to
                         `type.googleapis.com/google.pubsub.v1.PubsubMessage`.
                        The `data` field maps to the PubsubMessage data
                        in a base64-encoded string. The `attributes` field maps
                        to the PubsubMessage attributes if any is present.
         context (google.cloud.functions.Context): Metadata of triggering event
                        including `event_id` which maps to the PubsubMessage
                        messageId, `timestamp` which maps to the PubsubMessage
                        publishTime, `event_type` which maps to
                        `google.pubsub.topic.publish`, and `resource` which is
                        a dictionary that describes the service API endpoint
                        pubsub.googleapis.com, the triggering topic's name, and
                        the triggering event type
                        `type.googleapis.com/google.pubsub.v1.PubsubMessage`.
    Returns:
        None. The output is written to Cloud Logging.
    """
    import base64

    print("""This Function was triggered by messageId {} published at {} to {}
    """.format(context.event_id, context.timestamp, context.resource["name"]))

    if 'data' in event:
        name = base64.b64decode(event['data']).decode('utf-8')
    else:
        name = 'World'
    print('Hello {}!'.format(name))

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"context"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
// See the documentation for more details:
// https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data) // Automatically decoded from base64.
	if name == "" {
		name = "World"
	}
	log.Printf("Hello, %s!", name)
	return nil
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import com.google.events.cloud.pubsub.v1.Message;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;

public class HelloPubSub implements BackgroundFunction<Message> {
  private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());

  @Override
  public void accept(Message message, Context context) {
    String name = "world";
    if (message != null && message.getData() != null) {
      name = new String(
          Base64.getDecoder().decode(message.getData().getBytes(StandardCharsets.UTF_8)),
          StandardCharsets.UTF_8);
    }
    logger.info(String.format("Hello %s!", name));
    return;
  }
}

CloudEvent 函数

.NETRubyPHP 运行时编写的事件驱动函数称为 CloudEvent 函数。如需了解详情,请参阅编写 CloudEvent 函数

示例

C#

using CloudNative.CloudEvents;
using Google.Cloud.Functions.Framework;
using Google.Events.Protobuf.Cloud.PubSub.V1;
using Microsoft.Extensions.Logging;
using System.Threading;
using System.Threading.Tasks;

namespace HelloPubSub
{
    public class Function : ICloudEventFunction<MessagePublishedData>
    {
        private readonly ILogger _logger;

        public Function(ILogger<Function> logger) =>
            _logger = logger;

        public Task HandleAsync(CloudEvent cloudEvent, MessagePublishedData data, CancellationToken cancellationToken)
        {
            string nameFromMessage = data.Message?.TextData;
            string name = string.IsNullOrEmpty(nameFromMessage) ? "world" : nameFromMessage;
            _logger.LogInformation("Hello {name}", name);
            return Task.CompletedTask;
        }
    }
}

Ruby

require "functions_framework"
require "base64"

FunctionsFramework.cloud_event "hello_pubsub" do |event|
  # The event parameter is a CloudEvents::Event::V1 object.
  # See https://cloudevents.github.io/sdk-ruby/latest/CloudEvents/Event/V1.html
  name = Base64.decode64 event.data["message"]["data"] rescue "World"

  # A cloud_event function does not return a response, but you can log messages
  # or cause side effects such as sending additional events.
  logger.info "Hello, #{name}!"
end

PHP


use CloudEvents\V1\CloudEventInterface;
use Google\CloudFunctions\FunctionsFramework;

// Register the function with Functions Framework.
// This enables omitting the `FUNCTIONS_SIGNATURE_TYPE=cloudevent` environment
// variable when deploying. The `FUNCTION_TARGET` environment variable should
// match the first parameter.
FunctionsFramework::cloudEvent('helloworldPubsub', 'helloworldPubsub');

function helloworldPubsub(CloudEventInterface $event): void
{
    $log = fopen(getenv('LOGGER_OUTPUT') ?: 'php://stderr', 'wb');

    $cloudEventData = $event->getData();
    $pubSubData = base64_decode($cloudEventData['message']['data']);

    $name = $pubSubData ? htmlspecialchars($pubSubData) : 'World';
    fwrite($log, "Hello, $name!" . PHP_EOL);
}

构建源代码

为了让 Cloud Functions 能找到您的函数定义,每个运行时都对您的源代码有一定的结构要求。一般说来,我们建议将大型的多函数代码库拆分为较小的单函数代码库,以降低代码复杂度和单函数依赖项数量。

Node.js

对于 Node.js 运行时,您必须从 Cloud Functions 使用 require() 调用加载的 Node.js 模块中导出函数的源代码。Cloud Functions 会使用 package.json 文件中的 main 字段来确定要加载的模块。如果未指定 main 字段,则 Cloud Functions 将从 index.js 加载代码。

例如,以下源代码配置有效:

  • 位于函数根目录中的一个 index.js,用于导出一个或多个函数:

    .
    └── index.js
    
  • 使用 index.js 文件从 foo.js 文件导入代码,然后导出一个或多个函数:

    .
    ├── index.js
    └── foo.js
    
  • 使用 app.js 文件导出一个或多个函数,并在 package.json 文件中添加 "main": "app.js"

    .
    ├── app.js
    └── package.json
    

Python

对于 Python 运行时,您必须在函数根目录中名为 main.py 的 Python 源文件中定义函数的入口点。

例如,以下源代码配置有效:

  • 函数根目录中的一个 main.py 文件,用于定义一个或多个函数:

    .
    └── main.py
    
  • 使用 main.py 文件和 requirements.txt 文件指定依赖项:

    .
    ├── main.py
    └── requirements.txt
    
  • 使用 main.py 文件从本地依赖项导入代码:

    .
    ├── main.py
    └── mylocalpackage/
        ├── __init__.py
        └── myscript.py
    

Go

对于 Go 运行时,您的函数必须位于项目根目录下的 Go 软件包中。您的函数不能位于 package main 中。仅在使用 Go 模块时才支持子软件包。

例如,以下源代码配置有效:

  • 使用项目根目录下的软件包导出一个或多个函数:

    .
    └── function.go
    
  • 使用项目根目录下的软件包从子软件包导入代码并导出一个或多个函数:

    .
    ├── function.go
    ├── go.mod
    └── shared/
        └── shared.go
    
  • 使用项目根目录下包含子目录的软件包来定义 package main

    .
    ├── cmd/
    |   └── main.go
    └── function.go
    

Java

对于 Java 运行时,您应该创建一个包含 src/main/java/ 子目录和 pom.xml 文件的顶级函数目录。我们建议将测试放入 src/test/java/ 子目录中。

.
├── pom.xml
└── src/
    ├── main/
    |   └── java/
    |       └── MyFunction.java
    └── test
        └── java/
            └── MyFunctionTest.java

如果您的 .java 文件声明了一个软件包(例如 functions),则目录层次结构将如下所示:

.
├── pom.xml
└── src/
    ├── main/
    |   └── java/
    |       └── functions/
    |               └── MyFunction.java
    └── test/
        └── java/
                └── functions/
                    └── MyFunctionTest.java

如果您的函数是在特定软件包中进行定义(就像大多数 Java 函数一样),则必须在部署时指定为 --entry-point 值的一部分。

对多个函数进行分组

如果您打算将多个函数分组到单个项目中,请注意每个函数最终可能共享同一组依赖项。但是,某些函数可能并不需要所有共享依赖项。

我们建议您将每个函数放入各自的顶级目录中(如上所示),其中包含各自的 src/main/java 子目录和 pom.xml 文件。此方法可最大限度地减少特定函数所需的依赖项数量,进而减少函数所需的内存量。

此外,通过 Functions 框架在本地运行函数时,单独的函数可让您更轻松地指定单个函数。这对于本地开发和测试很有帮助。

C#

对于 .NET 运行时,您可以像构建任何其他 .NET 源代码一样构建您的项目。

使用模板创建 C# 函数时,它会在文件系统中的同一级别创建以下文件:

  • 名为 Function.cs 的函数源文件。
  • 扩展名为 .csproj 的项目文件。

例如:

.
├── Function.cs
└── HelloHttp.csproj

如果使用模板创建 F# 和 Visual Basic 函数,则适用相同的格式。对于 F#,文件名为 Function.fs,项目文件扩展名为 .fsproj。对于 Visual Basic,文件名为 CloudFunction.vb,项目文件扩展名为 .vbproj

不过,这些格式只是惯例,而非要求。您可以像构建其他常规 C#、F# 或 Visual Basic 项目一样构建代码,其中可包含多个源文件、资源等。

此外,如果每个项目仅实现一个函数,您可以在不指定函数名称的情况下开始托管该函数,这样可以简化本地开发和调试。

Ruby

对于 Ruby 运行时,您必须在函数根目录中名为 app.rb 的 Ruby 源文件中定义函数的入口点。此外,依赖项(至少包括 functions_framework gem)必须列在 Gemfile 中,并且关联的 Gemfile.lock 文件也位于函数的根目录中。您的函数可以在根目录或子目录中包含其他 Ruby 文件。

例如,以下源代码配置有效:

.
├── Gemfile
├── Gemfile.lock
└── app.rb

.
├── Gemfile
├── Gemfile.lock
├── app.rb
└── lib/
    └── my_class.rb

PHP

对于 PHP 运行时,您应该创建一个包含 index.php 文件的顶层函数目录,该文件包含您的函数实现。

composer.json 文件声明依赖项。当您按照指定 PHP 依赖项中的说明运行 composer require google/cloud-functions-framework 命令时,它会在您的函数代码目录中创建一个 vendor/ 目录,其中包含依赖项。composer.lock 文件会锁定函数的依赖项,这意味着您的函数会保留已安装依赖项版本的精确记录。如果您日后再次运行 composer install,它将使用 composer.lock 文件中指定的依赖项版本。

我们建议将测试放入 test/ 目录中。

.
├── index.php
├── composer.json
├── composer.lock
├── vendor/
└── test/

指定依赖项

您可以根据正在使用的运行时以惯用方式指定函数的依赖项。如需了解详情,请参阅相应页面:

命名 Cloud Functions 函数

Cloud Functions 函数有一个“name”属性,该属性在部署时进行设置;一旦设置,便无法更改。函数的名称用作其标识符,并且在区域内必须唯一。如需了解详情,请参阅部署文档

后续步骤

自行试用

如果您是 Google Cloud 新手,请创建一个帐号来评估 Cloud Functions 在实际场景中的表现。新客户还可获享 $300 赠金,用于运行、测试和部署工作负载。

免费试用 Cloud Functions