【GoTeams】-2:项目基础搭建(下)

news2025/3/7 5:18:36

在这里插入图片描述

本文目录

  • 1. 回顾
  • 2. Zap日志
  • 3. 配置
  • 4. 引入gprc
    • 梳理gRPC思路
    • 优雅关闭gRPC

1. 回顾

上篇文章我们进行了路由搭建,引入了redis,现在来看看对应的效果。

首先先把前端跑起来,然后点击注册获取验证码。

在这里插入图片描述

再看看控制台输出和redis是否已经有记录,验证没问题,现在redis这个环节是打通了。

在这里插入图片描述

2. Zap日志

go中原生的日志比较一般,我们可以集成一个流行的日志库进来。

这里用uber开源的zap日志库,在common路径下安装zap:go get -u go.uber.org/zap。

然后再安装一个日志分割库,go get -u github.com/natefinch/lumberjack,因为日志的存储有几种方式,比如按照日志级别将日志记录到不同的文件,按照业务来分别记录不同级别的日志,按照包结构划分记录不同级别日志。debug级别以上记录一个,info以上记录一个,warn以上记录一个

common路径下创建logs.go,然后编写对应的代码。

package logs

import (
	"github.com/gin-gonic/gin"
	"github.com/natefinch/lumberjack"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"net"
	"net/http"
	"net/http/httputil"
	"os"
	"runtime/debug"
	"strings"
	"time"
)

var lg *zap.Logger

type LogConfig struct {
	DebugFileName string `json:"debugFileName"`
	InfoFileName  string `json:"infoFileName"`
	WarnFileName  string `json:"warnFileName"`
	MaxSize       int    `json:"maxsize"`
	MaxAge        int    `json:"max_age"`
	MaxBackups    int    `json:"max_backups"`
}

// InitLogger 初始化Logger
func InitLogger(cfg *LogConfig) (err error) {
	writeSyncerDebug := getLogWriter(cfg.DebugFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
	writeSyncerInfo := getLogWriter(cfg.InfoFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
	writeSyncerWarn := getLogWriter(cfg.WarnFileName, cfg.MaxSize, cfg.MaxBackups, cfg.MaxAge)
	encoder := getEncoder()
	//文件输出
	debugCore := zapcore.NewCore(encoder, writeSyncerDebug, zapcore.DebugLevel)
	infoCore := zapcore.NewCore(encoder, writeSyncerInfo, zapcore.InfoLevel)
	warnCore := zapcore.NewCore(encoder, writeSyncerWarn, zapcore.WarnLevel)
	//标准输出
	consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
	std := zapcore.NewCore(consoleEncoder, zapcore.Lock(os.Stdout), zapcore.DebugLevel)
	core := zapcore.NewTee(debugCore, infoCore, warnCore, std)
	lg = zap.New(core, zap.AddCaller())
	zap.ReplaceGlobals(lg) // 替换zap包中全局的logger实例,后续在其他包中只需使用zap.L()调用即可
	return
}

func getEncoder() zapcore.Encoder {
	encoderConfig := zap.NewProductionEncoderConfig()
	encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	encoderConfig.TimeKey = "time"
	encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
	encoderConfig.EncodeDuration = zapcore.SecondsDurationEncoder
	encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder
	return zapcore.NewJSONEncoder(encoderConfig)
}

func getLogWriter(filename string, maxSize, maxBackup, maxAge int) zapcore.WriteSyncer {
	lumberJackLogger := &lumberjack.Logger{
		Filename:   filename,
		MaxSize:    maxSize,
		MaxBackups: maxBackup,
		MaxAge:     maxAge,
	}
	return zapcore.AddSync(lumberJackLogger)
}

// GinLogger 接收gin框架默认的日志
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next()

		cost := time.Since(start)
		lg.Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost),
		)
	}
}

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					lg.Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					lg.Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}

然后在main.go中来初始化我们的日志。

在这里插入图片描述
然后可以把对应的log地方进行更改了,比如下面的地方。
在这里插入图片描述
然后来验证一下是否能正常生成日志文件,正常生成了,没问题。
正常生成了

3. 配置

日志我们用了zap做集成,算是一个改进,但是配置比较复杂, 所以我们这里需要进一步优化这个配置。

配置我们引入viper进行操作,也非常简单,直接上图和代码吧,在user里边装viper这个包。

go get github.com/spf13/viper

首先在user下面创建cofig目录,然后创建config.yaml配置文件,然后创建config.go代码读取配置。

在这里插入图片描述

config.go的代码如下所示。

package config

import (
	"github.com/go-redis/redis/v8"
	"github.com/spf13/viper"
	"log"
	"os"
	"test.com/project-common/logs"
)

var C = InitConfig()

type Config struct {
	viper *viper.Viper
	SC    *ServerConfig
}

type ServerConfig struct {
	Name string
	Addr string
}

func InitConfig() *Config {
	conf := &Config{viper: viper.New()}
	workDir, _ := os.Getwd()
	conf.viper.SetConfigName("config")
	conf.viper.SetConfigType("yaml")
	conf.viper.AddConfigPath(workDir + "/config")
	conf.viper.AddConfigPath("etc/msproject/user")

	//读取config
	err := conf.viper.ReadInConfig()
	if err != nil {
		log.Fatalln(err)
	}
	conf.ReadServerConfig()
	conf.InitZapLog() //初始化zap日志
	return conf
}

func (c *Config) InitZapLog() {
	//从配置中读取日志配置,初始化日志
	lc := &logs.LogConfig{
		DebugFileName: c.viper.GetString("zap.debugFileName"),
		InfoFileName:  c.viper.GetString("zap.infoFileName"),
		WarnFileName:  c.viper.GetString("zap.warnFileName"),
		MaxSize:       c.viper.GetInt("maxSize"),
		MaxAge:        c.viper.GetInt("maxAge"),
		MaxBackups:    c.viper.GetInt("maxBackups"),
	}
	err := logs.InitLogger(lc)
	if err != nil {
		log.Fatalln(err)
	}
}

func (c *Config) ReadServerConfig() {
	sc := &ServerConfig{}
	sc.Name = c.viper.GetString("server.name")
	sc.Addr = c.viper.GetString("server.addr")
	c.SC = sc
}

// 读redis的配置
func (c *Config) ReadRedisConfig() *redis.Options {
	return &redis.Options{
		Addr:     c.viper.GetString("redis.host") + ":" + c.viper.GetString("redis.port"),
		Password: c.viper.GetString("redis.password"), // no password set
		DB:       c.viper.GetInt("db"),                // use default DB
	}
}

对应的redis.go中原本new一个redis客户端的代码也需要更改了,改为已有的读取配置的函数 ReadRedisConfig()

在这里插入图片描述
并且把原来main.go中关于zap的相关配置文件删除即可。
在这里插入图片描述
然后重新启动下,看看是否能够运行,ok,启动没问题。

在这里插入图片描述

4. 引入gprc

可以通过引入一个API把对应的服务连起来,可以把各种服务提出来,然后通过API进行定义。

在这里插入图片描述
api\proto下新建一个名为login.service.proto的文件,然后编写代码。

syntax = "proto3";
package login.service.v1;
option go_package = "project-user/pkg/service/login.service.v1";


message CaptchaMessage {
  string mobile = 1;
}
message CaptchaResponse{
}
service LoginService {
  rpc GetCaptcha(CaptchaMessage) returns (CaptchaResponse) {}
}

然后在proto路径下,运行命令:protoc --go_out=./gen --go_opt=paths=source_relative --go-grpc_out=./gen --go-grpc_opt=paths=source_relative login_service.proto,就可以生成对应文件了。

因为是第一版,所以我们先在gen下生成,然后复制移动到service下面,防止后面不断根据功能进行修改,而导致新生成的被覆盖。

那么我们来看看这login-service_grpc.pb.go文件到底生成了什么东西。

在这里插入图片描述


LoginServiceClient 是一个接口,定义了客户端可以调用的 GetCaptcha 方法。该方法接收一个 CaptchaMessage 请求,返回一个 CaptchaResponse 响应。

loginServiceClientLoginServiceClient 的具体实现,通过 NewLoginServiceClient 函数创建。它使用 grpc.ClientConnInterface 来发起 RPC 调用。

loginServiceClient.GetCaptcha 方法中,通过 c.cc.Invoke 发起 GetCaptcha 方法的 RPC 调用。它将请求数据序列化并发送到服务器,然后等待响应。

在这里插入图片描述

LoginServiceServer 是服务器端的接口,定义了 GetCaptcha 方法。所有实现该接口的服务器端逻辑必须嵌入 UnimplementedLoginServiceServer,以确保向前兼容性。
UnimplementedLoginServiceServer 提供了一个默认的未实现方法的错误响应,返回 codes.Unimplemented 状态码。

在这里插入图片描述

也就屙是说,接口还包含一个方法 mustEmbedUnimplementedLoginServiceServer(),这是一个空方法,用于确保实现者嵌入了 UnimplementedLoginServiceServer

这是 UnimplementedLoginServiceServerGetCaptcha 方法的默认实现。它返回 nil 作为响应,并通过 status.Errorf 返回一个带有 codes.Unimplemented 状态码的错误,表明该方法未被实现。这种设计确保了即使服务实现者没有实现某些方法,调用这些方法时也不会导致程序崩溃,而是返回一个明确的错误。

mustEmbedUnimplementedLoginServiceServer() 是一个空方法,用于确保服务实现者嵌入了 UnimplementedLoginServiceServer

testEmbeddedByValue() 是一个辅助方法,用于在运行时检查 UnimplementedLoginServiceServer 是否被正确嵌入(通过值而不是指针)。这避免了在方法调用时出现空指针引用。

type LoginServiceServer interface {
	GetCaptcha(context.Context, *CaptchaMessage) (*CaptchaResponse, error)
	mustEmbedUnimplementedLoginServiceServer()
}

// UnimplementedLoginServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedLoginServiceServer struct{}

func (UnimplementedLoginServiceServer) GetCaptcha(context.Context, *CaptchaMessage) (*CaptchaResponse, error) {
	return nil, status.Errorf(codes.Unimplemented, "method GetCaptcha not implemented")
}
func (UnimplementedLoginServiceServer) mustEmbedUnimplementedLoginServiceServer() {}
func (UnimplementedLoginServiceServer) testEmbeddedByValue()                      {}

所以主要是为了,确保向前兼容性:通过嵌入 UnimplementedLoginServiceServer,服务实现者可以在未来版本中添加新方法,而不会破坏现有实现。

梳理gRPC思路

首先我们实现了gRPC,那么原本的api下面的user相关的我们可以删除了。

来看看我们实现了什么。

首先main.go中的相关代码如下。

在这里插入图片描述
然后在service中我们实现了login_service.go代码,如下。

package login_service_v1

import (
	"context"
	"errors"
	"fmt"
	"go.uber.org/zap"
	"log"
	common "test.com/project-common"
	"test.com/project-common/logs"
	"test.com/project-user/pkg/dao"
	"test.com/project-user/pkg/repo"
	"time"
)

type LoginService struct {
	UnimplementedLoginServiceServer
	cache repo.Cache
}

func New() *LoginService {
	return &LoginService{
		cache: dao.Rc,
	}
}

func (ls LoginService) GetCaptcha(ctx context.Context, msg *CaptchaMessage) (*CaptchaResponse, error) {

	//1.获取参数
	moblie := msg.Mobile
	fmt.Println(moblie)
	//2.校验参数
	if !common.VerifyMoblie(moblie) {
		return nil, errors.New("手机号不合法")
	}
	//3.生成验证码(随机4位或者6位)
	code := "123456"
	//4.调用短信平台(三方,放入go协程中执行,接口可以快速响应,短信几秒到无所谓)
	go func() {
		time.Sleep(1 * time.Second)

		zap.L().Info("短信平台调用成功,发送短信 INFO")
		logs.LG.Debug("短信平台调用成功,发送短信 debug")
		zap.L().Error("短信平台调用成功,发送短信 error")

		// redis 假设后续缓存在mysql或者mongo当中,也有可能存储在别的当中
		// 所以考虑用接口实现,面向接口编程“低耦合,高内聚“
		// 5.存储验证码redis,设置过期时间15分钟即可
		c, cancel := context.WithTimeout(context.Background(), 2*time.Second)
		defer cancel()
		err := ls.cache.Put(c, "REGISTER_"+moblie, code, 15*time.Minute)
		if err != nil {
			log.Printf("验证码存入redis出错,causer by :%v\n", err)
		}
		log.Printf("将手机号和验证码存入redis成功:REGISTER %s : %s", moblie, code)
	}()
	return &CaptchaResponse{}, nil
}

并且在router中更新了如下代码。

package router

import (
	"github.com/gin-gonic/gin"
	"google.golang.org/grpc"
	"log"
	"net"
	"test.com/project-user/config"
	loginServiceV1 "test.com/project-user/pkg/service/login.service.v1"
)

// Router 接口
type Router interface {
	Route(r *gin.Engine)
}

type RegisterRouter struct {
}

func New() *RegisterRouter {
	return &RegisterRouter{}
}

func (*RegisterRouter) Route(ro Router, r *gin.Engine) {
	ro.Route(r)
}

var routers []Router

func InitRouter(r *gin.Engine) {
	for _, ro := range routers {
		ro.Route(r)
	}
}

type gRPCConfig struct {
	Addr         string
	RegisterFunc func(*grpc.Server)
}

func RegisterGrpc() *grpc.Server {
	c := gRPCConfig{
		Addr: config.C.GC.Addr,
		RegisterFunc: func(g *grpc.Server) {
			//注册
			loginServiceV1.RegisterLoginServiceServer(g, loginServiceV1.New())
		}}
	s := grpc.NewServer()
	c.RegisterFunc(s)
	lis, err := net.Listen("tcp", c.Addr)
	if err != nil {
		log.Println("cannot listen")
	}
	//把服务放到协程里边
	go func() {
		err = s.Serve(lis)
		if err != nil {
			log.Println("server started error", err)
			return
		}
	}()
	return s
}

在这里插入图片描述

好,有点复杂,这里我们画图梳理下关系。

首先在router.go文件中,我们声明了RegisterGrpc(),这是gRPC服务的入口点,主要是配置grpc配置,包括服务地址还有注册函数,并且创建gRPC实例,然后注册登录服务,最后是启动gRPC服务器(在goroutine中运行的。)

login_service.go 中:LoginService 结构体:实现了 gRPC 服务接口,New() 函数:创建 LoginService 实例,GetCaptcha() 方法:实现具体的验证码获取业务逻辑。

所以调用关系如下。
在这里插入图片描述
在这里插入图片描述
所以为什么要使用协程?因为如果不使用协程,s.Serve(lis)会阻塞主线程,导致后续代码无法继续运行,这样可以运行gRPC服务器与HTTP服务器(gin)同时运行。

gRPC可以独立运行,不影响主程序的其他功能。


优雅关闭gRPC

在main函数中,还有个stop,这是闭包函数,说实话这是第一次看到闭包函数的使用场景,首先我们捕获了外部变量gc,gc也就是gRPC服务器实例,然后定义了服务关闭的具体行为,也就是停止gRPC服务,作为参数传给srv.Run。

当Run函数接受一个stop函数作为参数,注释一种依赖注入的设计模式,当收到指令之后,会把gRPC给关闭了。

虽然 stop 函数被传入,但它并不会立即执行,代码会在 <-quit 这行被阻塞。只有当程序收到 SIGINT 或 SIGTERM 信号时(比如按 Ctrl+C),才会继续往下执行,然后才会检查 stop != nil 并执行 stop 函数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2310872.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

02-双指针-A-B 数对

题目 链接&#xff1a;P1102 A-B 数对 - 洛谷 思路 问题场景想象 我们可以把这个问题想象成在一个排队的队伍里找符合特定身高差的人对。给定的数列里的每个数就好比队伍里每个人的身高&#xff0c;而差值 C 就是我们要找的身高差。我们的目标是找出队伍里所有身高差恰好是 …

2025年Cursor最新安装使用教程

Cursor安装教程 一、Cursor下载二、Cursor安装三、Cursor编辑器快捷键(1) 基础编辑快捷键(2) 导航快捷键(3) 其他常用快捷键 一、Cursor下载 Cursor官方网站&#xff08;https://www.cursor.com/ &#xff09; 根据自己电脑操作系统选择对应安装包 二、Cursor安装 下载完成后…

快速部署:在虚拟机上安装 CentOS 7 的详细步骤

CentOS是一个开源的基于Red Hat Enterprise Linux (RHEL) 的Linux发行版&#xff0c;它的主要目的是提供一个与RHEL相似的操作系统但不包含RHEL的商业支持和服务&#xff0c;完全免费。主要面向那些希望在企业环境中使用稳定、可靠的Linux系统但又不想支付RHEL许可证费用的用户…

【有啥问啥】深入浅出:大模型应用工具 Ollama 技术详解

深入浅出&#xff1a;大模型应用工具 Ollama 技术详解 引言 近年来&#xff0c;大型模型&#xff08;Large Models&#xff0c;LLMs&#xff09;技术突飞猛进&#xff0c;在自然语言处理、计算机视觉、语音识别等领域展现出强大的能力。然而&#xff0c;部署和运行这些庞大的…

利用opencv_python(pdf2image、poppler)将pdf每页转为图片

1、安装依赖pdf2image pip install pdf2image 运行.py报错&#xff0c;因为缺少了poppler支持。 2、安装pdf2image的依赖poppler 以上命令直接报错。 改为手工下载&#xff1a; github: Releases oschwartz10612/poppler-windows GitHub 百度网盘&#xff1a; 百度网盘…

大数据测试总结

总结测试要点&#xff1a; 参考产品文档&#xff0c;技术文档梳理以下内容 需求来源 业务方应用场景 数据源&#xff0c;数据格转&#xff0c;数据产出&#xff0c;数据呈现方式&#xff08;数据消亡史&#xff09;&#xff0c;数据量级&#xff08;增量&#xff0c;全量&am…

Redis面试常见问题——集群方案

Redis集群方案 在Redis中提供的集群方案总共有三种 主从复制 哨兵模式 分片集群 主从复制 单节点Redis的并发能力是有上限的&#xff0c;要进一步提高Redis的并发能力&#xff0c;就需要搭建主从集群&#xff0c;实现读写分离。 主从数据同步原理 单节点Redis的并发能力是有…

Qt:day4

一、作业 1&#xff1a;实现绘图的时候&#xff0c;颜色的随时调整&#xff1b; 2&#xff1a;追加橡皮擦功能&#xff1b; 3&#xff1a;配合键盘事件&#xff0c;实现功能&#xff1b; 当键盘按 ctrlz 的时候&#xff0c;撤销最后一次绘图。 【Headers / widget.h】&#xff…

vue3之echarts仪表盘

vue3之echarts仪表盘 效果如下&#xff1a; 版本 "echarts": "^5.5.1" 核心代码&#xff1a; <template><div ref"chartRef" class"circle"></div> </template> <script lang"ts" setup>…

将PDF转为Word的在线工具

参考视频&#xff1a;外文翻译 文章目录 一、迅捷PDF转换器二、Smallpdf 一、迅捷PDF转换器 二、Smallpdf

MWC 2025|紫光展锐联手美格智能发布5G通信模组SRM812

在2025年世界移动通信大会&#xff08;MWC 2025&#xff09;期间&#xff0c;紫光展锐携手美格智能正式推出了基于紫光展锐V620平台的第二代5G Sub6G R16模组SRM812&#xff0c;以超高性价比方案&#xff0c;全面赋能合作伙伴&#xff0c;加速5G规模化应用在各垂直领域的全面落…

前端基础之ajax

vue-cli配置代理服务器解决跨域问题 我们可以使用一个代理服务器8080&#xff0c;Vue项目8080发送请求向代理服务器8080发送请求&#xff0c;再由在理服务器转发给后端服务器 首先需要在vue.config.js中配置代理服务器 const { defineConfig } require(vue/cli-service) modul…

【无标题】FrmImport

文章目录 前言一、问题描述二、解决方案三、软件开发&#xff08;源码&#xff09;四、项目展示五、资源链接 前言 我能抽象出整个世界&#xff0c;但是我不能抽象你。 想让你成为私有常量&#xff0c;这样外部函数就无法访问你。 又想让你成为全局常量&#xff0c;这样在我的…

IP-Guard软件设置P2P升级功能

日常使用IP-Guard软件遇到客户端升级&#xff0c;需要从服务器下载升级包&#xff0c;为了让快速升级&#xff0c;可以配置参数&#xff0c;具体设置见下图&#xff1a; 控制台—策略—定制配置—新增 关键字&#xff1a;obt_dislble_p2p2 内容&#xff1a;2

【Mac】git使用再学习

目录 前言 如何使用github建立自己的代码库 第一步&#xff1a;建立本地git与远程github的联系 生成密钥 将密钥加入github 第二步&#xff1a;创建github仓库并clone到本地 第三步&#xff1a;上传文件 常见的git命令 git commit git branch git merge/git rebase …

java后端开发day27--常用API(二)正则表达式爬虫

&#xff08;以下内容全部来自上述课程&#xff09; 1.正则表达式&#xff08;regex&#xff09; 可以校验字符串是否满足一定的规则&#xff0c;并用来校验数据格式的合法性。 1.作用 校验字符串是否满足规则在一段文本中查找满足要求的内容 2.内容定义 ps&#xff1a;一…

【TCP/IP协议栈】【传输层】端口号、套接字、多路复用/分解、网络字节序

参考资料&#xff1a; 前言&#xff1a; 总结&#xff1a; 【计算机网络】套接字&#xff08;应用层和传输层之间的接口&#xff09; 套接字是一个通用的通信接口抽象不仅限于TCP/IP协议族作为应用层和传输层之间的桥梁支持多种通信方式和协议族 套接字定义 在 TCP 或者 UDP…

【漫话机器学习系列】120.参数化建模(Parametric Modeling)

参数化建模&#xff08;Parametric Modeling&#xff09;详解 1. 引言 在数据建模和机器学习中&#xff0c;参数化建模&#xff08;Parametric Modeling&#xff09;是一种广泛应用的建模方法。它通过假设一个函数形式来表达变量之间的关系&#xff0c;并估算该函数的参数&am…

Web3 的未来:去中心化如何重塑互联网

Web3 的未来&#xff1a;去中心化如何重塑互联网 在这个信息爆炸的时代&#xff0c;我们正站在一个新的技术革命的门槛上——Web3。Web3 不仅仅是一个技术术语&#xff0c;它代表了一种全新的互联网理念&#xff0c;即去中心化。这种理念正在逐步改变我们对互联网的使用方式和…

DApp开发从入门到精通:以太坊/Solana公链生态实战解析

在区块链技术的推动下&#xff0c;去中心化应用&#xff08;DApp&#xff09;逐渐摆脱传统中心化后台的依赖&#xff0c;转向以智能合约为核心的全合约化开发模式。这种模式通过区块链网络的分布式特性&#xff0c;实现了数据存储、业务逻辑与用户交互的完全去中心化。 一、全合…