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

news2025/1/15 17:22:09

 在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() string
	Bind(*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 {
	Binding
	BindBody([]byte, any) error
}

// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {
	Name() string
	BindUri(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 error
	if 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.Values
		if r.URL != nil {
			var e error
			newValues, 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 error
	if 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 = f

	return 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 map
	ptrVal := reflect.ValueOf(ptr)
	var pointed any
	if 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 field
		return false, nil
	}

	vKind := value.Kind()

	if vKind == reflect.Ptr {
		var isNew bool
		vPtr := value
		if value.IsNil() {
			isNew = true
			vPtr = 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 bool
		for i := 0; i < value.NumField(); i++ {
			sf := tValue.Field(i)
			if sf.PkgPath != "" && !sf.Anonymous { // unexported
				continue
			}
			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 string
	var setOpt setOptions

    // 通过反射获取结构体字段tag对应的数据,
	tagValue = field.Tag.Get(tag)
    //将获取到的tag数据再使用逗号分隔 
	tagValue, opts := head(tagValue, ",")

	if tagValue == "" { // default value is FieldName
		tagValue = field.Name
	}
	if tagValue == "" { // when field is "emptyField" variable
		return false, nil
	}

	var opt string
	for len(opts) > 0 {
		opt, opts = head(opts, ",")

        // 如果获取到的tag值中包含了 default=xx  则对这个字段设置默认值
		if k, v := head(opt, "="); k == "default" {
			setOpt.isDefaultExists = true
			setOpt.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 string
		if !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.coloradmin.cn/o/1881952.html

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

相关文章

Apple苹果可能会在今年秋天宣布与Google Gemini谷歌双子座的交易

如果你对迄今为止唯一能与苹果设备集成的人工智能模型是ChatGPT感到失望&#xff0c;听起来你不必等待很长时间就能改变这种情况。据彭博社&#xff08;Bloomberg&#xff09;的马克古尔曼&#xff08;Mark Gurman&#xff09;今天在他的Power On时事通讯中称&#xff0c;苹果将…

GuLi商城-商品服务-API-三级分类-删除-逻辑删除

注意&#xff1a;官方文档说logic配置可以省略&#xff0c;代码中直观些&#xff0c;配上吧 逻辑删除注解&#xff1a; 实体类字段上加逻辑删除注解&#xff1a; 启动nacos&#xff1a; 启动商品服务&#xff1a; postman测试&#xff1a; 数据库字段值改成了0&#xff0c;说明…

Decorators与类

在Python中&#xff0c;装饰器&#xff08;decorator&#xff09;是一种用于修改函数或方法行为的特殊函数。装饰器可以用于函数、方法和类。在类中使用装饰器可以增强类的方法、属性&#xff0c;甚至整个类的功能。以下是一些关于我对装饰器与类的详细信息和示例教程。 1、问题…

vivado VIO IP核

参考&#xff1a;pg159 VIO&#xff1a;可以模拟输入/输出功能&#xff0c;实时监视和修改FPGA中的信号&#xff0c;用于调试和验证&#xff0c;与ILA相比&#xff0c;VIO无需占用RAM资源。 VIO IP的输出对于FPGA内部逻辑是输入信号&#xff0c;可以在调试界面设置输入值&…

第6章 复制

文章目录 前言1.配置1.1建立复制1.2断开复制1.3 安全性1.4 只读1.5 传输延迟 2. 拓扑2.1.一主一从结构2.2.一主多从结构2.3.树状主从结构 3.原理3.1复制过程3.2数据同步3.3全量复制 前言 复制功能&#xff0c;实现了相同数据的多个Redis副本。复制功能是高可用Redis的基础&…

STM32第十二课:ADC检测烟雾浓度(MQ2)

文章目录 需求一、MQ-2 气体传感器特点应用电路及引脚 二、实现流程1.开时钟&#xff0c;分频&#xff0c;配IO2.配置ADC的工作模式3.配置通道4.复位&#xff0c;AD校准5.数值的获取 需求实现总结 需求 使用ADC将MQ2模块检测到的烟雾浓度模拟量转化为数字量。 最后&#xff0c…

[Go 微服务] Kratos 验证码业务

文章目录 1.环境准备2.验证码服务2.1 kratos 初始化验证码服务项目2.2 使用 Protobuf 定义验证码生成接口2.3 业务逻辑代码实现 1.环境准备 protoc和protoc-gen-go插件安装和kratos工具安装 protoc下载 下载二进制文件&#xff1a;https://github.com/protocolbuffers/protobu…

CocosCreator构建IOS教程

CocosCreator构建IOS教程 添加include: Header Search Paths:拖拽include过来 添加SoundEngine: Header Search Paths: 把SoundEngine POSIX Common 三个文件夹拖拽到里面去

IEEE TNNLS | 脑电(EEG)自监督学习

摘要 数十年的研究表明&#xff0c;与传统的统计技术相比&#xff0c;机器学习在探索脑电图(EEG)记录中嵌入的高度非线性模式方面具有优势。然而&#xff0c;即使是最先进的机器学习技术也需要相对较大且标记完整的EEG存储库。EEG数据的收集和标记成本高昂。此外&#xff0c;由…

Google ghOSt 调度器分析(4)

调度器的优缺点 *ghOSt* 调度器的优缺点优点缺点*ghost* 与 *CFS* 调度运行时间比较 ghOSt 调度器的优缺点 优点 逻辑简单&#xff0c;实现简单&#xff1b;它只是在内核中增加了两个调度类&#xff0c;通过对这两个调度类的操作来完成相应任务的优先级的提升等操作&#xff…

HarmonyOS(42) Divider 分割器组件 实现分割线

Divider分割线 简介使用示例参考目录 简介 该组件可以帮助我们实现 水平分割线和竖直分割线&#xff0c;同时支持设置分割线的宽度、颜色、和两端的样式 使用示例 横向分割线 &#xff0c;默认就是横向分割 // Horizontal dividerColumn() {this.Block()Divider()this.Bloc…

如何使用ChatGPT提高数学建模竞赛的获奖概率

如何使用ChatGPT提高数学建模竞赛的获奖概率 数学建模助手GPT https://chatgpt-plus.top/g/g-OX0D7uMn9-shu-ju-jian-mo-zhu-shou-by-maynor 1. 问题分析与理解 在数学建模的初期&#xff0c;准确理解问题的背景和要求至关重要。通过使用ChatGPT&#xff0c;你可以&#xff…

【基础篇】第4章 查询与过滤

在Elasticsearch的世界里&#xff0c;高效地从海量数据中检索出所需信息是其核心价值所在。本章将深入解析查询与过滤的机制&#xff0c;从基础查询到复合查询&#xff0c;再到全文搜索与分析器的定制&#xff0c;为你揭开数据检索的神秘面纱。 4.1 基本查询 4.1.1 Match查询…

Spring MVC 获取三个域(request请求域,session 会话域,application 应用域)对象的方式

1. Spring MVC 获取三个域(request请求域&#xff0c;session 会话域&#xff0c;application 应用域)对象的方式 文章目录 1. Spring MVC 获取三个域(request请求域&#xff0c;session 会话域&#xff0c;application 应用域)对象的方式2. Servlet中的三个域对象3. 准备工作3…

8.12 矢量图层面要素单一符号使用十二(插值线渲染边界)

文章目录 前言插值线渲染边界&#xff08;Outline: Interpolated Line&#xff09;QGis设置面符号为插值线渲染边界&#xff08;Outline: Interpolated Line&#xff09;二次开发代码实现插值线渲染边界&#xff08;Outline: Interpolated Line&#xff09; 总结 前言 本章介绍…

【Linux】部署NFS服务实现数据共享

&#x1f468;‍&#x1f393;博主简介 &#x1f3c5;CSDN博客专家   &#x1f3c5;云计算领域优质创作者   &#x1f3c5;华为云开发者社区专家博主   &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流社区&#xff1a;运维交流社区 欢迎大家的加入&#xff01…

【Linux】虚拟机安装openEuler 24.03 X86_64 教程

目录 一、概述 1.1 openEuler 覆盖全场景的创新平台 1.2 系统框架 1.3 平台框架 二、安装详细步骤 一、概述 1.1 openEuler 覆盖全场景的创新平台 openEuler 已支持 x86、Arm、SW64、RISC-V、LoongArch 多处理器架构&#xff0c;逐步扩展 PowerPC 等更多芯片架构支持&…

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

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

nlp--最大匹配分词(计算召回率)

最大匹配算法是一种常见的中文分词算法&#xff0c;其核心思想是从左向右取词&#xff0c;以词典中最长的词为优先匹配。这里我将为你展示一个简单的最大匹配分词算法的实现&#xff0c;并结合输入任意句子、显示分词结果以及计算分词召回率。 代码 : # happy coding…

MATLAB使用系统辨识工具箱建立PID水温的传递函数系数

概述 利用PID控制水温&#xff0c;由于实际在工程项目中&#xff0c;手动调节PID参数比较耗费时间&#xff0c;所以可以先利用MATLAB中的Simulink软件建立模型&#xff0c;先在仿真软件上调节大概的PID参数&#xff0c;再利用此PID参数为基础在实际的工程项目中手动调节PID参数…