结合商业项目深入理解Go知识点

news2024/11/17 9:33:18

这篇文章比较硬核,爆肝5千字,把之前整理的知识点都串起来了。建议先收藏,慢慢看。

前言

上一篇文章 #【Go WEB进阶实战】开源的电商前后台API系统 很受大家欢迎,有好多小伙伴私信我问题:“gtoken真不错,能不能再多讲讲?”、“接口怎么设计Cache好?”、“代码规范讲一下吧”、“这个系统有没有前端页面?”,等等…

那我就再写一篇作为补充喽,小伙伴们还有什么问题欢迎在评论区留言。

之前整理过一篇可能是全网最用心的「Go学习建议和资料汇总」,也深受大家好评,大家可以先收藏慢慢学。

这篇文章更进一步,会结合电商前后台API系统,把Go语言的知识点应用到商业项目中,让大家结合实际的场景去理解,这样应该对大家更有帮助!

小提示:这篇文章的重点不是把各个知识点讲透,而是为了让大家理解各个知识点在商业项目中的应用。如果你的基础比较薄弱,每个知识点的最后也都附上了详解链接,方便大家去查漏补缺。

下面就开始和我进阶实战吧:

登录鉴权

我们在上一篇文章中有介绍,系统的登录鉴权是通过gtoken实现的,有的小伙伴没有搞清楚登录信息存储在哪里?我们是如何获得当前登录用户的信息?

首先gtoken的数据默认使用内存缓存gcache,这种缓存会随着服务的终止而销毁,当重启服务时,之前缓存的数据就丢失了;gtoken也支持使用redis,比如我们的项目中就是使用了gredis,将登录信息存储在redis中进行管理。

更多关于gtoken的知识点可以看这篇专题文章:# 通过阅读源码解决项目难题:GToken替换JWT实现SSO单点登录

如果你基础比较弱的话,我还录制了视频教程:# 【视频】登录鉴权的三种方式:token、jwt、session实战分享

下面聊聊如何获得登录用户信息的问题:

我们使用Go语言无论开发http项目还是rpc项目,上下文都是很重要的概念,用于共享变量和链路跟踪

我们通过Context上下文对象在一次请求中设置用户信息,共享变量,进而实现在后续链路中都能获得当前登录用户的信息:

Context上下文

以修改密码举例:

我们通过ghttp.Request的实例r,调用GetCtxVar() 方法。
比如:r.GetCtxVar(middleware.CtxAccountId),通过这种方式我们就可以获得登录用户信息了

小提示:为了行文清晰,让大家更直观的看到和知识点相关的代码,不重要的代码会用三个竖着的.省略。完整的代码可以fork文末的GitHub,已把这个项目开源。

调用示例代码

func (s *rotationService) UpdateMyPassword(r *ghttp.Request, req *UpdateMyPasswordReq) (res sql.Result, err error) {
   .
   .
   .
   //获得当前登录用户
   req.Id = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
   ctx := r.GetCtx()
   res, err = dao.AdminInfo.Ctx(ctx).WherePri(req.Id).Update(req)
   if err != nil {
      return nil, err
   }
   return
}

赋值示例代码

赋值的核心代码也很简单,就是通过 r.SetCtxVar(key, value) 方法,就能把变量赋值到context中了

package middleware

import (
   "github.com/goflyfox/gtoken/gtoken"
   "github.com/gogf/gf/net/ghttp"
   "github.com/gogf/gf/util/gconv"
   "malu/library/response"
)

const (
   CtxAccountId      = "account_id"       //token获取
   .
   .
   .
)

type TokenInfo struct {
   Id      int
    .
    .
    .
}

var GToken *gtoken.GfToken

var MiddlewareGToken = tokenMiddleware{}

type tokenMiddleware struct{}

func (s *tokenMiddleware) GetToken(r *ghttp.Request) {
   var tokenInfo TokenInfo
   token := GToken.GetTokenData(r)
   err := gconv.Struct(token.GetString("data"), &tokenInfo)
   if err != nil {
      response.Auth(r)
      return
   }
   
   r.SetCtxVar(CtxAccountId, tokenInfo.Id)
    .
    .
    .
   r.Middleware.Next()
}

小技巧

  1. 在架构设计中,在哪个场景下设置Context是非常重要的:上下文的变量必须在请求一开始便注入到请求流程中,以便于其他方法调用,所以我们在中间件中来实现是比较优雅的选择
  2. 结合实际场景,我们设置到Context中的变量可以是指针类型,因为任何地方获取到这个指针,不仅可以获取到里面的数据,而且能够直接修改里面的数据
  3. 建议养成好习惯:在service层的方法中,第一个参数必传context.Context对象或者*ghttp.Request对象。这样有利于我们后续扩展,能够方便的通过context共享数据,而且还能进行链路追踪

更详细的介绍看这里:# GoFrame 如何优雅的共享变量 | Context的使用

接口缓存

关于接口缓存,有小伙伴提出这样的疑问?

读者提问

当然要设计接口数据缓存了,而且在GoFrame中还有比较优雅的实践方式:链式操作设置缓存。

我们给查询接口添加缓存的思路是这样的:

常规操作

  1. 定义缓存key
  2. 根据缓存key查询是否有值
    • 有值返回缓存中的值,不查询DB
    • 无值,查询DB,写入缓存
  3. 返回数据
func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) {
   cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
   res := Cache::get(cacheKey)
   if(!res){
       err = dao.ArticleInfo.Ctx(r.GetCtx()).WherePri(req.Id).Scan(&res)
       if err != nil {
          return res, err
       }
       Cache::set(cacheKey,res,time.Hour)   
   }
   return
}

GoFrame为我们提供了非常优雅的链接操作:

链式操作

我们只需要在链式查询中使用Cache()方法,设置缓存时间和缓存key就可以了,GoFrame为我们实现了上述常规操作中的繁琐操作:

链式操作:取值

func (s *rotationService) Detail(r *ghttp.Request, req *DetailReq) (res model.ArticleInfo, err error) {
   //查询时优先查询缓存
   cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
   err = dao.ArticleInfo.Ctx(r.GetCtx()).Cache(time.Hour, cacheKey).WherePri(req.Id).Scan(&res)
   if err != nil {
      return res, err
   }
   return
}

链式操作:更新值

更新操作只需要将Cache()方法的第一个参数过期时间设置为负数,就会清空缓存

func (s *rotationService) Update(r *ghttp.Request, req *UpdateArticleReq) (res sql.Result, err error) {
   ctx := r.GetCtx()
    .
    .
    .
   //更新缓存
   cacheKey := ArticleDetailCacheKey + gconv.String(req.Id)
   res, err = dao.ArticleInfo.Ctx(ctx).Cache(-1, cacheKey).WherePri(req.Id).Update(req)
   if err != nil {
      return nil, err
   }
   return
}

除了这个典型的场景,我们项目的热门商品是通过LRU缓存淘汰策略实现的,小伙伴们可以看这篇详解一探究竟:# GoFrame gcache使用实践 | 缓存控制 淘汰策略

接口兼容处理

需求场景

我们电商系统的文章和商品都支持收藏和取消收藏

取消收藏有2种情况:一种是根据收藏id
删除;另一种是根据收藏类型和文章id(或者商品id)删除

思考题

我们根据上述的需求是设计两个接口分别实现呢?还是只设计一个接口兼容实现呢?

我倾向于只使用一种接口,兼容实现:这样不仅减少代码量,而且后期有逻辑调整时,只修改一处代码就可以了。

看下我们是如何实现的:

结构体

首先定义我们的请求结构体,允许通过收藏id删除;
或者根据类型和对象id删除(收藏类型:1商品 2文章)

type DeleteReq struct {
   Id       int `json:"id"`
   Type     int `json:"type"`
   ObjectId int `json:"object_id"`
}

api层

然后我们编写api层,这部分代码很简单,所有的api层代码都是这种规范

  1. 定义请求参数结构体
  2. 解析请求参数,做数据校验,有问题直接返回错误;正常则继续向下执行
  3. 调用service层对应的方法,传入上下文context和请求体
  4. 根据service层的返回结果决定是返回错误码,还是返回数据。

小技巧:所有的api层都是这样的思路,我们的逻辑处理一般写在service中

func (*collectionApi) Delete(r *ghttp.Request) {
   var req *DeleteReq
   if err := r.Parse(&req); err != nil {
      response.ParamErr(r, err)
   }
   
   if res, err := service.Delete(r.Context(), req); err != nil {
      response.Code(r, err)
   } else {
      response.SuccessWithData(r, res)
   }
}

service层

最后我们编写service层代码,实现取消收藏接口兼容的重点也在这里了

我们根据传入的id做判断,如果id不为0,根据收藏id删除;否则的话就根据传入的type类型区别是文章还是商品,根据ObjectId确定要删除对象的id。

func (s *collectionService) Delete(ctx context.Context, req *DeleteReq) (res sql.Result, err error) {
   if req.Id != 0 {
      //根据收藏id删除
      res, err = dao.CollectionInfo.Ctx(ctx).WherePri(req.Id).Delete()
   } else {
      //根据类型和对象id删除
      res, err = dao.CollectionInfo.Ctx(ctx).
         Where(dao.CollectionInfo.Columns.Type, req.Type).
         Where(dao.CollectionInfo.Columns.ObjectId, req.ObjectId).
         Delete()
   }
   if err != nil {
      return nil, err
   }
   return
}

小技巧:我们查询条件的字段都是通过这种方式取值的:dao.CollectionInfo.Columns.Type,而不会写死字符串type,原因是如果我们的字段有修改,前者这种写法可以一改全改;而后者写死字符串的方式很难找全要修改的地方,维护成本比较高。

统计查询

咱们想一个复杂点的场景,进阶实战一下GoFrame ORM的使用:

我们需要查询最近7天每天的订单量,如果当天没有订单就返回0。期望的数据结构是这样的:

"order_total": [10, 0, 10, 20, 10, 0, 7],

我们如何实现呢?

service层

重点看这段查询语句

err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(id) total,date_format(created_at, '%Y-%m-%d') today").Group("today").Scan(&TodayTotals)

在GoFrame中 where的第二个参数如果传数组,默认就是where in查询;

我们在Fields()方法中除了可以指定查询字段,还可以使用查询函数,也可以指定别名:

func OrderTotal(ctx context.Context) (counts []int) {
   counts = []int{0, 0, 0, 0, 0, 0, 0}
   recent7Dates := shared.GetRecent7Date()
   TodayTotals := []TodayTotal{}
   //只取最近7天
   err := dao.OrderInfo.Ctx(ctx).Where(dao.OrderInfo.Columns.CreatedAt+" >= ", shared.GetBefore7Date()).Fields("count(*) total,date_format(created_at, '%Y-%m-%d') today").Group("today").Scan(&TodayTotals)
   fmt.Printf("result:%v", TodayTotals)
   for i, date := range recent7Dates {
      for _, todayTotal := range TodayTotals {
         if date == todayTotal.Today {
            counts[i] = todayTotal.Total
         }
      }
   }
   if err != nil {
      return counts
   }
   return
}

工具类

受某位知乎大神的启发,生成最近一周的日期我是这么实现的:

从性能角度考虑可能不是最优写法,但是理解成本肯定非常低:

//生成最近一周的日期
func GetRecent7Date() (dates []string) {
   gt := gtime.New(time.Now())
   dates = []string{
      gt.Format("Y-m-d"), //今天
      gt.Add(-gtime.D * 1).Format("Y-m-d"), //1天前
      gt.Add(-gtime.D * 2).Format("Y-m-d"),
      gt.Add(-gtime.D * 3).Format("Y-m-d"),
      gt.Add(-gtime.D * 4).Format("Y-m-d"),
      gt.Add(-gtime.D * 5).Format("Y-m-d"),
      gt.Add(-gtime.D * 6).Format("Y-m-d"), //6天前
   }
   return
}

事务处理

事务的应用场景很清晰:当我们提供的某个服务,需要操作多次DB,并且这些操作要具有原子性,要么都成功,要么都失败。这种情况就需要事务处理。

事务处理的特点是:只要其中有一个环节失败了,之前成功的DB操作也会回滚到之前的状态。

事务处理实战

比如我们创建订单时就需要做事务处理,我们一个订单可以添加多个商品,创建订单时除了添加主订单表,也会添加商品订单表。

GoFrame的事务处理非常简单:

  1. 只需要我们通过g.DB().Begin()开启事务
  2. 在链式操作中通过.TX(tx)方法添加事务
  3. 在最后判断是否有错误发生,有错误则通过Rollback()回滚事务,没错误则通过Commit()方法提交事务。
func (s *orderService) Add(r *ghttp.Request, req *AddOrderReq) (res sql.Result, err error) {
   req.OrderInfo.UserId = gconv.Int(r.GetCtxVar(middleware.CtxAccountId))
   req.OrderInfo.Number = shared.GetOrderNum()

   tx, err := g.DB().Begin()
   if err != nil {
      return nil, errors.New("启动事务失败")
   }

   //defer方法最后执行 如果有报错则回滚 如果没有报错,则提交事务
   defer func() {
      if err != nil {
         tx.Rollback()
      } else {
         tx.Commit()
      }
   }()

   //生成主订单
   lastInsertId, err := dao.OrderInfo.Ctx(r.GetCtx()).TX(tx).InsertAndGetId(req.OrderInfo)
   if err != nil {
      return nil, err
   }
   //生成商品订单
   for _, info := range req.OrderGoodsInfos {
      info.OrderId = gconv.Int(lastInsertId)
      _, err := dao.OrderGoodsInfo.Ctx(r.GetCtx()).TX(tx).Insert(info)
      if err != nil {
         return nil, err
      }
   }
   return
}

更多关于事务的知识点可以阅读这篇文章:
# Go语言中比较优雅的写法

灵活应用

需求

我们需要根据多个查询条件去查询商品,比如根据商品名称和商品分类去查询。

需求分析

我们来分析一下,客户端会有几种查询场景?

  1. 根据商品名称和商品分类2个条件查询
  2. 只根据商品名称查询
  3. 只根据商品分类查询
  4. 都没有传值,不命中查询条件,返回全部商品

常规实现

做了需求分析之后,正常的思路就是写if…else…判断了:

whereCondition := gmap.New()
if req.Keyword != "" && req.CategoryId != 0 {
   whereCondition = g.Map{
      "name like": "%" + req.Keyword + "%",
      "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
   }
} else if req.Keyword != "" {
   whereCondition = g.Map{
      "name like": "%" + req.Keyword + "%",
   }
} else if req.CategoryId != 0 {
   whereCondition = g.Map{
      "level1_category_id =? OR level2_category_id =? OR level3_category_id =? ": g.Slice{req.CategoryId, req.CategoryId, req.CategoryId},
   }
} else {
   whereCondition = g.Map{}
}

但是这种写法太乱了,而且不容易扩展:如果我们再加一个查询条件,不仅要新增一个else,就要改已经存在的if…else判断,后面维护起来简直是噩梦啊。

优化之后

在经过思考之后,使用map的set方法灵活赋值是个很好的选择,优化后的代码如下:

whereCondition := gmap.New()
if req.Keyword != "" {
   whereCondition.Set(dao.GoodsInfo.Columns.Name+" like ", "%"+req.Keyword+"%")
}
if req.CategoryId != 0 {
   whereCondition.Set("level1_category_id =? OR level2_category_id =? OR level3_category_id =? ", g.Slice{req.CategoryId, req.CategoryId, req.CategoryId})
}

优化后的代码异常清晰,如果我们再加新的查询条件,只需要在代码中再加一个if判断就可以了。

我的感悟

我在学习map基础用法的时候,并不能想到这种应用场景,这是很正常的。只有当真正开发商业项目,在具体需求的驱动之下,督促我们做优化,这时候会刺激我们回顾之前学到的知识点。结合实际需求帮助大家将之前学到的Go知识灵活运用,这是我开源这个项目的目的,也是我写这篇文章的目的。

了解更多set知识

# GoFrame gset使用技巧总结 | 天然支持排序和有序遍历、出栈、子集判断、序列化、遍历修改

好了,扩展的知识点就聊到这里,下面是对你学Go有帮助的学习资料,欢迎和我一起学习Go,实践Go。

GitHub

本项目的GitHub地址,欢迎star、fork、复刻:

电商实战项目V1版本

电商实战项目V2版本

一起进步 抱团取暖

就业环境不好,就业压力大,除了提高自己更应该抱团取暖,互相帮助:

点这里—>加入就业互助交流群👏👏👏

最后

最后,如果你觉得这期内容不错的话,一定要三连支持一波。

下方有我的公众号卡片,欢迎扫码关注,领取免费学习资料。

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

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

相关文章

【SpringBoot应用篇】SpringBoot使用Aspect AOP注解实现日志管理(增强版)

【SpringBoot应用篇】SpringBoot使用Aspect AOP注解实现日志管理(增强版)pomLog实体类OperateLogOrderGoodLogAspect转换器ConvertGoodConvertOrderConvertAopController启动类EnableAutoOperateLog需求: 需要保存的日志内容在方法的参数中,并…

Elasticsearch 谷歌插件 Elasticsearch-head 使用

目录 什么是Elasticsearch-head 安装 ​编辑界面 ​编辑集群健康值的几种状态如下 解决跨域问题 基本使用 创建索引 点击概览 点击数据浏览 什么是Elasticsearch-head ElasticSearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎&#x…

js的Date对象

Date 对象用于处理日期与时间。可以通过 new 关键词来定义 Date 对象。 有四种方式初始化日期: new Date(); new Date(value); new Date(dateString); new Date(year, monthIndex [, day [, hours [, minutes [, seconds [, milliseconds]]]]]);实例化例子: var …

gitlab-ci.yml关键字(二)(全局)default、stages

default 这是一个全局关键字,可以在全局设置某些关键字的的默认值,如果在job中没有定义该关键字的值,那么就会使用全局设置的默认值 示例 default:image: ruby:3.0rspec:script: bundle exec rspecrspec 2.7:image: ruby:2.7script: bundl…

LeetCode 598 范围求和Ⅱ

LeetCode刷题记录 文章目录&#x1f4dc;题目描述&#x1f4a1;解题思路⌨C代码&#x1f4dc;题目描述 给你一个 m x n 的矩阵 M &#xff0c;初始化时所有的 0 和一个操作数组 op &#xff0c;其中 ops[i] [ai, bi] 意味着当所有的0 < x < ai和 0 < y < bi 时&am…

力扣——环形链表

142. 环形链表 II - 力扣&#xff08;LeetCode&#xff09; 这个题是要求我们判断链表是否存在环&#xff0c;有则返回环开始的结点&#xff0c;没有则返回一个NULL&#xff1b; 废话就不说了&#xff0c;直入主题&#xff1a; /*** Definition for singly-linked list.* str…

代码快一点~生活慢一点~【python性能调试工具分享】

背景1 天下武功&#xff0c;为快不破&#xff01; 上学的时候&#xff0c;有些人考试半小时就已经把卷子写完了。有些人还没写完。 工作的时候&#xff0c;有些人一天就能把活干完了&#xff0c;有些人还没开始。 写论文的时候&#xff0c;有些人代码都写完了&#xff0c;数…

ospf不规则区域实验

r11&#xff0c;r3为运行商&#xff0c;要求全网可达 首先现在r13上配置ospf 然后配置一条缺省路由并且下发路由 抓取流量 [r13]acl 2000 [r13-acl-basic-2000]rule permit source any [r13]int g0/0/1 [r13-GigabitEthernet0/0/1]nat outbound 2000 下一步实现ospf100 里面…

Ant Design入门

目录 一&#xff1a;什么是Ant Design&#xff1f; 二&#xff1a;开始使用 三&#xff1a;布局 四&#xff1a;表格 一&#xff1a;什么是Ant Design&#xff1f; Ant Design是阿里蚂蚁金服团队基于React开发的ui组件&#xff0c;主要用于中后台系统的使用。 官网&#x…

【技术】5G技术的应用场景及发展趋势

5G是具有高速率、低时延和大连接特点的新一代宽带移动通信技术。5G系统基于大带宽和大规模天线方案&#xff0c;能实现亚米级高精度定位。 据《5G经济社会影响白皮书》可知&#xff0c;按照2020年5G才大规模商用算起&#xff0c;预计2020年至2025年期间&#xff0c;中国5G发展将…

抓包工具wiresharke及抓包流程

背景&#xff1a;公司的系统在生产环境运行一段时间之后&#xff0c;通过skywalking监控工具发现时不时会有接口调用耗时很长的情况出现。且监控到的数据和华为云ELB的监控日志不匹配&#xff0c;为了验证是否是由华为云ELB转发延迟导致&#xff0c;决定在生产上抓包验证&#…

【应用】博图SCL语言之抢答器应用

使用博图的SCL语言来完成多人抢答器的应用案例。 文章目录 目录 文章目录 前言 一、控制要求和I/O分配 1.控制要求 2.I/O分配 3.具体场景 二、编写 1.建立变量 2.编写 1.四路抢答器互锁 2.抢答提示指示灯 3.提前抢答和超时不抢答 4.完善 三、效果 1.仿真效果 2.虚拟工厂效果 …

IPO OC 系列模拟信号隔离转换模块0-1mA /0-10mA/0-20mA/ 4-20mA/0-75mV/0-2.5V/0-5V/0-10V

概述 IPO OC 系列模拟信号隔离放大器是一种将输入信号隔离放大、转换成按比例输出的直流信号混合集成厚模电路。产品广泛应用在电力、远程监控、仪器仪表、医疗设备、工业自控等需要电量隔离测控的行业。该模块内部嵌入了一个高效微功率的电源&#xff0c;可以向输入端和输出端…

缅怀致敬 继往开来 | 萨师煊教授诞辰100周年纪念主题活动在京举行

2022年12月27日是我国数据库学科奠基人——萨师煊教授诞辰100周年纪念日。为缅怀萨师煊先生对我国数据库领域做出的突出贡献&#xff0c;弘扬萨师煊先生敢为人先、严谨治学的高尚品格&#xff0c;“萨师煊教授诞辰100周年纪念活动”27日在北京举行。本次纪念活动由中国人民大学…

云原生丨手把手教你使用zabbix监控postgresql数据库(超详细讲解)

文章目录一、前言二、什么是zabbix三、zabbix安装步骤四、监控postgresql实现步骤一、前言 对于运维人员来说&#xff0c;监控是非常重要的&#xff0c;因为如果想要保证线上业务整体能够稳定运行&#xff0c;那么我们则需要实时关注与其相关的各项指标是否正常。 而一个业务…

数据结构-图

1、图的基本概念 &#xff08;1&#xff09;定义 图是一种较为复杂的非线性结构。 图就是由顶点的有穷非空集合和顶点之间的边组成的集合。通常表示为G(V,E)&#xff0c;其中&#xff0c;G表示一个图&#xff0c;V表示顶点的集合&#xff0c;E表示边的集合。 &#xff08;2&…

当年谷歌为什么退出中国?

《时代》周刊中&#xff0c;百度公司创始人兼CEO李彦宏成为封面人物&#xff0c;成为中国互联网登《时代》的第一人。 而专访中的一段话&#xff0c;却让一段往事再次成为了舆论热议的焦点。专访中&#xff0c;李彦宏表示&#xff0c;谷歌当年退出中国是因为迫于百度给予的市场…

高可用软件什么意思?哪些高可用软件好用?

你知道高可用软件什么意思吗&#xff1f;哪些高可用软件好用&#xff1f;这里我们小编就给大家简单回答一下这两个问题。希望能对大家有用。 高可用软件什么意思&#xff1f; 所谓高可用是指系统无中断地执行其功能的能力&#xff1b;因此高可用软件是指具备处理能力&#xff…

SpringBoot 之自动装配简单使用

什么是自动装配&#xff1f; Spring Boot 自动装配是指 Spring Boot 应用程序在启动时&#xff0c;框架会自动根据应用程序的配置来创建和连接各种对象之间的依赖关系。这意味着&#xff0c;在应用程序中使用的对象可以通过声明它们的依赖关系来自动创建&#xff0c;而不需要人…

Linux中wait()函数

编程过程中&#xff0c;有时需要让一个进程等待另一个进程&#xff0c;最常见的是父进程等待自己的子进程&#xff0c;或者父进程回收自己的子进程资源包括僵尸进程。这里简单介绍一下系统调用函数&#xff1a;wait() 函数原型是#include <sys/types.h>#include <wai…