Skip to content

Document

谷溪 edited this page Jun 1, 2020 · 5 revisions

Go项目管理

使用两种语言来编写一个项目具有天生的复杂性。为了易于使用,GoTask项目的目标之一就是极力降低引入Go语言的成本。

构建

建议将Go的构建方式打包到Hyperf或其他Swoole框架的启动命令中。这样便不会出现Go语言和PHP不同步的现象。缺点是启动会变慢一些。 为了方便进行Demo演示,也可以在PHP脚本开始处利用PHP的exec函数进行打包。

对于Hyperf用户,GoTask已经集成了自动构建Go项目的选项。

//config/autoload/gotask.php
    'go_build' => [
        'enable' => false,
        'workdir' => BASE_PATH . '/gotask',
        'command' => 'go build -o ../bin/app cmd/app.go',
    ],

在将enable设为true后启动项目时将会在默认路径进行自动打包。

不建议生产环境使用该选项。

运行

通过Swoole进程管理Go边车存在几个好处,首先,Go项目的生命周期将完全由Swoole接管。Swoole启动Go启动,Swoole停则Go停。当Go崩溃时,Swoole会将Go拉起。其次,Go将继承Swoole的环境变量,这使得我们可以共享配置信息。

在非Hyperf项目中,可以手动启动Go边车或使用其他框架推荐的方式封装。

$process = new Process(function (Process $process) {
    $process->exec(__DIR__ . '/app', ['-address', ADDR]);
});
$process->start();

在Hyperf的项目中,只要配置gotask.enable处于开启状态,边车就是自动启停的。在0.2.0版本以后,Go边车的stdout也默认重定向到PHP侧,由PHP统一输出,这使得Go边车可以复用PHP的日志配置,比如通过monolog对日志进行分页等。

相关配置:

//config/autoload/gotask.php
    'go_log' => [
        'redirect' => true,
        'level' => 'info',
    ],

PHP向Go投递

原理

PHP将数据用json序列化后传递给Go。Go将json反序列化成指定方法中约束的类型。Go运算完成后,通过同样的方式将结果序列化投递给PHP,PHP反序列化拿到数组。

通过添加PAYLOAD_RAW flag,也可以不序列化,直接投递字节。

创建一个GoTask

PHP侧一共有四种GoTask实现,具有共同的interface,可以根据情况选择。

interface GoTask
{
    function call($method, $payload = null, $flag = 0);
}
限单协程使用 跨协程使用
通过Socket投递 SocketIPCSender SocketGoTask
通过标准输入输出投递 PipeIPCSender PipeGoTask

在Hyperf中,通过容器注入GoTask,注入的实例是SocketGoTask,当做单例使用即可。

$socketIPCSender = new SocketIPCSender('127.0.0.1:6001');
$pipeIPCSender = new PipeIPCSender($process); //Swoole\Process
$socketGoTask = new SocketGoTask($connectionPool); //Hyperf\GoTask\GoTaskConnectionPool
$pipeGoTask = new PipeGoTask($process); //Swoole\Process
/** @var SocketGoTask $gotask (Hyperf only) */
$goTask = ApplicationContext::getContainer()->get(GoTask::class);

go侧需要至少一个net/rpc风格的Struct作为服务容器,

type T struct {}
// net/rpc structs must have method like this:
func (t *T) MethodName(argType T1, replyType *T2) error

调用gotask.Register(new(T))将其注册,然后执行gotast.Run()启动服务。

// Register a net/rpc compatible service
func Register(receiver interface{}) error
// Run the sidecar, receive any fatal errors.
func Run() error

文档: https://pkg.go.dev/github.com/hyperf/gotask/v2/pkg/gotask?tab=doc

函数定义

interface GoTask
{
    function call($method, $payload = null, $flag = 0);
}
  • $method: IPC函数名,形如'Struct.Func'。
  • $payload: 需要传递的信息。任何可以JSON序列化的值均可。
  • $flag: IPC协议的Flag。
    • Hyperf\GoTask\GoTask::PAYLOAD_RAW:不自动序列化payload,作为[]bytes传递给Go
func (app *App) HelloString(payload interface{}, result *interface{}) err error
  • app: go注册的服务struct
  • payload:接收的信息。可以约束类型。
  • result:返回的信息。
  • err: 如果不为nil,则会转化为PHP侧的异常抛出
  • 服务interface与net/rpc兼容。

结构化投递示例

$task->call('App.HelloStruct', [
        'firstName' => 'LeBron',
        'lastName' => 'James',
        'id' => 23,
    ]));
type Name struct {
	Id        int    `json:"id"`
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
}

func (a *App) HelloStruct(name Name, r *interface{}) error {
	*r = map[string]Name{
		"hello": name,
	}
	return nil
}

二进制投递示例

$task->call('App.HelloBytes', base64_encode('My Bytes'), GoTask::PAYLOAD_RAW);
func (a *App) HelloBytes(name []byte, r *[]byte) error {
	reader := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(name))
	*r, _ = ioutil.ReadAll(reader)
	return nil
}

代码生成

PHP通过call($method, $payload = null, $flag = 0)方法调用Go服务时,由于$method只是一个字符串,这意味着无法提供IDE提示和静态分析。我们可以对PHP类进行进一步的封装,比如:

<?php

declare(strict_types=1);

namespace App\GoTask;

use Hyperf\GoTask\GoTask;
use Hyperf\GoTask\GoTaskProxy;

class Worker extends GoTaskProxy
{
    /**
     * @param string $payload
     * @return bool
     */
    public function test(string $payload) : bool
    {
        return parent::call("Worker.Test", $payload, 0);
    }

    /**
     * @param  $payload
     * @return mixed
     */
    public function debug($payload)
    {
        return parent::call("Worker.Debug", $payload, GoTask::PAYLOAD_RAW);
    }
}`

1.0.0后,支持通过go语言反射自动生成上述PHP代码封装。

执行

./bin/app -reflect

即可在标准输出生成GoTask对应的PHP代码。

如需直接生成到代码目录,可以在Go服务上加PHPPath和PHPNamespace两个公开变量。

type App struct{
  PHPPath string
  PHPNamespace string
}

命令行环境

v2.1.0 起,Hyperf用户如果需要在命令行中向Go进程投递,只需实现WithGoTask接口。

class MyCommand extends HyperfCommand implements WithGoTask {}

Go向PHP投递

在某些时候,Go可能也需要调用PHP的函数。在0.2.0版本以后加入了go2php的支持。

非Hyperf项目中,请在Go的启动参数中传入go2php的socket地址。

$process = new Process(function (Process $process) {
    sleep(1);
    $process->exec(__DIR__ . '/app', ['-go2php-address', ADDR]);
}, false, 0, true);
$process->start();

run(function () {
    $server = new SocketIPCReceiver(ADDR);
    $server->start();
});

在Hyperf项目中,只要配置一下就可以了。

    'go2php' => [
        'enable' => false,
        'address' => \Hyperf\GoTask\ConfigProvider::address(),
    ],

在Go边车中,可以通过FQCN调用PHP对象的方法。

	client, err := gotask.NewAutoClient()
	if err != nil {
		log.Fatalln(err)
	}
	defer client.Close()

	var res []byte
	err = client.Call("Namespace\\Example::HelloString", "Hyperf", &res)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println(string(res))

这里因为PHP的socket address已经通过启动参数传入,所以NewAutoClient()就不用再传地址了。如果不知道地址,可以使用NewClient(conn net.Conn)来创建客户端。

<?php
namespace Namespace;

class Example
{
    public function HelloString(string $payload)
    {
        return "Hello, {$payload}!";
    }
}

在Hyperf框架中,该对象从容器中获取的且为长生命周期,可以使用依赖注入。

go2php使用的协议与php2go完全一致,不再赘述。

日志

在 0.3.0 后go语言新增"github.com/hyperf/gotask/v2/pkg/log"包。Hyperf下开启gotask.go2php.enable后可用。

log模块与默认的日志stdout重定向不同,是通过socket将日志投递给PHP。遵循PSR-3规范,功能更为丰富。可以理解为Hyperf日志组件的Go实现。

文档:https://pkg.go.dev/github.com/hyperf/gotask/v2/pkg/log?tab=doc

配置

在 0.3.0 后go语言新增"github.com/hyperf/gotask/v2/pkg/config"包。Hyperf下开启gotask.go2php.enable后可用。

用于从Go边车获取PHP的Config配置。可以理解为Hyperf配置组件的Go实现。

文档:https://pkg.go.dev/github.com/hyperf/gotask/v2/pkg/config?tab=doc

mongo_client

在 2.1.0 后新增"github.com/hyperf/gotask/v2/pkg/mongo_client"包。通过该模块封装Mongo查询,使得PHP侧获得如下API:

<?php
namespace App\Controller;

use Hyperf\GoTask\MongoClient\MongoClient;

class IndexController
{
    public function index(MongoClient $client)
    {
        $col = $client->my_database->my_col;
        $col->insertOne(['gender' => 'male', 'age' => 18]);
        $col->insertMany([['gender' => 'male', 'age' => 20], ['gender' => 'female', 'age' => 18]]);
        $col->countDocuments();
        $col->findOne(['gender' => 'male']);
        $col->find(['gender' => 'male'], ['skip' => 1, 'limit' => 1]);
        $col->updateOne(['gender' => 'male'], ['$inc' => ['age' => 1]]);
        $col->updateMany(['gender' => 'male'], ['$inc' => ['age' => 1]]);
        $col->replaceOne(['gender' => 'female'], ['gender' => 'female', 'age' => 15]);
        $col->aggregate([
              ['$match' => ['gender' => 'male']],
              ['$group' => ['_id' => '$gender', 'total' => ['$sum' => '$age']]],
        ]);
        $col->deleteOne(['gender' => 'male']);
        $col->deleteMany(['age' => 15]);
        $col->drop();
        // if there is a command not yet supported, use runCommand or runCommandCursor.
        $client->my_database->runCommand(['ping' => 1]);
        return $client->my_database->runCommandCursor(['listCollections' => 1]); 
    }
}

文档:https://pkg.go.dev/github.com/hyperf/gotask/v2/pkg/mongo_client?tab=doc