go-zero开发入门之gateway深入研究1

2023-12-16

创建一个 gateway 示例:

// main.go
package main

import (
    "flag"
    "fmt"
    "gateway/middleware"

    "github.com/zeromicro/go-zero/core/conf"
    "github.com/zeromicro/go-zero/gateway"
)

var configFile = flag.String("f", "etc/gateway.yaml", "the config file")

func main() {
    var c gateway.GatewayConf
    flag.Parse()

    // 加载 gateway 配置,如果配置有问题记录 FATAL 日志后即退出
    conf.MustLoad(*configFile, &c)

    // 实例化 gateway,如果出错记录 FATAL 日志后即退出
    // 可能的出错包括:
    // 1)初始化日志 logx 失败(创建日志文件失败),日志文件含以下五种:
    //    信息级别的日志:infoLog
    //    错误级别的日志:errorLog
    //    严重级别的日志:severeLog
    //    慢查询日志:slowLog
    //    统计日志:statLog
    // 而堆栈日志 stackLog 同 errorLog 一起,访问日志 access 同 infoLog 。
    server := gateway.MustNewServer(c)
    defer server.Stop()

    fmt.Printf("Starting gateway at %s:%d...\n", c.Host, c.Port)
    server.Start()
}


// gateway/server.go
// MustNewServer creates a new gateway server.
func MustNewServer(c GatewayConf, opts ...Option) *Server {
	svr := &Server{
		upstreams: c.Upstreams,
		Server:    rest.MustNewServer(c.RestConf),
	}
	for _, opt := range opts {
		opt(svr)
	}

	return svr
}

// rest/server.go
// MustNewServer returns a server with given config of c and options defined in opts.
// Be aware that later RunOption might overwrite previous one that write the same option.
// The process will exit if error occurs.
func MustNewServer(c RestConf, opts ...RunOption) *Server {
	server, err := NewServer(c, opts...)
	if err != nil {
		logx.Must(err)
	}

	return server
}

gateway.MustNewServer 调用了 rest.MustNewServer,但在 rest.MustNewServer 增加了 upstreams 的初始化。upstreams 源自于 gateway.GatewayConf,对应的配置如下:。

Upstreams: # 网关上游的配置列表
  - Grpc: # 网关上游只能为 grpc 服务,不支持 http 等服务其它服务
    Etcd: # 服务发现用的 Etcd 配置
        Hosts: # Etcd 的服务地址列表
          - 127.0.0.1:2379
        Key: login.rpc # 服务注册在 Etcd 的 key
    ProtoSets: # 服务的 pb 文件列表(使用工具 protoc 根据 proto 生成 pb 文件:protoc --descriptor_set_out=login.pb login.proto)
      - proto/login.pb
    Mappings: # Mappings can also be written in proto options 定义 http 路径到 rpc 路径的映射列表
      - Method: get
        Path: /v1/login
        RpcPath: login.Login/login // 格式:包名.服务名/方法名

从上述内容可以看出,go-zero 的 gateway 在 rest 基础上增加了 upstreams 。当然不仅这一些,在 gateway 启动时也增加了特有的东西:

// Start starts the gateway server.
func (s *Server) Start() {
	logx.Must(s.build()) // 这也是 gateway 在 rest 基础上新增的
	s.Server.Start()
}

上述 s.build() 的源代码如下:

// gateway/server.go
func (s *Server) build() error {
    // 调用 s.ensureUpstreamNames() 确保所有上游服务(gRPC 服务)的名称都是唯一的,
    // 如果有重复的名称,函数返回错误。
	if err := s.ensureUpstreamNames(); err != nil {
		return err
	}

    // 使用 mr.MapReduceVoid 函数进行 MapReduce 操作,这个函数接收三个参数:
    // 1)一个用于生成数据源的函数
    // 2)一个 Map 函数
    // 3)一个 Reduce 函数
	return mr.MapReduceVoid(func(source chan<- Upstream) {
        // 生成数据源的函数:
        // 遍历 s.upstreams(上游服务列表),将每个上游服务发送到 Map 函数
		for _, up := range s.upstreams {
			source <- up
		}
	}, func(up Upstream, writer mr.Writer[rest.Route], cancel func(error)) { // Map 函数,对于每个上游服务,执行以下操作:
		var cli zrpc.Client

        // 创建一个 gRPC 客户端 cli,用于与上游服务通信
		if s.dialer != nil {
			cli = s.dialer(up.Grpc)
		} else {
			cli = zrpc.MustNewClient(up.Grpc)
		}

        // 调用 s.createDescriptorSource(cli, up) 创建一个描述符源 grpcurl.DescriptorSource),
        // 用于获取 gRPC 服务的元数据。
		source, err := s.createDescriptorSource(cli, up)
		if err != nil {
			cancel(fmt.Errorf("%s: %w", up.Name, err))
			return
		}

        // 使用 internal.GetMethods(source) 获取 gRPC 服务的所有方法
		methods, err := internal.GetMethods(source)
		if err != nil {
			cancel(fmt.Errorf("%s: %w", up.Name, err))
			return
		}

        // 创建一个 gRPCurl 解析器,用于解析 gRPC 方法的元数据
		resolver := grpcurl.AnyResolverFromDescriptorSource(source)

        // 遍历这些方法,为每个具有 HTTP 方法和路径的方法生成一个 HTTP 处理器(s.buildHandler(...)),
        // 并将它们映射到 RESTful API 的路由上。
		for _, m := range methods {
			if len(m.HttpMethod) > 0 && len(m.HttpPath) > 0 {
				writer.Write(rest.Route{
					Method:  m.HttpMethod,
					Path:    m.HttpPath,
                    // http 调用转为 rpc 调用
					Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
				})
			}
		}

		methodSet := make(map[string]struct{})
		for _, m := range methods {
			methodSet[m.RpcPath] = struct{}{}
		}

        // 遍历 up.Mappings(自定义的 RESTful API 映射),
        // 为每个映射生成一个 HTTP 处理器,并将生成的路由写入到 Reduce 函数。
        // 如果映射中指定的 gRPC 方法不存在,则返回错误。
		for _, m := range up.Mappings {
            // 在将方法映射到路由之前,函数会检查映射是否存在,如果不存在则返回错误
			if _, ok := methodSet[m.RpcPath]; !ok {
				cancel(fmt.Errorf("%s: rpc method %s not found", up.Name, m.RpcPath))
				return
			}

			writer.Write(rest.Route{
				Method:  strings.ToUpper(m.Method),
				Path:    m.Path,

                // 调用 buildHandler 函数来构建一个处理器,用于处理 RESTful API 请求
				Handler: s.buildHandler(source, resolver, cli, m.RpcPath),
			})
		}
	}, func(pipe <-chan rest.Route, cancel func(error)) {
        // Reduce 函数:
        // 从管道中读取生成的路由,并将它们添加到 HTTP 服务器(s.Server)中
		for route := range pipe {
			s.Server.AddRoute(route)
		}
	})
}

这个函数的主要目的是将 gRPC 服务的方法映射到 HTTP RESTful API,并将生成的 API 添加到 HTTP 服务器中。通过这种方式,可以在 gRPC 服务的基础上提供一个 RESTful API,使得客户端可以使用 HTTP 调用 gRPC 服务。

下为 mr.MapReduceVoid 的源代码:

// core/mr/mapreduce.go
// MapReduceVoid maps all elements generated from given generate,
// and reduce the output elements with given reducer.
func MapReduceVoid[T, U any](generate GenerateFunc[T], mapper MapperFunc[T, U], reducer VoidReducerFunc[U], opts ...Option) error {
	_, err := MapReduce(generate, mapper, func(input <-chan U, writer Writer[any], cancel func(error)) {
		reducer(input, cancel)
	}, opts...)
	if errors.Is(err, ErrReduceNoOutput) {
		return nil
	}

	return err
}

// MapReduce maps all elements generated from given generate func,
// and reduces the output elements with given reducer.
func MapReduce[T, U, V any](generate GenerateFunc[T], mapper MapperFunc[T, U], reducer ReducerFunc[U, V], opts ...Option) (V, error) {
	panicChan := &onceChan{channel: make(chan any)}
	source := buildSource(generate, panicChan)
	return mapReduceWithPanicChan(source, panicChan, mapper, reducer, opts...)
}
// gateway/server.go
func (s *Server) buildHandler(source grpcurl.DescriptorSource, resolver jsonpb.AnyResolver,
	cli zrpc.Client, rpcPath string) func(http.ResponseWriter, *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		parser, err := internal.NewRequestParser(r, resolver)
		if err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
			return
		}

		w.Header().Set(httpx.ContentType, httpx.JsonContentType)
		handler := internal.NewEventHandler(w, resolver)
        // http 调用转成了 grpc 调用
		if err := grpcurl.InvokeRPC(r.Context(), source, cli.Conn(), rpcPath, s.prepareMetadata(r.Header),
			handler, parser.Next); err != nil {
			httpx.ErrorCtx(r.Context(), w, err)
		}

		st := handler.Status
		if st.Code() != codes.OK {
			httpx.ErrorCtx(r.Context(), w, st.Err())
		}
	}
}

http 调用转 grpc 调用过程复杂,最终调用了 grpc-go 的 Invoke:

// https://github.com/grpc/grpc-go/blob/master/clientconn.go
// ClientConnInterface defines the functions clients need to perform unary and
// streaming RPCs.  It is implemented by *ClientConn, and is only intended to
// be referenced by generated code.
type ClientConnInterface interface { // ClientConn 实现了该接口,实现落在两个文件中:clientconn.go 和 call.go
	// Invoke performs a unary RPC and returns after the response is received
	// into reply.
	Invoke(ctx context.Context, method string, args any, reply any, opts ...CallOption) error
	// NewStream begins a streaming RPC.
	NewStream(ctx context.Context, desc *StreamDesc, method string, opts ...CallOption) (ClientStream, error)
}

中间还用到了 grpcdynamic 的 Stub.InvokeRpc:

// https://github.com/jhump/protoreflect/blob/main/dynamic/grpcdynamic/stub.go
// InvokeRpc sends a unary RPC and returns the response. Use this for unary methods.
func (s Stub) InvokeRpc(ctx context.Context, method *desc.MethodDescriptor, request proto.Message, opts ...grpc.CallOption) (proto.Message, error) {
	if method.IsClientStreaming() || method.IsServerStreaming() {
		return nil, fmt.Errorf("InvokeRpc is for unary methods; %q is %s", method.GetFullyQualifiedName(), methodType(method))
	}
	if err := checkMessageType(method.GetInputType(), request); err != nil {
		return nil, err
	}
	resp := s.mf.NewMessage(method.GetOutputType())
	if err := s.channel.Invoke(ctx, requestMethod(method), request, resp, opts...); err != nil {
		return nil, err
	}
	return resp, nil
}
// https://github.com/grpc/grpc-go/blob/master/call.go
package grpc

import (
	"context"
)

// Invoke sends the RPC request on the wire and returns after response is
// received.  This is typically called by generated code.
//
// All errors returned by Invoke are compatible with the status package.
func (cc *ClientConn) Invoke(ctx context.Context, method string, args, reply any, opts ...CallOption) error {
	// allow interceptor to see all applicable call options, which means those
	// configured as defaults from dial option as well as per-call options
	opts = combine(cc.dopts.callOptions, opts)

	if cc.dopts.unaryInt != nil {
		return cc.dopts.unaryInt(ctx, method, args, reply, cc, invoke, opts...)
	}
	return invoke(ctx, method, args, reply, cc, opts...)
}

func invoke(ctx context.Context, method string, req, reply any, cc *ClientConn, opts ...CallOption) error {
    // cs 类型为,
    // 结构体 clientStream 实现了接口 ClientStream
	cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
	if err != nil {
		return err
	}
	if err := cs.SendMsg(req); err != nil { // 发送请求
		return err
	}
	return cs.RecvMsg(reply) // 接收响应
}

// ClientStream defines the client-side behavior of a streaming RPC.
//
// All errors returned from ClientStream methods are compatible with the
// status package.
type ClientStream interface {
	// Header returns the header metadata received from the server if there
	// is any. It blocks if the metadata is not ready to read.  If the metadata
	// is nil and the error is also nil, then the stream was terminated without
	// headers, and the status can be discovered by calling RecvMsg.
	Header() (metadata.MD, error)

	// Trailer returns the trailer metadata from the server, if there is any.
	// It must only be called after stream.CloseAndRecv has returned, or
	// stream.Recv has returned a non-nil error (including io.EOF).
	Trailer() metadata.MD

	// CloseSend closes the send direction of the stream. It closes the stream
	// when non-nil error is met. It is also not safe to call CloseSend
	// concurrently with SendMsg.
	CloseSend() error

	// Context returns the context for this stream.
	//
	// It should not be called until after Header or RecvMsg has returned. Once
	// called, subsequent client-side retries are disabled.
	Context() context.Context

	// SendMsg is generally called by generated code. On error, SendMsg aborts
	// the stream. If the error was generated by the client, the status is
	// returned directly; otherwise, io.EOF is returned and the status of
	// the stream may be discovered using RecvMsg.
	//
	// SendMsg blocks until:
	//   - There is sufficient flow control to schedule m with the transport, or
	//   - The stream is done, or
	//   - The stream breaks.
	//
	// SendMsg does not wait until the message is received by the server. An
	// untimely stream closure may result in lost messages. To ensure delivery,
	// users should ensure the RPC completed successfully using RecvMsg.
	//
	// It is safe to have a goroutine calling SendMsg and another goroutine
	// calling RecvMsg on the same stream at the same time, but it is not safe
	// to call SendMsg on the same stream in different goroutines. It is also
	// not safe to call CloseSend concurrently with SendMsg.
	//
	// It is not safe to modify the message after calling SendMsg. Tracing
	// libraries and stats handlers may use the message lazily.
	SendMsg(m any) error

	// RecvMsg blocks until it receives a message into m or the stream is
	// done. It returns io.EOF when the stream completes successfully. On
	// any other error, the stream is aborted and the error contains the RPC
	// status.
	//
	// It is safe to have a goroutine calling SendMsg and another goroutine
	// calling RecvMsg on the same stream at the same time, but it is not
	// safe to call RecvMsg on the same stream in different goroutines.
	RecvMsg(m any) error
}

调用路径归纳总结:

   grpcurl/grpcurl.InvokeRPC()/invoke.go
-> grpcdynamic/Stub.InvokeRpc()/stub.go
-> grpc-go/grpc.ClientConn.Invoke()/clientconn.go|call.go // ClientConn 是一个 struct,实现了接口 ClientConnInterface
-> grpc-go/grpc.invoke()/call.go // invoke 是 grpc 下的全局私有函数
-> grpc-go/grpc.clientStream::SendMsg()/stream.go // clientStream 是一个 struct,实现了接口 ClientStream
-> grpc-go/grpc.csAttempt::SendMsg()/stream.go // csAttempt 是一个 struct,实现了接口 ClientTransport
-> grpc-go/grpc.ClientTransport::write()/internal/transport/transport.go // ClientTransport 是一个接口,结构体 http2Client 实现了 ClientTransport
-> grpc-go/grpc.http2Client::Write()/internal/transport/http2_client.go // 结构体 http2Client 实现了 ClientTransport,将数据写入 http2Client.controlBuf 中

http2Client::Write 将数据写入 http2Client.controlBuf 后返回,数据的发送由另外的协程 loopyWriter.run() 负责:

// https://github.com/grpc/grpc-go/blob/master/internal/transport/controlbuf.go
//
// run should be run in a separate goroutine.
// It reads control frames from controlBuf and processes them by:
// 1. Updating loopy's internal state, or/and
// 2. Writing out HTTP2 frames on the wire.
//
// Loopy keeps all active streams with data to send in a linked-list.
// All streams in the activeStreams linked-list must have both:
// 1. Data to send, and
// 2. Stream level flow control quota available.
//
// In each iteration of run loop, other than processing the incoming control
// frame, loopy calls processData, which processes one node from the
// activeStreams linked-list.  This results in writing of HTTP2 frames into an
// underlying write buffer.  When there's no more control frames to read from
// controlBuf, loopy flushes the write buffer.  As an optimization, to increase
// the batch size for each flush, loopy yields the processor, once if the batch
// size is too low to give stream goroutines a chance to fill it up.
//
// Upon exiting, if the error causing the exit is not an I/O error, run()
// flushes and closes the underlying connection.  Otherwise, the connection is
// left open to allow the I/O error to be encountered by the reader instead.
func (l *loopyWriter) run() (err error) {
	defer func() {
		if l.logger.V(logLevel) {
			l.logger.Infof("loopyWriter exiting with error: %v", err)
		}
		if !isIOError(err) {
			l.framer.writer.Flush()
			l.conn.Close()
		}
		l.cbuf.finish()
	}()
	for {
		it, err := l.cbuf.get(true)
		if err != nil {
			return err
		}
		if err = l.handle(it); err != nil {
			return err
		}
		if _, err = l.processData(); err != nil {
			return err
		}
		gosched := true
	hasdata:
		for {
			it, err := l.cbuf.get(false)
			if err != nil {
				return err
			}
			if it != nil {
				if err = l.handle(it); err != nil {
					return err
				}
				if _, err = l.processData(); err != nil {
					return err
				}
				continue hasdata
			}
			isEmpty, err := l.processData() // 最底层调用了 Go 标准库的 io.Writer::Write(),Writer 是一个接口
			if err != nil {
				return err
			}
			if !isEmpty {
				continue hasdata
			}
			if gosched {
				gosched = false
				if l.framer.writer.offset < minBatchSize {
					runtime.Gosched()
					continue hasdata
				}
			}
			l.framer.writer.Flush()
			break hasdata
		}
	}
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

go-zero开发入门之gateway深入研究1 的相关文章

  • 在 OSX 上交叉编译 Go?

    我正在尝试在 OSX 上交叉编译 go 应用程序以构建适用于 Windows 和 Linux 的二进制文件 我已经阅读了网上能找到的所有内容 我发现的最接近的例子已经发布在 除了疯狂邮件列表上许多未完成的讨论之外 http solovyov
  • 是否可以将 Go 代码作为脚本运行?

    随着Go正在成为 系统 的语言 我想知道是否可以将 Go 代码作为脚本运行而不编译它 有可能这样做吗 动机 因为有关于动机的问题 取自如何使用 Scala 作为脚本语言 http alvinalexander com scala how t
  • 带有导出字段的私有类型

    在 Go 教程的第二天有这样的练习 为什么拥有带有导出字段的私有类型会很有用 例如 package geometry type point struct X Y int name string 请注意point是小写的 因此不会导出 而字段
  • Golang 从管道读取读取大量数据

    我正在尝试读取一个正在被焦油化 流式传输到标准输入的存档 但我正在以某种方式读取far管道中的数据多于 tar 发送的数据 我像这样运行我的命令 tar cf somefolder my go binary 源代码是这样的 package
  • 模块路径格式错误...第一个路径元素中缺少点

    我有一个包含 2 个不同可执行文件的项目 每个可执行文件都有自己的依赖项以及对根的共享依赖项 如下所示 Root gt server gt main go gt someOtherFiles go gt go mod gt go sum g
  • 防止使用 golang 服务器访问文件夹中的文件

    我在 golang 中有一个服务器可以处理这样的文件夹路径 fs http FileServer http Dir assets http Handle Images fs http ListenAndServe 8000 nil 但在这个
  • K8s更改配置映射并更新应用程序日志级别

    我想更改在 K8S 上运行的 Golang 应用程序的登录配置 我在本地尝试了以下代码 它按预期工作 我正在使用 viper 来监视配置文件更改 这是带有日志配置的配置图 apiVersion v1 kind ConfigMap data
  • java.lang.NoSuchMethodError:com.fasterxml.jackson.databind.type。使用 apache beam Spark runner 运行 go 示例时

    我想跑grades https github com apache beam tree master sdks go examples gradesapache beam go sdk 提出的示例 在一个主服务器和两个从服务器 spark2
  • Bazel 构建缺少严格的依赖关系

    我正在尝试使用 brazel 构建 Go 应用程序 它是一个现有的私有 GitHub 存储库 位置如下 github xyz com repo name 我正在研究 我的目标是从 main go 文件创建一个二进制文件 该文件的方法依赖于其
  • 如何在 Visual Studio Code 中为 Golang 启用竞争检测器?

    我搜索了很多网页来找到我应该放入哪个短语settings json在 VS Code Golang 扩展 由 Microsoft 发布 中添加构建标志 在我的例子中是竞赛检测器 I added go buildFlags race 在扩展名
  • Go SQL查询不一致

    我在执行查询时遇到一些非常奇怪的不一致 并且想知道是否有人知道原因 想象一下我有一个定义如下的结构 type Result struct Afield string db A Bfield interface db B Cfield str
  • Golang - 更改 Windows 上的构建工作路径

    我正在使用 SublimeText3 GoSublime 插件 在 Windows 8 上测试简单的 Go 程序 go run v example go 在运行之前它正在内部编译 应用程序数据 本地 温度 目录 我的防病毒程序认为这是病毒并
  • 给定方法值,获取接收者对象

    Go 有没有办法从方法值获取接收者对象 例如有没有这样的MagicFunc这将使以下程序输出字符串my info来自底层 Foo 实例 package main import fmt type Foo struct A string fun
  • 为什么我的 SQL 占位符没有被替换(使用 Go pq)?

    根据文档 我正在这样做 var thingname string asdf var id int err database QueryRow SELECT id from things where thing thingname Scan
  • 视频第一帧

    我正在创建一个单页应用程序 后端使用 Golang 前端使用 javascript 我想找到一种使用 Golang 获取视频第一帧的方法 首先 我将 mp4 视频文件上传到服务器 它保存在服务器上 有没有办法使用 Golang 获取该视频的
  • nsq 无法通过连接到 nsqlookupd 来消费消息

    我尝试使用 docker compose 来运行 nsq docker compose yml如下 version 3 services nsqlookupd image nsqio nsq command nsqlookupd ports
  • 如何在golang模板上打印JSON?

    我需要在客户端有一个对象 所以我使用 json marshal 将其转换为 JSON 并将其打印到模板中 该对象被打印为转义 JSON 字符串 我期待它是var arr o1 o2 但它是var arr o1 o2 我知道我可以在客户端进行
  • 在golang中获取TTFB(第一个字节的时间)值

    我正在尝试获取 TTFB 值和 Connect 值 c exec Command curl w Connect time connect TTFB time starttransfer Total time time total o dev
  • 如何在 Golang 中将 []byte XML 转换为 JSON 输出

    有没有办法在 Golang 中将 XML byte 转换为 JSON 输出 我有以下功能body is byte但我想在一些操作之后将此 XML 响应转换为 JSON 我试过了Unmarshal in xml打包没有成功 POST func
  • Go 的范围不能超过 (类型接口 {})

    我正处于尝试将我的注意力集中在 Go 上的婴儿阶段 目前 我正在模拟一个 API 请求 该请求返回包含对象数组的 JSON 格式的字符串 我试图找出迭代每个记录并访问每个字段的最合适的方法 最终 每个字段都将写入 Excel 电子表格 但现

随机推荐

  • 技术面试,如何谈薪资?

    众所周知 程序员是一个很容易出现薪资倒挂的职业 工作 3年比工作 5年薪资高的例子比比皆是 在 你手上有 offer吗 文章中 我们分析了如何巧妙地谈 offer 今天我们一起来分析如何谈薪资 顺利实现薪资倒挂 守住底线 不管是主动换工作还
  • 留给兼容安卓时间不多了!华为原生鸿蒙系统越来越近:跟iOS、安卓一样独立

    前言 据国内媒体报道称 余承东已经明确表态 华为明年将会推出鸿蒙原生应用与原生体验 HarmonyOS NEXT的产品 现在的情况就是 鸿蒙留给兼容安卓生态的时间越来越少了 而在之前已经有不少App厂商转入到他们的生态 并已经在开发相关的A
  • Docker仓库加密认证

    一 强制使用非加密访问仓库 insecure registry 实验环境 准备第二台虚拟机并配置docker服务及开启等 并把文件拷贝到第二台 记得配置好两台虚拟机仓库名的解析 配置步骤 1 配置文件使用非加密端口 vim etc dock
  • 鸿蒙开发入门:快速修复

    快速修复概述 快速修复是HarmonyOS系统提供给开发者的一种技术手段 支持开发者以远快于应用升级的方式对应用程序包进行缺陷修复 和全量应用升级软件版本相比 快速修复的主要优势在小 快和用户体验好 在较短的时间内不中断正在运行的应用的情况
  • Android神兵利器之协程和Lifecycle

    导语 一个安卓开发究竟要经历怎样的颠沛流离才终于能遇见Jetpack 遇见协程和Lifecycle 在Jetpack出现以前安卓应用架构这一块可以说非常混乱 早期没有官方架构组件 小公司可能都是mvc一把梭或者引入了简易版的mvp模式 而大
  • go-zero开发入门之网关往rpc服务传递数据1

    go zero 的网关往 rpc 服务传递数据时 可以使用 headers 但需要注意前缀规则 否则会发现数据传递不过去 或者对方取不到数据 go zero 的网关对服务的调用使用了第三方库 grpcurl 入口函数为 InvokeRPC
  • 30天精通Nodejs--第十三天:MySQL2

    目录 引言 MySQL2简介 使用说明 安装 连接到数据库 连接池 新增 查询 修改 删除
  • 鸿蒙开发入门:应用配置文件概述(一)

    应用配置文件概述 Stage模型 每个应用项目必须在项目的代码目录下加入配置文件 这些配置文件会向编译工具 操作系统和应用市场提供应用的基本信息 在基于Stage模型开发的应用项目代码下 都存在一个app json5及一个或多个module
  • 设置bat工作目录

    在执行bat脚本的时候 如果直接双击bat脚本 此时的工作路径一般为 C Users Administrator gt 很多时候需要将工作路径设置为bat脚本所在的目录 可以在bat脚本内设置当前工作路径为bat文件所在目录 cd d dp
  • 检查网络连通性的几种方法

    检查网络连通性的几种方法 检查网络连通性是确保计算机或设备能够与其他设备或互联网通信的重要步骤 以下是一些用于检查网络连通性的方法 Ping命令 在命令提示符 Windows 或终端 Linux macOS 中 使用ping命令 例如 在W
  • Java面试八股文及答案整理( 2023年 12月最新版,持续更新)

    一 Java 基础 1 JDK 和 JRE 有什么区别 JDK Java Development Kit 的简称 java 开发工具包 提供了 java 的开发环境和运行环境 JRE Java Runtime Environment 的简称
  • 深入理解 Android Activity 启动模式

    在 Android 应用开发中 Activity 是用户界面的核心组件 而 Activity 的启动模式则是决定应用界面如何在任务栈中交互 管理以及呈现的关键因素 正确的启动模式选择能够优化用户体验 提高应用性能 并确保应用在各种情景下都能
  • 各大厂为什么要适配鸿蒙?鸿蒙到底值不值得学

    各大厂为什么要适配鸿蒙 今天在脉脉上看到这个问题 有好多人在下面回复说什么不适配就不爱国之类的话 但是我们仔细想想 这些大厂资本家真的会被这些 不爱国 的舆论影响吗 这些大厂的公关花钱分分钟就能把舆论导向指向其他地方 不然为什么网上那么多黑
  • Centeos安装mysql

    安装mysql 检查MariaDB 因为这个会和MySQL有冲突 所以先检查一下是否有安装 查看mariadb rpm qa grep mariadb 卸载mariadb mariadb libs 5 5 44 2 el7 centos x
  • 规则引擎与商业CRM的完美邂逅:将智能决策融入商业扩展

    一 背景介绍 商业CRM系统的商机模块业务复杂 场景繁多 规则调整频繁 商机流转效率一定程度决定了销售开单的效率 如何高效配合产品侧完成业务规则调整 商机流转经历了硬编码到半配置化的优化升级 过程中遇到了一些问题 也总结了一些经验 今天来和
  • 程序员未来是不是会大量失业?近期分析分享

    程序员未来是不是会大量失业 是的 随着Al的发展恐怕就在这几年 市场环境堪忧 最近两年 也就是从2022年开始 经济下行和行业不景气的情况大家都有目共睹 互联网行业的就业情况越来越不容乐观 头部互联网公司 效益出现了不同程度的下滑 最明显的
  • go-zero开发入门-API服务开发示例

    接口定义 定义 API 接口文件 接口文件 add api 的内容如下 syntax v1 info title API 接口文件示例 desc 演示如何编写 API 接口文件 author 一见 date 2023年12月07日 vers
  • RPA提升电商运营效率!用rpa实现电商主图采集分析

    作为一名经验丰富的文案编辑 经常会面临着许多重复的工作 比如电商主图的采集与分析 通常需要我们手动打开每个商品链接 下载商品主图 然后将其整理成表格 进行分析 这个过程非常繁琐 不仅耗费大量的时间和精力 还容易出错 然而 自从我们开始使用八
  • unistd.h中定义的setsid()与fork()

    setsid 是一个UNIX系统调用 用于创建一个新的会话 session 并将当前进程设置为该会话的领头进程 session leader 通常情况下 setsid 函数用于创建守护进程 daemon 以确保它与任何终端分离 从而可以在后
  • go-zero开发入门之gateway深入研究1

    创建一个 gateway 示例 main go package main import flag fmt gateway middleware github com zeromicro go zero core conf github co