使用 Golang 和 Docker 实现 Python 计算服务

这篇具有很好参考价值的文章主要介绍了使用 Golang 和 Docker 实现 Python 计算服务。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

本篇文章,我们继续前一篇的话题《使用 Golang 和 Docker 运行 Python 代码》,聊聊如何使用 Golang 和 Docker 将普通的 Python 软件包封装为高性能的服务。

写在前面

在上一篇内容中,我们提到过 Python 在 Golang 中运行,存在一些使用场景限制。

如果我们在整个项目中直接引入这个方案,会让整个项目也受到相关的技术限制,影响开发和调试体验。

这个技术方案合适的实现场景,除了前文中直接封装为 Docker CLI 工具外,其实还有包装成独立可调用的网络服务。

本篇文章,我们就来聊聊这个思路的玩法,还是以前文中提到的 Python 项目 derek73/python-nameparser 为例。

本文中相关的代码,已经上传至 soulteary/go-nameparser,欢迎自取、“一键三连”,以及提交你的 PR。

程序实现的初步调整

在进行服务封装前,我们需要先针对昨天的程序进行基础的调整。

初步调整 Python 程序的实现

我们要封装的代码,依赖 Python 项目 python-nameparser。这个项目的使用方法,主要有两种:

  • 直接调用模块提供的 HumanName 来将我们想序列化的字符串进行改写,得到新的字符串。
  • 在调用 HumanName 之后,使用 .as_dict() 方法,将输出结果转换为字典类型,获取结构化信息。

如果使用代码来表示,和昨天文章中类似,会像下面这样:

from nameparser import HumanName

# 直接调用方法,获得序列化后的字符串
print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)"))

# 获取字符串的结构化数据
print(HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)").as_dict())

为了能够尽可能简单的实现跨程序数据交互,尽可能性价比高的完成内容逻辑处理。

我们可以将代码调整为类似下面这样,一次性获取两种不同场景的计算诉求,把两种计算结果组合在一起:

from nameparser import HumanName
import json

result = HumanName("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")

data = {
    "text": str(result),
    "detail": result.as_dict()
}

print(json.dumps(data))

上面的程序执行完毕,我们将得到类似下面的结果:

{"text": "Dr. Juan Q. Xavier de la Vega III (Doc Vega)", "detail": {"title": "Dr.", "first": "Juan", "middle": "Q. Xavier", "last": "de la Vega", "suffix": "III", "nickname": "Doc Vega"}}

在实际使用的时候,我们只需要将上面程序中的占位文本替换为实际要处理的文本,就能搞定核心的计算逻辑啦。

初步调整 Golang 程序的实现

我们结合上文中的 Python 程序,将前文中的 Golang 代码也进行适当调整:

package main

import (
	"fmt"

	python3 "github.com/go-python/cpy3"
)

func main() {
	defer python3.Py_Finalize()
	python3.Py_Initialize()

	name := "Dr. Juan Q. Xavier de la Vega III (Doc Vega)"

	code := fmt.Sprintf(`
from nameparser import HumanName
import json

result = HumanName("%s")

data = {
	"text": str(result),
	"detail": result.as_dict()
}

print(json.dumps(data))
`, name)
	python3.PyRun_SimpleString(code)
}

Golang 这样,在程序执行的时候,就能够通过调整 Go 程序中的name 变量,来让 Python 程序计算不同的内容啦。

但是,如果我们坚持使用这样的方式,我们还需要封装一些额外的功能,来捕获程序运行过程中的日志内容。

对于这个程序或许还是可行的,因为交互数据不复杂,出错可能性也低,但依旧存在容易将无关紧要的日志填充到我们想要得到的结果中的可能性。

并且,如果这样封装的程序,对于维护依赖的 Python 程序而言,体验也并不好。如果我们的 Python 程序相对复杂,或者想要直接操作 Python 中的复杂的数据对象的话。

封装和使用 Python 软件包

为了解决这些问题,我们需要对程序进行进一步的封装和调整。

封装 Python 软件包

为了程序的使用和后续 Python 代码的维护更简单,我们需要将项目使用的 Python 代码封装成一个简单的 Python 模块。

我们在目录中创建一个名为 convert 的文件夹,然后在里面创建一个名为 convert.py 的 Python 程序,程序内容如下:

from nameparser import HumanName
import json

def Convert(s):
    result = HumanName(s)
    data = {
        "text": str(result),
        "detail": result.as_dict()
    }
    return json.dumps(data)

这样,我们就有了一个最简单的,能够使用模块方式调用的 Python 模块,能够调用我们需要使用的 HumanName 函数,并将传入的字符串序列化为我们想要的结果。

这部分代码保存在 soulteary/go-nameparser/convert,可以自取。

使用 Golang 直接调用 Python 包里的函数

当我们完成了 Python 模块的功能封装之后,我们需要完成两个函数,来让 Golang 能够自由调用我们封装 Python 模块中的方法,来进行具体的逻辑计算。

我们先来完成在 Go 中加载 Python 软件包的功能,能够加载指定目录的 Python 软件包到 Golang 程序的内存中:

func LoadModule(dir string) *python3.PyObject {
	path := python3.PyImport_ImportModule("sys").GetAttrString("path")
	python3.PyList_Insert(path, 0, python3.PyUnicode_FromString(dir))
	return python3.PyImport_ImportModule(filepath.Base(dir))
}

然后,来完成加载前文中,我们封装好的 Python “Convert” 模块和模块中的 Convert 函数功能:

func Convert(input string) string {
	module := LoadModule("./convert")
	function := module.GetAttrString("Convert")
	args := python3.PyTuple_New(1)
	python3.PyTuple_SetItem(args, 0, python3.PyUnicode_FromString(input))
	return python3.PyUnicode_AsUTF8(function.Call(args, python3.Py_None))
}

实际使用时,我们只要先初始化 Python 环境,然后调用 Go 中的 Convert 函数,就能够在 Go 中,调用 Python 模块进行计算啦:

package main

import (
	"fmt"
	"log"
	"path/filepath"

	python3 "github.com/datadog/go-python3"
)

func main() {
	defer python3.Py_Finalize()
	python3.Py_Initialize()
	if !python3.Py_IsInitialized() {
		log.Fatalln("Failed to initialize Python environment")
	}

	ret := Convert("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")
	fmt.Printf(ret)
}

程序运行完毕之后,日志结果也是符合预期的。

 {"text": "Dr. Juan Q. Xavier de la Vega III (Doc Vega)", "detail": {"title": "Dr.", "first": "Juan", "middle": "Q. Xavier", "last": "de la Vega", "suffix": "III", "nickname": "Doc Vega"}}

针对 Python 输出结果进行结构化解析

在上面的程序中,使用 Go 执行 Python 函数,输出了字符串。如果我们想在 Golang 使用结构化的方式来访问数据字段,还需要进行一个简单的数据解析动作。

先定义一个数据结构,然后调用 json.Unmarshal 处理字符串即可:

...

type HumanName struct {
	Text   string `json:"text"`
	Detail struct {
		Title    string `json:"title"`
		First    string `json:"first"`
		Middle   string `json:"middle"`
		Last     string `json:"last"`
		Suffix   string `json:"suffix"`
		Nickname string `json:"nickname"`
	} `json:"detail"`
}

func main() {
	defer python3.Py_Finalize()
	python3.Py_Initialize()
	if !python3.Py_IsInitialized() {
		log.Fatalln("Failed to initialize Python environment")
	}

	ret := Convert("Dr. Juan Q. Xavier de la Vega III (Doc Vega)")

	var name HumanName
	err := json.Unmarshal([]byte(ret), &name)
	if err != nil {
		fmt.Println("Parsing JSON failed:", err)
		return
	}

	fmt.Println("Name:", name.Text)
	fmt.Println("Detail:", name.Detail)
}

程序运行完毕,我们就能够得到解析结果啦。以及在 Golang 程序中随意的使用这个来自 Python 的数据:

Name: Dr. Juan Q. Xavier de la Vega III (Doc Vega)
Detail: {Dr. Juan Q. Xavier de la Vega III Doc Vega}

实现可访问的 API

当我们能够随意解析和使用来自 Python 程序的计算结果后,就可以进行 API 接口的封装啦。

实现 HTTP 接口

实现 HTTP 接口并不难,如果我们想实现一个能够接收 POST 请求,对请求参数中的 name 字段进行计算的函数,代码实现类似下面,不到 30 行:

func Parse(input string) (ret HumanName, err error) {
	var name HumanName
	err = json.Unmarshal([]byte(Convert(input)), &name)
	if err != nil {
		return ret, fmt.Errorf("Parsing JSON failed: %v", err)
	}
	return name, nil
}

type Data struct {
	Name string `json:"name"`
}

route.POST("/api/convert", func(c *gin.Context) {
	var data Data
	if err := c.ShouldBindJSON(&data); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	result, err := Parse(data.Name)
	if err != nil {
		c.JSON(http.StatusInternalServerError, err)
		return
	}
	c.JSON(http.StatusOK, result)
})

在上面的代码中,我们封装了一个新的函数,能够将 Python 返回的内容序列化为对象,方便其他逻辑调用,比如本例中的 Gin 的接口返回时使用。

当然,如果我们 Python 结果能够确保是稳定的 JSON 结构输出,并且我们的 Go 程序不需要针对 Python 计算结果进行调整,那么我们也可以直接透传结果,不需要做数据的解析处理。

当我们完成最终服务端代码后,可以使用 curl 来验证接口:

curl --request POST 'http://127.0.0.1:8080/api/convert' --header 'Content-Type: application/json' --data-raw '{"name": "Dr. Juan Q. Xavier de la Vega III (Doc Vega)"}'

接口返回结果,自然也是符合预期的:

{"text":"Dr. Juan Q. Xavier de la Vega III (Doc Vega)","detail":{"title":"Dr.","first":"Juan","middle":"Q. Xavier","last":"de la Vega","suffix":"III","nickname":"Doc Vega"}}

封装 GRPC 接口

关于 GRPC 的快速上手攻略,官方已经些的很好了,这里就不过多赘述了。唯一需要注意的是你使用的工具版本和程序中的 GRPC 版本是否一致。在折腾 GRPC 之前,我们需要先全局安装两个工具(使用文档中的版本):

go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

完成准备工作后,我们根据官方建议来快速集成和实现下程序的 GRPC 接口。

先定义一个名为 message.proto 的 protobuf 文件,在里面包含我们要启动一个名为 Converter 的服务,服务公开暴露一个名为 HumanName 的方法,以及这个方法的入参和出参:

syntax = "proto3";

option go_package = "./pkg/pb";

package pb;

// The Converter service definition.
service Converter {
  // Sends a Converter request
  rpc HumanName (ConvertRequest) returns (ConvertReply) {}
}

// The request message containing the name.
message ConvertRequest {
  string name = 1;
}

// The response message containing the name parsed
message ConvertReply {
  string message = 1;
}

保存完毕文件之后,我们执行命令:

protoc --go_out=. --go-grpc_out=. *.proto

所需要的 GRPC 相关的程序就会自动生成完毕,并保存在上面配置指定的目录中:option go_package = "./pkg/pb";

最后,我们再用 30 行左右代码,实现调用上面生成好的 GRPC 接口的 GRPC 服务即可:

package rpc

import (
	"context"
	"log"
	"net"

	"github.com/soulteary/go-nameparser/internal/bridge"
	"github.com/soulteary/go-nameparser/internal/define"
	pb "github.com/soulteary/go-nameparser/pkg/pb"
	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedConverterServer
}

func (s *server) HumanName(ctx context.Context, in *pb.ConvertRequest) (*pb.ConvertReply, error) {
	return &pb.ConvertReply{Message: bridge.Convert(in.GetName())}, nil
}

func Launch() {
	lis, err := net.Listen("tcp", define.GRPC_PORT)
	if err != nil {
		log.Fatalf("GRPC server failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterConverterServer(s, &server{})
	log.Printf("GRPC server listening at %v", lis.Addr())

	if err := s.Serve(lis); err != nil {
		log.Fatalf("GRPC server failed to serve: %v", err)
	}
}

当我们完成了最终的 GRPC 服务程序之后,就能够对外提供 GRPC 服务啦。

至此,我们就完成了文章开头提到的内容,以及完成了我们封装的 Python 程序和调用程序的解耦。

通过 GRPC 方式调用服务

GRPC 服务的调用也很简单,我们只需要把上文中生成好的 “PB” 目录复制到我们的客户端程序目录中,然后使用下面的代码,即可调用上文中我们封装好的服务。GRPC 客户端的完整代码在这里。

package main

import (
	"context"
	"flag"
	"log"
	"time"

	pb "grpc-client/pb"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

const (
	TestName = "Dr. Juan Q. Xavier de la Vega III (Doc Vega)"
	GrpcAddr = "localhost:8081"
)

func main() {
	flag.Parse()
	// Set up a connection to the server.
	conn, err := grpc.Dial(GrpcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewConverterClient(conn)

	// Contact the server and print out its response.
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	r, err := c.HumanName(ctx, &pb.ConvertRequest{Name: TestName})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())
}

当服务端启动完毕后,我们使用客户端进行调用,就能够得到下面的复合预期的结果啦。

2023/05/22 22:56:58 Greeting: {"text": "Dr. Juan Q. Xavier de la Vega III (Doc Vega)", "detail": {"title": "Dr.", "first": "Juan", "middle": "Q. Xavier", "last": "de la Vega", "suffix": "III", "nickname": "Doc Vega"}}

改进 Docker 镜像

相比较前文,本篇文章中,我们的项目目录和依赖相对复杂。就不再相对适用在 Docker 中动态初始化项目依赖和进行依赖下载了,会浪费太多时间。

所以,我们可以调整实现,来加速镜像的构建:

# Base Images
FROM golang:1.20.4-alpine3.18 AS go-builder
FROM python:3.7-alpine3.18 AS builder
# Base Builder Env
COPY --from=go-builder /usr/local/go/ /usr/local/go/
ENV PATH="/usr/local/go/bin:${PATH}"
RUN python -m pip install --upgrade pip
RUN apk add build-base pkgconfig
ENV PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
ENV CGO_ENABLED=1
# Copy source code
# Install deps
RUN pip install nameparser
COPY ./go.* /app/
WORKDIR /app
RUN go mod tidy
COPY . ./
# Build the binary
RUN go build -o HumanName

# Run Image
FROM python:3.7-alpine3.18
# Copy Python Deps
COPY --from=builder /usr/local/lib/python3.7/site-packages/nameparser /usr/local/lib/python3.7/site-packages/nameparser
COPY --from=builder /app/convert /convert
# Copy binary
COPY --from=builder /app/HumanName /HumanName
CMD ["/HumanName"]

使用预构建容器镜像

针对本文中提到的服务,我已经构建了一个镜像,并推送到了 DockerHub 中。如果你感兴趣,可以执行下面的命令,启动它,以及进行一些基础的性能测试 😄

docker run --rm -it -p 8080:8080 -p 8081:8081 soulteary/go-nameparser

最后

好了,这篇文章就先聊到这里啦。

有机会我们展开聊聊另外两种更强力的方案 😄

–EOF


我们有一个小小的折腾群,里面聚集了一些喜欢折腾的小伙伴。

在不发广告的情况下,我们在里面会一起聊聊软硬件、HomeLab、编程上的一些问题,也会在群里不定期的分享一些技术资料。

喜欢折腾的小伙伴,欢迎阅读下面的内容,扫码添加好友。

关于“交友”的一些建议和看法

添加好友时,请备注实名和公司或学校、注明来源和目的,否则不会通过审核。

关于折腾群入群的那些事


本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 署名 4.0 国际 (CC BY 4.0)

本文作者: 苏洋

创建时间: 2023年05月22日
统计字数: 10239字
阅读时间: 21分钟阅读
本文链接: https://soulteary.com/2023/05/22/using-golang-and-docker-to-implement-python-computing-services.html文章来源地址https://www.toymoban.com/news/detail-580884.html

到了这里,关于使用 Golang 和 Docker 实现 Python 计算服务的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Docker部署Golang服务

    不管是开发还是生产环境,通过 docker 方式部署服务都是一种不错的选择,能够解决不同开发环境一致性的问题。 本文以项目:https://github.com/johncxf/go-api 为例。 Dockerfile 构建 Go 运用环境 在项目根目录下添加 Dockerfile 文件: 如果需要缩小镜像大小,则可以用以下方式进行构建

    2024年01月22日
    浏览(41)
  • 小白的proxmox ve(pve)打造AIO(all in boom)折腾日记 (六)黑群晖nas虚拟机建立(包括群晖安装使用及docker jellyfin、qbittorrent的安装使用)

            很多人搞aio主机的最终目的就在于搞一个nas,群晖系统算是nas中用户用的比较多的,各种软件做的也相对比较好。我这里安装的是黑群晖DS918-7.01。         群晖安装部分主要参考教程为:Proxmox VE(PVE)安装黑群晖DSM7.X教程(PVE虚拟机安装群晖DS918-7.01保姆级教程)

    2024年02月16日
    浏览(51)
  • armbian折腾之docker搭建chatgptweb指导(无需魔法)

    好久都没有折腾armbian,导致吃了很长时间的灰,今天偶然看到B站UP主 JeeJK007 的搭建视频,便想着能不能在本地搭建一个玩一玩。在此感谢UP主 JeeJK007 的无私奉献! 优势:docker部署没有硬件环境限制,使用第三方API,不用魔法,成本低 使用的工具和资料 ChatGPT-4 Turbo网站搭建

    2024年02月03日
    浏览(42)
  • 随身WIFI折腾日记(三)---Docker+ssh远程访问+青龙面板

    安装完Docker以后,我们便可以一键部署一些服务上去了。 启动docker:sudo systemctl start docker 停止docker:sudo systemctl stop docker 重启docker:suto systemctl restart docker 开机启动docker:sudo systemctl enable docker 查看docker状态:sudo systemctl status docker 查看docker启动情况:docker version 查看docker是

    2024年02月13日
    浏览(46)
  • Docker安装Nginx/Python/Golang/Vscode【亲测可用】

    一、docker安装nginx docker安装nginx,安装的是最新版本的:docker pull nginx:latest 创建一个容器:docker run --name my-nginx -p 80:80 -d nginx:latest 开启一个交互模式终端:docker exec -it my-nginx bash 创建django项目:django-admin startproject mysite 查看容器ID:docker ps -a; 设置容器自动重启:docker update

    2024年02月02日
    浏览(37)
  • 【Docker】golang使用DockerFile正确食用指南

    大家好 我是寸铁👊 总结了一篇golang使用DockerFile正确食用指南✨ 喜欢的小伙伴可以点点关注 💝 今天寸铁想让编写好的 go 程序在 docker 上面跑,要想实现这样的效果,就需要用到今天的主角: Docker File ,那怎么使用 DockerFile 呢? 那具体怎么做呢?其实很简单,不过网上的博

    2024年03月12日
    浏览(74)
  • docker版jxTMS使用指南:python服务之设备策略

    本文讲解4.0版的jxTMS中python服务的设备策略,整个系列的文章请查看:docker版jxTMS使用指南:4.0版升级内容 docker版本的使用,请参考:docker版jxTMS使用指南 jxTMS实现的接口机对设备的数据采集与处理采取的是框架组装模式。 即jxTMS定义了一整套的设备数据采集、处理、查询/访

    2024年02月07日
    浏览(48)
  • Golang实现简单WebSocket服务

    我们每天接触到各类应用,如社交、在线文档、直播等,后端都需要使用WebSocket技术提供实时通信能力。本文介绍如何使用Golang实现实时后端WebSocket服务,首先使用Gin框架搭建http服务,然后使用 gorilla/websocket 库实现简单后端WebSocket服务,示例实现从0到1的过程,适合初学者快

    2024年02月16日
    浏览(43)
  • vue3+nodejs(websocket)实现监控拉rtsp流,使用flv.js+ffmpeg包(主要建立websocket是为了转码传流)

    关于拉取监控摄像头的流,我个人去查了很多资料,也是因为之前没有接触过这一模块,加上目前公司也没有后端去写接口,所以我直接用node去写websocket,与前端建立起通信,能够进行后续转码、传流,能够实现实时播放监控画面。 这里的rtsp流是要事先知道的,监控的这个

    2024年02月20日
    浏览(49)
  • Golang 实现一个简单的 RPC 服务

    分享一个简单的 rpc 服务框架 一、服务端实现 二、客户端实现

    2024年04月10日
    浏览(45)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包