gRPC 源码详解(一)配置化的结构体

深入解析grpc源码2-客户端与服务器通信流程 前面已经讲了grpc 基础使用、protobuf 使用及原理,今天开始逐步探究grpc-go 源码实现,grpc 底层通信协议是对http2 协议的封装,grpc 并没有使用golang 官方实现的http2,而是自己实现了http2,引用了官方的帧解析器和其他枚举值,http2在其他语言实现大同小异。 一般rpc 协议会包含请求方法,请求参数,请求参数在grpc被称为消息,内容是protobuf二进制内容,方法会携带在http2 的header 里面,最后服务器解包,获取方法路由到相应的handler. 阅读详情

grpc 源码结构详解

DialOptions

DialOptions 是最重要的一环,负责配置每一次 rpc 请求的时候的一应选择。

结构

先来看看这个的结构
链接

// dialOptions configure a Dial call. dialOptions are set by the DialOption
// values passed to Dial.
type dialOptions struct {
	unaryInt  UnaryClientInterceptor
	streamInt StreamClientInterceptor

	chainUnaryInts  []UnaryClientInterceptor
	chainStreamInts []StreamClientInterceptor

	cp          Compressor
	dc          Decompressor
	bs          backoff.Strategy
	block       bool
	insecure    bool
	timeout     time.Duration
	scChan      <-chan ServiceConfig
	authority   string
	copts       transport.ConnectOptions
	callOptions []CallOption
	// This is used by v1 balancer dial option WithBalancer to support v1
	// balancer, and also by WithBalancerName dial option.
	balancerBuilder balancer.Builder
	// This is to support grpclb.
	resolverBuilder             resolver.Builder
	channelzParentID            int64
	disableServiceConfig        bool
	disableRetry                bool
	disableHealthCheck          bool
	healthCheckFunc             internal.HealthChecker
	minConnectTimeout           func() time.Duration
	defaultServiceConfig        *ServiceConfig // defaultServiceConfig is parsed from defaultServiceConfigRawJSON.
	defaultServiceConfigRawJSON *string
}

由于命名非常规范,加上注释很容易看懂每一个 field 配置的哪一条属性。如果掠过看的 大概有 压缩解压器,超时阻塞设置,认证安全转发,负载均衡,服务持久化的信息存储 ,配置,心跳检测等。

其一应函数方法都是设置 其中字段的。

如何设置

这里是 grpc 设计较好的地方,通过函数设置,同时设有生成函数的函数。什么意思呢?首先结合图来理解,这也是整个 grpc 设置的精华部分

在这里插入图片描述
这里的意思是 , DialOptions 是一个导出接口,实现函数是 apply 同时接受参数 dialOptions 来修改它。
而实际上,是使用 newFuncDialOption 函数包装一个 修改 dialOptions 的方法给 funcDialOption 结构体,在实际 Dial 调用的时候 是使用闭包 调用 funcDialOption 结构体的 apply 方法。
可以在这里看一下 Dial 方法的源码(Dial 调用的是 DialContext
起作用的就是 opt.apply()

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
	cc := &ClientConn{
		target:            target,
		csMgr:             &connectivityStateManager{},
		conns:             make(map[*addrConn]struct{}),
		dopts:             defaultDialOptions(),
		blockingpicker:    newPickerWrapper(),
		czData:            new(channelzData),
		firstResolveEvent: grpcsync.NewEvent(),
	}
	···
	for _, opt := range opts {
		opt.apply(&cc.dopts)
	}
    ···
}

这里的 options 可以说是 client 发起 rpc 请求的核心中转站。
另一个重要的接口,同时也集中在 dialOptions 结构体中初始化处理的是
callOptions []CallOption

CallOption

CallOption 是一个接口,定义在 rpc_util 包内

结构
// CallOption configures a Call before it starts or extracts information from
// a Call after it completes.
type CallOption interface {
	// before is called before the call is sent to any server.  If before
	// returns a non-nil error, the RPC fails with that error.
	before(*callInfo) error

	// after is called after the call has completed.  after cannot return an
	// error, so any failures should be reported via output parameters.
	after(*callInfo)
}

操作的是 callInfo 结构里的数据,其被包含在 dialOptions 结构体中,
即每一次 dial 的时候进行调用。

callInfo

同时它自身定义很有意思,操作的是 callInfo 结构体

// callInfo contains all related configuration and information about an RPC.
type callInfo struct {
	compressorType        string
	failFast              bool
	stream                ClientStream
	maxReceiveMessageSize *int
	maxSendMessageSize    *int
	creds                 credentials.PerRPCCredentials
	contentSubtype        string
	codec                 baseCodec
	maxRetryRPCBufferSize int
}

可以看到 callInfo 中字段用来表示 单次调用中独有的自定义选项如 压缩,流控,认证,编解码器等。

一个实现

简单看一个 CallOption 接口的实现

// Header returns a CallOptions that retrieves the header metadata
// for a unary RPC.
func Header(md *metadata.MD) CallOption {
	return HeaderCallOption{HeaderAddr: md}
}

// HeaderCallOption is a CallOption for collecting response header metadata.
// The metadata field will be populated *after* the RPC completes.
// This is an EXPERIMENTAL API.
type HeaderCallOption struct {
	HeaderAddr *metadata.MD
}

func (o HeaderCallOption) before(c *callInfo) error { return nil }
func (o HeaderCallOption) after(c *callInfo) {
	if c.stream != nil {
		*o.HeaderAddr, _ = c.stream.Header()
	}
}

重点看到,实际操作是在 before 和 after 方法中执行,它们会在 Client 发起请求的时候自动执行,顾名思义,一个在调用前执行,一个在调用后执行。

实现注意

这里可以看出,这里也是通过函数返回一个拥有这两个方法的结构体,注意这一个设计,可以作为你自己的 Option 设计的时候的参考。

两种方法

有两种方法让 Client 接受你的 CallOption 设置

  1. 在 Client 使用方法的时候直接作为 参数传递,将刚才所说的函数-返回一个实现了 CallOption 接口的结构体。
  2. 在 生成 Client 的时候就传递设置。具体如下
  3. 通过 dialOptions.go 中的 函数 grpc.WithDefaultCallOptions()
  4. 这个函数会将 CallOption 设置到 dialOptions 中的字段 []CallOption 中。
// WithDefaultCallOptions returns a DialOption which sets the default
// CallOptions for calls over the connection.
func WithDefaultCallOptions(cos ...CallOption) DialOption {
	return newFuncDialOption(func(o *dialOptions) {
		o.callOptions = append(o.callOptions, cos...)
	})
}

有没有感觉有点不好理解?给你们一个实例

  1. 使用的第一种方法
response, err := myclient.MyCall(ctx, request, grpc.CallContentSubtype("mycodec"))
  1. 使用第二种方法
myclient := grpc.Dial(ctx, target, grpc.WithDefaultCallOptions(grpc.CallContentSubtype("mycodec")))

这里假设 我们设置了一个 mycodec 的译码器。马上下面解释它的设计。

另

值得注意的是, 我好像只提到了在 Client 调用时设置,callOption 只在客户端设置的情况是不是让大家感到困惑。
实际上 gRPC server 端会自动检测 callOption 的设置,并检测自己是否支持此项选择,如果不支持则会返回失败。也就是说,在 Server 端注册的所有 Codec 译码器之后,Client 直接使用相应的设置就好了。

Codec

在 gRPC 中 Codec 有两个接口定义,一个是 baseCodec 包含正常的 Marshal 和 Unmarshal 方法,另一个是拥有名字的 Codec 定义在 encoding 包内,这是由于在注册 registry 的时候会使用到这个方法。

接口
type Codec interface {
	// Marshal returns the wire format of v.
	Marshal(v interface{}) ([]byte, error)
	// Unmarshal parses the wire format into v.
	Unmarshal(data []byte, v interface{}) error
	// String returns the name of the Codec implementation.  This is unused by
	// gRPC.
	String() string
}

就是这个方法

// RegisterCodec registers the provided Codec for use with all gRPC clients and
// servers.
//
// The Codec will be stored and looked up by result of its Name() method, which
// should match the content-subtype of the encoding handled by the Codec.  This
// is case-insensitive, and is stored and looked up as lowercase.  If the
// result of calling Name() is an empty string, RegisterCodec will panic. See
// Content-Type on
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
// more details.
//
// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe.  If multiple Compressors are
// registered with the same name, the one registered last will take effect.
func RegisterCodec(codec Codec) {
	if codec == nil {
		panic("cannot register a nil Codec")
	}
	if codec.Name() == "" {
		panic("cannot register Codec with empty string result for Name()")
	}
	contentSubtype := strings.ToLower(codec.Name())
	registeredCodecs[contentSubtype] = codec
}

Compressor

同时 encoding 包中还定义了 Compressor 接口,参照 Codec 理解即可。

// Compressor is used for compressing and decompressing when sending or
// receiving messages.
type Compressor interface {
	// Compress writes the data written to wc to w after compressing it.  If an
	// error occurs while initializing the compressor, that error is returned
	// instead.
	Compress(w io.Writer) (io.WriteCloser, error)
	// Decompress reads data from r, decompresses it, and provides the
	// uncompressed data via the returned io.Reader.  If an error occurs while
	// initializing the decompressor, that error is returned instead.
	Decompress(r io.Reader) (io.Reader, error)
	// Name is the name of the compression codec and is used to set the content
	// coding header.  The result must be static; the result cannot change
	// between calls.
	Name() string
}

MetaData

这个包对应 context 中的 Value field 也就是 key-value 形式的存储

在其他包中简写是 MD

结构
type MD map[string][]string

函数

实现了完善的存储功能,从单一读写到批量(采用 pair 模式,…string 作为参数,len(string)%2==1 时会报错,由于会有孤立的没有配对的元信息。

另外几个函数是实现了从 context 中的读取和写入(这里的写入是 使用 context.WithValue 方法,即生成 parent context 的 copy。

注意⚠️
  • 值得注意的是,在 MetaData 结构体中, value 的结构是 []string 。
  • 同时 key 不可以以 “grpc-” 开头,这是因为在 grpc 的 internal 包中已经保留了。
  • 更为重要的是 在 context 中的读取方式,其实是 MetaData 结构对应的是 context Value 中的 value 值,而 key 值设为 一个空结构体同时区分输入输入
    • type mdIncomingKey struct{}<br />
    • type mdOutgoingKey struct{}
深入解析grpc源码1-grpc介绍及使用 1grpc 介绍 在grpc 官网上,grpc 是这样介绍的: 开源高性能远程调用(RPC)框架,可以在任何环境中运行。它支持可插拔的负载均衡、链路跟踪、健康检查和身份验证等等商业和安全功能 grpc 是一项进程间通信技术,可以用来连接、调用、操作和调试构建分布式程序,调用过程如同调用函数一样,整个过程操作起来很简单,就像调用本地方法一样。与许多rpc 系统一样,grpc 是定义服务的思想,服务器需要实现此接口并运行grpc 来处理客户端调用。 gRPC客户端和服务器可以在各种环境中运行并相互通 阅读详情

相关推荐

奇迹MU模拟器源码深度解析:从VC++网络编程到MMORPG架构设计

网络编程是构建分布式系统的核心技术,其核心原理在于通过Socket API实现进程间通信,采用自定义二进制协议进行高效数据序列化与反序列化。在游戏开发领域,尤其是MMORPG服务器架构中,网络编程的价值尤为突出,它直接决定了游戏的实时性、可扩展性与安全性。经典游戏如《奇迹MU》的服务端模拟器源码,为我们提供了一个完整的研究样本,展示了如何基于VC++和Winsock实现多进程协作的C/S架构。通过剖析其GameServer、DataServer、JoinServer等核心模块,可以深入理解状态机设计、数据包

weixin_30509393的博客 454

grpc源码解析

上一篇笔者总结的grpc的文档只是整体介绍了一下grpc的框架和特性,但是一些细节的实现和一些概念性知识点依然存在疑惑,查了一些资料,都比较千篇一律,因此现在尝试学习grpc的源码来解答自己的疑惑 目标 了解grpc项目的总架构 了解grpc连接池、client/server端的解析工作 了解grpc拦截器等特性的设计模式 server端处理流程 lis, err := net.Listen("tcp", port) if err != nil { logs.Error("failed to

u012588879的专栏 2656

YOLO归一插件:统一推理接口,简化目标检测部署流程

在计算机视觉领域,目标检测是识别图像中物体位置与类别的核心技术。其原理通常基于深度学习模型,通过卷积神经网络提取特征并预测边界框。这项技术的核心价值在于为安防监控、自动驾驶、工业质检等场景提供自动化视觉感知能力。然而,不同版本的YOLO模型在预处理、后处理上存在差异,导致部署碎片化和性能波动。针对这一痛点,YOLO归一插件应运而生,它通过标准化预处理、模型加载和结果解析,为YOLOv5、v8、v11等主流版本提供统一的推理接口。该工具支持批量任务和GPU加速,能有效提升开发效率与推理一致性,是构建稳健视觉服

weixin_34248705的博客 577

Go - 学习 grpc.Dial(target string, opts …DialOption) 的写法

咱们平时是这样使用 grpc.Dial 方法的,比如:conn,err:=grpc.Dial("127.0.0.1:8000", grpc.WithChainS...

新亮笔记 4555

基于Protobuf的代码生成引擎Vishwakarma:从原理到自定义生成器实战

在微服务架构中,Protocol Buffers(Protobuf)作为高效的接口定义语言(IDL),已成为定义服务契约和数据结构的标准。其核心原理是通过.proto文件定义结构化数据,并利用protoc编译器生成多语言序列化代码,解决了跨语言数据交换的难题。然而,原生protoc生成的代码往往过于基础,缺乏与特定业务框架、验证逻辑或团队规范的集成,这构成了开发中的“最后一公里”问题。为了解决这一痛点,代码生成技术应运而生,它通过自动化手段,将IDL转换为符合项目特定要求的、可直接使用的客户端SDK、服务端

weixin_33742618的博客 863

OpenClaw工程化:从AI Agent玩具到生产级基础设施的蜕变

在AI Agent领域,从概念验证到生产部署面临的核心挑战是稳定性、可扩展性和工程规范性。这涉及到状态管理、工具执行安全性和系统可观测性等基础工程问题。通过引入基础设施层(如Harness),可以为Agent核心提供持久化状态管理、沙盒化工具执行和结构化监控能力,从而将AI能力转化为可靠的生产力工具。本文以OpenClaw为例,探讨其如何通过工程化实践解决这些痛点,实现从演示原型到企业级应用的跨越,为开发者构建高可用、可维护的智能体系统提供参考框架。

weixin_34259232的博客 786

gRPC源码分析0-导读

gRPC是Google开源的新一代RPC框架,官网是http://www.grpc.io。正式发布于2016年8月,技术栈非常的新,基于HTTP/2,netty4.1,proto3。虽然目前在工程化方面gRPC还非常不足,但它也值得我们好好研究它,学习他。 1. 使用场景 按照Google的说法,使用广泛,但主要使用场景还是在移动端: Efficiently connecting p...

weixin_30325793的博客 231

PaddleOCRSharp服务化实战:OCRService源码解析与参数调优

OCR技术在企业数字化转型中应用广泛,从票据识别到文档数字化,都离不开稳定高效的识别能力。开源框架PaddleOCR在中文场景表现突出,但原生Python实现难以直接嵌入.NET生态,C#开发者常面临集成门槛。PaddleOCRSharp作为PaddleOCR的.NET封装库,提供跨平台推理能力和简洁API,有效解决了这一痛点。然而,直接调用原始封装存在引擎生命周期管理、参数配置、结果归一化等生产级问题。通过构建OCRService服务层,可以统一处理图像预处理、检测参数调优、并发控制与资源释放,从而提升识

weixin_34008784的博客 433

小团队大开发:iogo微服务集群框架(golang/grpc/http/protobuf/etcd/freetoo/5G物联网/服务发现/负载均衡/一致性哈希/码客/卢益贵)

小团队大开发:iogo微服务集群框架 KeyWord: iogo,golang,grpc,protobuf,etcd,zookeeper,microservice,distributed lock,service discovery,cluster,load balancing,k8s,docker,redis,mamcache,5G物联网,微服务框架,分布式集群,分布式锁,分布式文件系统,分布式数据库,高速缓存集群,服务发现,负载均衡,容器编排,一致性哈希 目录 小团队大开发:iogo微..

码客 卢益贵 ygluu 1946

grpc源码,方便编译

grpc源码,方便编译。

java发送HTTP请求(附带源码)

java发送HTTP请求(附带源码)

欢迎来到我的博客!这里是一个专注于计算机技术的分享平台,涵盖编程开发、算法研究、系统架构、软件工程等多个领域。无论你是初学者还是资深开发者,都能在这里找到有价值的内容。 2012

C# Code-Behind 技术全面解析

Code-Behind 是 ASP.NET Web Forms 中的一种技术模式,通过将用户界面标记(.aspx文件)与程序逻辑代码(.aspx.cs文件)分离,实现表现层与逻辑层的解耦。其核心思想是前端展示与后台逻辑的物理文件分离,通过ASP.NET编译器将两者合并编译成一个Page类并执行。Code-Behind 的优势包括较好的代码组织、较高的开发效率,尤其适合快速开发数据驱动应用。然而,与现代开发模式如ASP.NET MVC、ASP.NET Core和Blazor相比,Code-Behind 在可维

记录学习的过程 958

Geo 优化源码二次开发:自定义地域规则改造实操文档

规则1: 对于来自“广东省-深圳市”的用户,在查询结果中附加一个标签。规则2: 禁止来自“江苏省-南京市”且 IP 段为的内网用户访问某个特定 API 端点。规则3: 当用户来自“海外(非中国)”时,默认使用英文界面标识lang: en。我们需要改造rule/包来支持这些动态、可配置的规则。首先,在中定义新的规则结构体,支持更细粒度的条件与动作。// CustomRule 自定义地域规则// 条件部分// 动作部分。

2608_96547732的博客 330

BPMN2.0规范及流程引擎选型方案

本文主要简明扼要的介绍了BPMN2.0规范的内容,可让读者快速度的了解BPMN2.0所涵盖的主要内容和图形样式。也较为详细的阐述了如何绘制标准的流程图,以及在软件层面中如何实现业务流程功能,和技术选型。

比特桃 1万+

《UE4游戏开发》之 《音效系统的实现》

音效系统需要负责管理什么事情: 1.1 确定声音控制的分类:如背景音乐、技能音效、界面音效、角色配音等,不同游戏对声音有不同的分类 1.2 控制每个分类的声音的开启与关闭,以及调节每个分类声音的大小 解决如何分类: 2.1 首先需要知道ue4为声音分类提供了什么:SoundClass通常需要一个根分类【SC_RPG_Master】,便于总体控制音效,以及指定引擎默认启动的音效 打开SoundClass资产【SC_RPG_Master】,对根控制的设置,将逐级传递并按比例进行设置,以此达到控制一类声音的目的.

qq_21919621的博客 2126

TubeMQ源码构建运行

准备工作 简介 TubeMQ是一款开源的分布式消息中间件,采用Protocol Buffer构建消息对象模型。于2019年9月12日开源,即中秋 前一天。 下载 安装 git 等工具 简单粗暴git直接拉取 git clone https://github.com/Tencent/TubeM 源码结构 整体结构 结构释义 可见暂时支持Linux启动、打包可配置化 、丰富的Demo。 试用 牛刀小试 编译 点击生产者Demo运行,报错: Error:(26...

uuqaz的博客 334

AI工程化之生成式UI & A2UI(二)

本文介绍了生成式UI框架A2UI的架构设计与核心实现。项目采用AI协作开发模式,通过ATDD(AI驱动的测试驱动开发)确保代码质量。前端SDK基于zustand实现框架无关的状态管理,包含协议解析器、存储管理、渲染器等核心模块。重点展示了基础store构建、A2UI协议解析器的实现过程,包括mock数据准备、测试用例生成和可视化调试。项目采用monorepo管理,利用vite实现高效开发构建。整个系统设计遵循模块化原则,通过清晰的接口定义实现各组件解耦,为后续功能扩展打下基础。

qq_34306228的博客 407

OpenClaw与NVIDIA AI集成实战:构建企业级智能体工作流

在AI应用开发中,模型服务化部署与业务逻辑解耦是关键挑战。通过将AI模型封装为标准化API服务,可以实现专业化分工,让业务系统专注于流程编排。OpenClaw作为智能体框架,通过技能机制将复杂业务逻辑模块化,而NVIDIA的Nemotron大语言模型和NeMo框架提供强大的AI推理能力。这种架构让企业能够构建可编排的AI工作流,将模型推理任务交给专门的GPU集群处理,业务代码只需通过标准协议调用推理端点。结合Docker容器化部署和NVIDIA Container Toolkit,可以快速搭建稳定环境。通过

weixin_29040367的博客 224

Python爬虫实战:全方位解析前程无忧(51job)职位详情爬取及反爬破解

平台规模:覆盖全国各行业、数百万条岗位信息。数据特点:职位描述详细,职位标签丰富。数据价值:支持招聘趋势分析、职业发展研究、薪资行情调研。推荐Python 3.10+,支持最新async特性与库兼容。

2201_76125261的博客 1488

如何借助GPT- 6 写好一篇论文:从引言到参考文献完整步骤(附专业提示词)

七哥就结合这篇文章的写作思路,依次拆解引言、方法、结果、讨论、摘要、标题和参考文献分别应该怎么写,同时也聊聊,在每一个环节里,我们可以怎样合理借助 GPT-6,让论文写作变得更清晰、更高效。可以和七哥交流学术写作或Gemini、GPT、Claude 等大模型 学术实操相关问题,多多交流,相互成就,共同进步。8、不修改、不补写、不猜测任何样本量、阈值、剂量、设备型号、参数或统计方法,所有缺失但影响方法完整性的信息统一标记为【待作者确认】、检查方法中设定的每一个分析项目能否在后续结果章节找到对应位置。

智能AI让学术科研更容易 113

JAVA教练培训课程培训教练排课系统源码的设计理念

摘要:本文设计了一个基于Java的教练培训课程与排课系统,围绕高效性、灵活性、可扩展性和用户体验四大原则展开。系统采用分层架构,包含课程管理、教练管理、排课引擎和冲突检测等核心模块,支持智能排课算法和动态调整功能。技术选型上使用SpringBoot、MySQL、Redis和Vue.js等主流框架,实现模块化设计和多终端适配。通过拖拽式排课、可视化展示和智能提示等优化手段提升用户体验,同时支持微服务扩展和配置化管理,满足不同业务场景需求。

m0_73514975的博客 237

msfvenom制作windows/linux/android/ios木马

msfvenom选项如下 -p, --payload<payload> 指定 payload(攻击荷载)。如果使用自定义payload,请使用'-'或者stdin指定。 -l, --list [module_typel列出指定模块的所有可用资源。模块类型包括:payloads, encoders, nops, all -n, --nopsled <length>为payload预先指定一个NOP滑动长度。 -f, --format <format>指定输出格式(使

Cwillchris的博客 2734

大白话说Java设计模式-07-抽象工厂模式(源码剖析篇)

本文从JDK、Spring、MyBatis源码角度深度剖析抽象工厂模式的工业级实现,详解JDK Swing UI组件工厂、Spring AbstractBeanFactory、MyBatis SqlSessionFactory等场景中抽象工厂模式的设计思路,分析框架中抽象工厂如何适配多产品族扩展需求,对比不同实现的性能与扩展性权衡,帮助读者掌握抽象工厂模式在框架级代码中的应用原理与实践借鉴思路。

Java后端 | AI智能体应用实战派 | 电脑技能行家 278

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

Caddy源码阅读(二)启动流程与 Event 事件通知

Caddy源码阅读(二)启动流程与 Event 事件通知 Preface Caddy 是 Go 语言构建的轻量配置化服务器。https://github.com/caddyserver/caddy Caddy 整个软件可以说是由不同的 插件 堆砌起来的。自己本身仅提供 Plugin 的注册运行逻辑和 Server 的监听服务功能。 学习 caddy 的源码,实际上是学习 如何构建一个 ...

aohui3277的博客 331

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

上一篇: 《第五项修炼》学习型组织的艺术与实践
下一篇: 用 Go 语言实现一个 telegram 的 bot - 成功复读!
杨鼎睿
博客等级 码龄9年 7粉丝 27原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值