全网最详细的 gin框架请求数据绑定Bind 源码解析 -- 帮助你全面了解gin框架的请求数据绑定原理和方法

 在gin框架中,我们可以将多种请求数据(json, form,uri,header等)直接绑定到我们定义的结构体,底层是通过反射方式获取我们定义在结构体上面的tag来实现请求数据到我们的结构体数据的绑定的。 在gin的底层有2大体系的数据绑定一个是Bind,是个是ShouldBind, 下面我们就从数据绑定入口开始一层层的解开gin数据绑定的神秘面纱!

gin中支持的数据绑定类型

        gin框架中的所有数据的绑定都是通过请求类型的 Content-Type这个 MIME类型来完成的,他所支持的类型如下:

// Content-Type MIME of the most common data formats.
const (MIMEJSON              = "application/json"MIMEHTML              = "text/html"MIMEXML               = "application/xml"MIMEXML2              = "text/xml"MIMEPlain             = "text/plain"MIMEPOSTForm          = "application/x-www-form-urlencoded"MIMEMultipartPOSTForm = "multipart/form-data"MIMEPROTOBUF          = "application/x-protobuf"MIMEMSGPACK           = "application/x-msgpack"MIMEMSGPACK2          = "application/msgpack"MIMEYAML              = "application/x-yaml"MIMEYAML2             = "application/yaml"MIMETOML              = "application/toml"
)

我们在Bind和ShouldBind  2大序列 中是使用的XXX 定义

// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (JSON          BindingBody = jsonBinding{}XML           BindingBody = xmlBinding{}Form          Binding     = formBinding{}Query         Binding     = queryBinding{}FormPost      Binding     = formPostBinding{}FormMultipart Binding     = formMultipartBinding{}ProtoBuf      BindingBody = protobufBinding{}MsgPack       BindingBody = msgpackBinding{}YAML          BindingBody = yamlBinding{}Uri           BindingUri  = uriBinding{}Header        Binding     = headerBinding{}TOML          BindingBody = tomlBinding{}
)

上面这些就是gin框架中支持的数据的绑定类型XXX定义, 如 BindJSON,  BindForm,  ShouldBindJSON,    ShouldBinxUri  等。

gin框架中的2大类型的数据绑定方式

        他们实现的功能是一样的,区别在于Bind序列如果数据绑定失败会直接抛异常并退出当前请求,而ShouldBind 则不会中断当前的请求。 原因是 Bind序列使用的是 c.MustBindWith ,注意这里的名字前缀 Must  在go语言的开发中我们通常的做法就是带这个Must的方法,就表示必须要满足的方法, 如果不满足就直接给你个 panic 异常(直接退出当前请求),gin框架也不另外MustXxx 的方法也是必须要满足的,否则panic中断当前请求; 而ShouldBind序列是通过  c.ShouldBindWith 来实现的,他在数据绑定异常时会忽略异常,继续后的的请求。

1.  Bind  序列 示例

他由Bind方法,和 BindXXX 方法主从,他们内部都是调用了c.MustBindWith 方法,这里的XXX 即gin中支持的数据绑定类型,见 gin中支持的数据绑定类型定义 

func (c *Context) Bind(obj any) error {b := binding.Default(c.Request.Method, c.ContentType())return c.MustBindWith(obj, b)
}// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON).
func (c *Context) BindJSON(obj any) error {return c.MustBindWith(obj, binding.JSON)
}
// ....

2.  ShouldBind序列 示例

func (c *Context) ShouldBind(obj any) error {b := binding.Default(c.Request.Method, c.ContentType())return c.ShouldBindWith(obj, b)
}// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj any) error {return c.ShouldBindWith(obj, binding.JSON)
}
// ......

gin 中数据绑定接口定义

        不管是那个序列的数据绑定,他们都是通过实现以下接口来完成具体的数据绑定的,这个也是go语言的一个核心思想 -- 面向接口编程 !  你没有看错就是面向接口编程,而你常见其他语言,如java 等好像都是说的面向对象编程,而go语言的特别就在于此, go语言中把面向接口编程做到了极致!

        gin框架中为数据绑定定义了3个接口来实现不同类型的数据绑定。 


// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {Name() stringBind(*http.Request, any) error
}// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {BindingBindBody([]byte, any) error
}// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {Name() stringBindUri(map[string][]string, any) error
}

gin中的数据绑定实现

        gin框架中已经给我们实现了多种常见的数据类型的绑定,见 gin中支持的数据绑定类型 。 当然, 如果已有实现中没有你想要的数据类型的绑定或者你想自己动手来实现, 这个也非常简单, 你只要实现上面定义的对应的接口即可! 不知道怎么实现的话你就参考一下gin中已有的实现,哈哈!

gin框架数据绑定实现截图

form数据绑定实现示例

        这里的数据实现比较多, 我们就以 我们最常用的form数据绑定实现为例,和大家一起来学习一下gin中的数据绑定是如何实现的。

1.  数据绑定入口

        下面的formBinding 绑定是普通form的绑定, 另外还有formPostBinding  POST类型的数据绑定, formMultipartBinding 这个是针对媒体上传类型的数据的绑定实现,我们就不一一列举了,他们的实现思路都差不多。


func (formBinding) Bind(req *http.Request, obj any) error {if err := req.ParseForm(); err != nil {return err}if err := req.ParseMultipartForm(defaultMemory); err != nil && !errors.Is(err, http.ErrNotMultipart) {return err}if err := mapForm(obj, req.Form); err != nil {return err}return validate(obj)
}
2.  请求form数据解析 req.ParseForm() 

// ParseForm populates r.Form and r.PostForm.
//
// For all requests, ParseForm parses the raw query from the URL and updates
// r.Form.
//
// For POST, PUT, and PATCH requests, it also reads the request body, parses it
// as a form and puts the results into both r.PostForm and r.Form. Request body
// parameters take precedence over URL query string values in r.Form.
//
// If the request Body's size has not already been limited by [MaxBytesReader],
// the size is capped at 10MB.
//
// For other HTTP methods, or when the Content-Type is not
// application/x-www-form-urlencoded, the request Body is not read, and
// r.PostForm is initialized to a non-nil, empty value.
//
// [Request.ParseMultipartForm] calls ParseForm automatically.
// ParseForm is idempotent.
func (r *Request) ParseForm() error {var err errorif r.PostForm == nil {if r.Method == "POST" || r.Method == "PUT" || r.Method == "PATCH" {r.PostForm, err = parsePostForm(r)}if r.PostForm == nil {r.PostForm = make(url.Values)}}if r.Form == nil {if len(r.PostForm) > 0 {r.Form = make(url.Values)copyValues(r.Form, r.PostForm)}var newValues url.Valuesif r.URL != nil {var e errornewValues, e = url.ParseQuery(r.URL.RawQuery)if err == nil {err = e}}if newValues == nil {newValues = make(url.Values)}if r.Form == nil {r.Form = newValues} else {copyValues(r.Form, newValues)}}return err
}
3. 上传类型数据解析 req.ParseMultipartForm

// ParseMultipartForm parses a request body as multipart/form-data.
// The whole request body is parsed and up to a total of maxMemory bytes of
// its file parts are stored in memory, with the remainder stored on
// disk in temporary files.
// ParseMultipartForm calls [Request.ParseForm] if necessary.
// If ParseForm returns an error, ParseMultipartForm returns it but also
// continues parsing the request body.
// After one call to ParseMultipartForm, subsequent calls have no effect.
func (r *Request) ParseMultipartForm(maxMemory int64) error {if r.MultipartForm == multipartByReader {return errors.New("http: multipart handled by MultipartReader")}var parseFormErr errorif r.Form == nil {// Let errors in ParseForm fall through, and just// return it at the end.parseFormErr = r.ParseForm()}if r.MultipartForm != nil {return nil}mr, err := r.multipartReader(false)if err != nil {return err}f, err := mr.ReadForm(maxMemory)if err != nil {return err}if r.PostForm == nil {r.PostForm = make(url.Values)}for k, v := range f.Value {r.Form[k] = append(r.Form[k], v...)// r.PostForm should also be populated. See Issue 9305.r.PostForm[k] = append(r.PostForm[k], v...)}r.MultipartForm = freturn parseFormErr
}
4. 数据映射 函数 mapForm , mapFormByTag

   注意这里是一个函数,上面2个ParseForm 和 ParseMultipartForm 都是在请求对象上面的方法。

从下面的代码可见, 他这里调用的是mapFormByTag 这个函数,这个即是根据我们定义在结构体中的Tag来映射数据, 这里因为是form类型的数据绑定,所以这个地方的第三个参数就是 form

func mapForm(ptr any, form map[string][]string) error {return mapFormByTag(ptr, form, "form")
}

我们接着看看这个mapFormByTag

这里的ptr就是我们要将数据绑定到的我们自定义的结构体对象的指针, form 这个就是上面解析后的请求表单的数据map,   第三个参数 tag 这个就是我们要解析的数据类型的Tag定义名称,这里就的 form 就表示他解析的数据就是我们的结构体TAG中的名称为form的Tag数据, 如我们结构体中的字段Page的定义  Page int `json:"page" form:"page" `  , 这里的tag名称就是form,而对于的字段名称就是 page, 就表示他可以绑定请求参数page的值到 结构体的 Page 字段。


func mapFormByTag(ptr any, form map[string][]string, tag string) error {// Check if ptr is a mapptrVal := reflect.ValueOf(ptr)var pointed anyif ptrVal.Kind() == reflect.Ptr {ptrVal = ptrVal.Elem()pointed = ptrVal.Interface()}if ptrVal.Kind() == reflect.Map &&ptrVal.Type().Key().Kind() == reflect.String {if pointed != nil {ptr = pointed}return setFormMap(ptr, form)}return mappingByPtr(ptr, formSource(form), tag)
}

PS: 这里有一个很容易忽略但又非常重要的知识点,就是Elem这个方法的应用时机。 当我们在对一个对象应用函数 reflect.ValueOf() 获取对于的 reflect.Value 对象后, 如果any类型的入参 ptr是一个指针,则获取到的Value对象就必须要调用 .Elem()方法获取指针对应的具体的数据的 reflect.Value后再进行操作,否则就获取不到你想要的数据,因为你拜佛没有找对庙门,哈哈!

5. mappingByPtr函数

这个就是具体的数据绑定映射函数的实现逻辑了, 他这个里面用了递归方式来处理数据的映射,另外还使用了一个 setter 数据设置接口来进行数据的设置。 

从下面的代码我们可以看到,在mapping的第一行就继续了一个tag名称的判断,如果名称是 - 就直接返回 忽略了这个字段的映射处理。  如我们结构体中某个字段的tag 名称是是这样定义的  Name string `form:"-" `  这个就表示会忽略Name这个字段的form数据的绑定


func mappingByPtr(ptr any, setter setter, tag string) error {_, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag)return err
}func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {if field.Tag.Get(tag) == "-" { // just ignoring this fieldreturn false, nil}vKind := value.Kind()if vKind == reflect.Ptr {var isNew boolvPtr := valueif value.IsNil() {isNew = truevPtr = reflect.New(value.Type().Elem())}isSet, err := mapping(vPtr.Elem(), field, setter, tag)if err != nil {return false, err}if isNew && isSet {value.Set(vPtr)}return isSet, nil}if vKind != reflect.Struct || !field.Anonymous {ok, err := tryToSetValue(value, field, setter, tag)if err != nil {return false, err}if ok {return true, nil}}if vKind == reflect.Struct {tValue := value.Type()var isSet boolfor i := 0; i < value.NumField(); i++ {sf := tValue.Field(i)if sf.PkgPath != "" && !sf.Anonymous { // unexportedcontinue}ok, err := mapping(value.Field(i), sf, setter, tag)if err != nil {return false, err}isSet = isSet || ok}return isSet, nil}return false, nil
}
6 setter数据设置接口定义

这个接口就定义了一个方法, TrySet 尝试帮我们设置数据

// setter tries to set value on a walking by fields of a struct
type setter interface {TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSet bool, err error)
}
7. 数据设置函数 tryToSetValue 

这里就是数据映射过程中的字段Tag值的获取核心函数。 通过下面的代码我们可以找到gin的数据绑定的Tag中的数据是如何处理的。    详见下面的代码注释


func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) {var tagValue stringvar setOpt setOptions// 通过反射获取结构体字段tag对应的数据,tagValue = field.Tag.Get(tag)//将获取到的tag数据再使用逗号分隔 tagValue, opts := head(tagValue, ",")if tagValue == "" { // default value is FieldNametagValue = field.Name}if tagValue == "" { // when field is "emptyField" variablereturn false, nil}var opt stringfor len(opts) > 0 {opt, opts = head(opts, ",")// 如果获取到的tag值中包含了 default=xx  则对这个字段设置默认值if k, v := head(opt, "="); k == "default" {setOpt.isDefaultExists = truesetOpt.defaultValue = v}}return setter.TrySet(value, field, tagValue, setOpt)
}

        根据上面的代码 举例说明:  field.Tag.Get(tag)  这个就是获取我们在结构体中设置的tag对应的值, 如 假设tag为form, 我们有一个结构体中的字段定义是  Page int `json:"page" form:"page,default=1" `     这里的代码field.Tag.Get(tag)  获取到的内容就是 page,default=1

tagValue, opts := head(tagValue, ",") 这个获取到的是tagValue就是 page, opts的值就是default=1

这个定义的意思就是 将请求表单中的 page 对应的字段帮我们绑定到我们定义的这个结构体的 Page字段上面,如果请求表单中没有相关的数据则使用这里定义的默认值1(default=1就是定义默认值), 这个地方就是如何给绑定数据设置默认值的方式, 这个知识点gin官方文档和示例可没有哦!! 这个就是通过这里的源码发现的使用方法。

form的setter接口执行 TrySet
// TrySet tries to set a value by request's form source (like map[string][]string)
func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSet bool, err error) {return setByForm(value, field, form, tagValue, opt)
}
 form数据设置函数 setByForm

通过下面的代码,可见他可以设置的数据类型有 切片, 数组,还有可序列化的数据(默认),这个可序列化的数据类型就包含所有的可以被序列化的数据。


func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSet bool, err error) {vs, ok := form[tagValue]if !ok && !opt.isDefaultExists {return false, nil}switch value.Kind() {case reflect.Slice:if !ok {vs = []string{opt.defaultValue}}return true, setSlice(vs, value, field)case reflect.Array:if !ok {vs = []string{opt.defaultValue}}if len(vs) != value.Len() {return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())}return true, setArray(vs, value, field)default:var val stringif !ok {val = opt.defaultValue}if len(vs) > 0 {val = vs[0]}if ok, err := trySetCustom(val, value); ok {return ok, err}return true, setWithProperType(val, value, field)}
}
可序列化的数据 设置
// trySetCustom tries to set a custom type value
// If the value implements the BindUnmarshaler interface, it will be used to set the value, we will return `true`
// to skip the default value setting.
func trySetCustom(val string, value reflect.Value) (isSet bool, err error) {switch v := value.Addr().Interface().(type) {case BindUnmarshaler:return true, v.UnmarshalParam(val)}return false, nil
}

 这个就是可序列化的数据的设置的具体逻辑,这里也是根据反射方式先获取要设置的结构体的字段的类型,然后根据不同的类型来设置具体的值。  细心的你应该能够注意到,我们上面提到的小知识点 指针类型的数据需要先调用 .Elem()方法 的应用,见下面的 case reflect.Ptr:  


func setWithProperType(val string, value reflect.Value, field reflect.StructField) error {switch value.Kind() {case reflect.Int:return setIntField(val, 0, value)case reflect.Int8:return setIntField(val, 8, value)case reflect.Int16:return setIntField(val, 16, value)case reflect.Int32:return setIntField(val, 32, value)case reflect.Int64:switch value.Interface().(type) {case time.Duration:return setTimeDuration(val, value)}return setIntField(val, 64, value)case reflect.Uint:return setUintField(val, 0, value)case reflect.Uint8:return setUintField(val, 8, value)case reflect.Uint16:return setUintField(val, 16, value)case reflect.Uint32:return setUintField(val, 32, value)case reflect.Uint64:return setUintField(val, 64, value)case reflect.Bool:return setBoolField(val, value)case reflect.Float32:return setFloatField(val, 32, value)case reflect.Float64:return setFloatField(val, 64, value)case reflect.String:value.SetString(val)case reflect.Struct:switch value.Interface().(type) {case time.Time:return setTimeField(val, field, value)case multipart.FileHeader:return nil}return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())case reflect.Map:return json.Unmarshal(bytesconv.StringToBytes(val), value.Addr().Interface())case reflect.Ptr:if !value.Elem().IsValid() {value.Set(reflect.New(value.Type().Elem()))}return setWithProperType(val, value.Elem(), field)default:return errUnknownType}return nil
}

ok, 至此,gin框架中的数据请求绑定源码都扒完了...   后面就是如何使用了,当你了解了他的原理后使用那就是小菜一碟了, 本文就不做讨论了。。。。。。

如果本文对你有帮助,欢迎点赞,收藏,评论, 你的支持就是我们继续产出优质内容的动力哦 :) 

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

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

相关文章

Python pip install模块时C++编译环境问题

pip install模块时C编译环境问题 在接触和使用python后&#xff0c;常常会通过pip install命令安装第三方模块&#xff0c;大多数模块可以直接安装&#xff0c;但许多新同学仍会遇见某些模块需要实时编译后才能安装&#xff0c;如报错信息大概是缺乏C编译环境&#xff0c;本文则…

【Elasticsearch】Elasticsearch索引创建与管理详解

文章目录 &#x1f4d1;引言一、Elasticsearch 索引的基础概念二、创建索引2.1 使用默认设置创建索引2.2 自定义设置创建索引2.3 创建索引并设置映射 三、索引模板3.1 创建索引模板3.2 使用索引模板创建索引 四、管理索引4.1 查看索引4.2 更新索引设置4.3 删除索引 五、索引别名…

Go-知识测试-性能测试

Go-知识测试-性能测试 1. 定义2. 例子3. testing.common 测试基础数据4. testing.TB 接口5. 关键函数5.1 testing.runBenchmarks5.2 testing.B.runN5.3 testing.B.StartTimer5.4 testing.B.StopTimer5.5 testing.B.ResetTimer5.6 testing.B.Run5.7 testing.B.run15.8 testing.B…

监听蓝牙对话的BlueSpy技术复现

本文是之前文章的BlueSpy技术的复现过程&#xff1a;https://mp.weixin.qq.com/s/iCeImLLPAwwKH1avLmqEpA 2个月前&#xff0c;网络安全和情报公司Tarlogic在西班牙安全大会RootedCon 2024上提出了一项利用蓝牙漏洞的BlueSpy技术&#xff0c;并在之后发布了一个名为BlueSpy的概…

git 提交代码忽略eslint代码检测

在暂存代码的时候会出现以上情况因为在提交代码的时候会默认运行代码进行检测&#xff0c;如果不符合代码规范就会进行报错 解决&#xff1a; 使用 git commit --no-verify -m xxx 忽略eslint的检测

Laravel 谨慎使用Storage::append()

在 driver 为 local 时&#xff0c;Storage::append()在高并发下&#xff0c;会存在丢失数据问题&#xff0c;文件被覆写&#xff0c;而非尾部添加&#xff0c;如果明确是本地文件操作&#xff0c;像日志写入&#xff0c;建议使用 Illuminate\Filesystem\Filesystem或者php原生…

邀请函 | 极限科技全新搜索引擎 INFINI Pizza 亮相 2024 可信数据库发展大会!

过去一年&#xff0c;在全球 AI 浪潮和国家数据局成立的推动下&#xff0c;数据库产业变革不断、热闹非凡。2024 年&#xff0c;站在中国数字经济产业升级和数据要素市场化建设的时代交汇点上&#xff0c;“2024 可信数据库发展大会” 将于 2024 年 7 月 16-17 日在北京悠唐皇冠…

肆拾玖坊的商业模式,49坊新零售奖金制度体系,众筹众创+会员制

肆拾玖坊之所以能够在短时间内成为白酒行业的“现象级”企业,,不仅是依靠独特商业模式,同时也依靠的是坚持用户为核心,围绕用户需求,让用户与产品直接产生连接理念。 坐标&#xff1a;厦门&#xff0c;我是易创客肖琳 深耕社交新零售行业10年&#xff0c;主要提供新零售系统工…

前端技术(二)——javasctipt 介绍

一、javascript基础 1. javascript简介 ⑴ javascript的起源 ⑵ javascript 简史 ⑶ javascript发展的时间线 ⑷ javascript的实现 ⑸ js第一个代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>…

Vue中的axios深度探索:从基础安装到高级功能应用的全面指南

文章目录 前言一、axios 请求1. axios的概念2. axios的安装3. axiso请求方式介绍4. axios请求本地数据5. axios跨域6. axios全局注册7. axios支持的请求类型1&#xff09;get请求2&#xff09;post请求3&#xff09;put请求4&#xff09;patch请求5&#xff09;delete请求 二、…

MyBatis操作数据库(入门)

本节目标 使用MyBatis完成简单的增删改查操作&#xff0c;参数传递掌握MyBatis的两种写法&#xff1a;注解和XML方式掌握MyBatis相关的日志配置 前言 在应用分层学习中&#xff0c;我们了解web应用程序一般分为三层&#xff0c;即Controller、Service、Dao。在之前的案例中&a…

化学SCI期刊,中科院4区,易录用,几乎不退稿

一、期刊名称 Chemical Papers 二、期刊简介概况 期刊类型&#xff1a;SCI 学科领域&#xff1a;化学 影响因子&#xff1a;2.1 中科院分区&#xff1a;4区 三、期刊征稿范围 该杂志致力于基础和应用化学和化学工程研究。它的范围很广&#xff0c;涵盖了所有化学科学&…

2024年江苏智能制造工厂名单:我看出了未来择业和跳槽方向

导语 大家好&#xff0c;我是社长&#xff0c;老K。专注分享智能制造和智能仓储物流等内容。 新书《智能物流系统构成与技术实践》 在当今这个飞速发展的时代&#xff0c;智能制造已成为推动工业进步的强大引擎。随着技术革新的浪潮一波接一波地涌来&#xff0c;我们不禁要问&a…

动手学深度学习(Pytorch版)代码实践 -计算机视觉-49风格迁移

49风格迁移 读入内容图像&#xff1a; import torch import torchvision from torch import nn import matplotlib.pylab as plt import liliPytorch as lp from d2l import torch as d2l# 读取内容图像 content_img d2l.Image.open(../limuPytorch/images/rainier.jpg) plt.…

使用 Swift 递归搜索目录中文件的内容,同时支持 Glob 模式和正则表达式

文章目录 前言项目设置查找文件读取CODEOWNERS文件解析规则搜索匹配的文件确定文件所有者输出结果总结前言 如果你新加入一个团队,想要快速的了解团队的领域和团队中拥有的代码库的详细信息。 如果新团队中的代码库在 GitHub / GitLab 中并且你不熟悉代码所有权模型的概念或…

Unity开箱即用的UGUI面板的拖拽移动功能

文章目录 &#x1f449;一、背景&#x1f449;二、效果图&#x1f449;三、原理&#x1f449;四、核心代码&#x1f449;五&#xff0c;总结 &#x1f449;一、背景 之前做PC项目时常常有面板拖拽移动的需求&#xff0c;今天总结封装一下&#xff0c;做成一个随时随地可复用的…

Linux 安装 Redis 教程

优质博文&#xff1a;IT-BLOG-CN 一、准备工作 配置gcc&#xff1a;安装Redis前需要配置gcc&#xff1a; yum install gcc如果配置gcc出现依赖包问题&#xff0c;在安装时提示需要的依赖包版本和本地版本不一致&#xff0c;本地版本过高&#xff0c;出现如下问题&#xff1a…

Windows 11 安装 安卓子系统 (WSA)

How to Install Windows Subsystem for Android (WSA) on Windows 11 新手教程&#xff1a;如何安装Windows 11 安卓子系统 说明 Windows Subsystem for Android 或 WSA 是由 Hyper-V 提供支持的虚拟机&#xff0c;可在 Windows 11 操作系统上运行 Android 应用程序。虽然它需…

python基础_类

在Python中&#xff0c;类&#xff08;Class&#xff09;是面向对象编程&#xff08;OOP&#xff09;的核心概念之一。类提供了一种创建新对象的模板&#xff0c;这些对象通常被称为类的实例或对象。以下是关于Python类的一些关键点和特性&#xff1a; 定义类 类通过class关键…

ctfshow-web入门-命令执行(web71-web74)

目录 1、web71 2、web72 3、web73 4、web74 1、web71 像上一题那样扫描但是输出全是问号 查看提示&#xff1a;我们可以结合 exit() 函数执行php代码让后面的匹配缓冲区不执行直接退出。 payload&#xff1a; cvar_export(scandir(/));exit(); 同理读取 flag.txt cinclud…