• [行业前沿] 有哪些场景适合使用泛型?
    在 Go 中,泛型(自 1.18 起支持)并非万能工具,但在特定场景下能显著提升代码的复用性、类型安全性和可读性。以下是适合使用泛型的典型场景,附带简洁示例:1. 容器类数据结构当实现通用集合(如列表、栈、队列、缓存)时,泛型避免为每种类型重复编写代码。type Stack[T any] struct { items []T}func (s *Stack[T]) Push(x T) { s.items = append(s.items, x)}func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } n := len(s.items) x := s.items[n-1] s.items = s.items[:n-1] return x, true}// 使用intStack := &Stack[int]{}intStack.Push(42)stringStack := &Stack[string]{}stringStack.Push("hello")2. 工具函数:操作切片、映射等对切片进行查找、过滤、转换等操作时,泛型函数比 interface{} 更安全高效。func Filter[T any](slice []T, pred func(T) bool) []T { var result []T for _, v := range slice { if pred(v) { result = append(result, v) } } return result}// 使用evens := Filter([]int{1, 2, 3, 4}, func(x int) bool { return x%2 == 0 })names := Filter([]string{"Alice", "Bob"}, func(s string) bool { return len(s) > 3 })3. 通用结果封装(类似 Rust 的 Result)处理可能失败的操作时,用泛型统一成功值的类型。type Result[T any] struct { Value T Error error}func Try[T any](fn func() (T, error)) Result[T] { v, err := fn() return Result[T]{Value: v, Error: err}}// 使用res := Try(func() (int, error) { return strconv.Atoi("42")})if res.Error != nil { // handle error} else { fmt.Println(res.Value) // 类型是 int}4. 回调或中间件模式当回调函数的参数或返回值类型需与上下文一致时。func WithRetry[T any](fn func() (T, error), maxRetries int) (T, error) { var zero T for i := 0; i < maxRetries; i++ { if v, err := fn(); err == nil { return v, nil } } return zero, errors.New("max retries exceeded")}// 使用value, err := WithRetry(func() (string, error) { return fetchFromAPI(), nil}, 3)5. 依赖注入或工厂模式创建对象时保留类型信息。type Factory[T any] struct { creator func() T}func NewFactory[T any](f func() T) *Factory[T] { return &Factory[T]{creator: f}}func (f *Factory[T]) Build() T { return f.creator()}// 使用dbFactory := NewFactory(func() *sql.DB { return connectDB() })db := dbFactory.Build() // 类型是 *sql.DB,不是 interface{}6. 数值计算通用函数对多种数字类型执行相同运算(需配合类型约束)。type Number interface { int | int32 | int64 | float32 | float64}func Sum[T Number](nums []T) T { var total T for _, n := range nums { total += n } return total}// 使用totalInt := Sum([]int{1, 2, 3}) // inttotalFloat := Sum([]float64{1.5, 2.5}) // float64不适合使用泛型的场景仅有一个具体类型使用:直接写具体类型更清晰;需要运行时动态类型行为(如 JSON 解析到任意结构):仍需 interface{} 或 reflect;性能极度敏感且泛型导致无法内联:需 benchmark 验证;类型逻辑差异大:强行泛型反而降低可读性。总结适合使用泛型的核心特征是:✅ 逻辑相同,仅类型不同;✅ 希望编译期保证类型安全;✅ 避免重复代码。Go 的泛型设计哲学是“适度抽象”——它不追求 Haskell 或 C++ 的表达力,而是解决最痛的重复问题。当你发现自己在复制粘贴代码只为改一个类型名时,就是泛型登场的最佳时机。
  • [行业前沿] 如何避免运行时类型检查的开销?
    在 Go 中,运行时类型检查(如类型断言 x.(T)、reflect 包操作)会带来性能开销,尤其在高频调用路径中。要避免或最小化这类开销,核心思路是将类型决策从运行时提前到编译时或初始化阶段。以下是具体可行的方法:1. 使用泛型(Go 1.18+)替代 interface{} 和类型断言问题代码(运行时检查):func getValue(data map[string]interface{}, key string) interface{} { return data[key]}v := getValue(m, "count")if count, ok := v.(int); ok { // 使用 count}优化后(编译时确定类型):func getValue[T any](data map[string]T, key string) (T, bool) { v, ok := data[key] return v, ok}count, ok := getValue[int](m, "count") 适用场景:数据结构类型已知且固定。2. 在初始化时解析并缓存具体类型(预解析模式)如果输入是动态的(如 JSON),但后续使用是静态的,可在启动时一次性解析:type Config struct { Timeout int Debug bool Name string}func LoadConfig(rawJSON []byte) (*Config, error) { var m map[string]interface{} json.Unmarshal(rawJSON, &m) cfg := &Config{} // 初始化阶段做一次类型检查 if v, ok := m["timeout"].(float64); ok { cfg.Timeout = int(v) } if v, ok := m["debug"].(bool); ok { cfg.Debug = v } // 后续业务逻辑直接使用 cfg.Timeout,无任何类型检查 return cfg, nil}3. 用带类型标签的结构体代替 interface{}如前文所述,避免存储 interface{},改用显式字段表示可能的类型:type Value struct { kind byte // 0=int, 1=string, 2=bool iVal int64 sVal string bVal bool}func (v Value) AsInt() (int, bool) { if v.kind == 0 { return int(v.iVal), true // 无类型断言,直接读字段 } return 0, false}访问时只是简单的字段读取和整数比较,CPU 分支预测友好,无反射或接口调度开销。4. 避免使用 reflect 包,改用代码生成问题:reflect 用于通用序列化/反序列化(如 JSON、ORM),但速度慢。解决方案:用 go generate + 模板生成专用解析代码。例如,定义结构体://go:generate easyjson -all config.gotype Config struct { Timeout int `json:"timeout"` Debug bool `json:"debug"`}生成的 easyjson 代码直接操作字节流和具体字段,不经过 interface{} 或 reflect。 性能提升可达 3–5 倍,且内存分配更少。其他工具:gogen, ffjson, json-iterator/go(部分使用代码生成)。5. 使用函数指针或接口方法替代类型分支如果必须处理多种类型,但行为不同,可将逻辑封装到接口中,避免 switch type:低效写法:func process(v interface{}) { switch x := v.(type) { case int: handleInt(x) case string: handleString(x) }}高效写法:type Processor interface { Process()}type IntProcessor intfunc (i IntProcessor) Process() { handleInt(int(i)) }type StringProcessor stringfunc (s StringProcessor) Process() { handleString(string(s)) }// 调用方传入已知类型的 Processorp.Process() // 接口调用有轻微开销,但比类型断言稳定注意:接口调用仍有虚表查找开销,但在类型分支复杂时可能更优。6. 利用编译器优化:保持简单、可内联的函数确保 AsInt() 这类访问函数足够简单,以便编译器内联:// 好:简单、无循环、无 deferfunc (v Value) AsInt() (int, bool) { return v.iVal, v.kind == kindInt}// 差:复杂逻辑阻碍内联func (v Value) AsInt() (int, bool) { if v.validate() && v.kind == kindInt && v.checkRange() { return int(v.iVal), true } return 0, false}可通过 -gcflags="-m" 查看是否被内联:go build -gcflags="-m -m" .7. 避免在热路径中创建 interface{} 值每次将具体类型赋值给 interface{} 都会产生“装箱”(boxing):分配堆内存存储类型信息和值副本。var x interface{} = someInt // 可能触发逃逸分配尽量让 interface{} 只出现在 API 边界,内部使用具体类型。8. 使用 sync.Pool 复用临时对象(仅当分配是瓶颈时)如果必须频繁创建包含 interface{} 的对象(如解析器中间结构),可用 sync.Pool 减少 GC 压力:var parserPool = sync.Pool{ New: func() interface{} { return &Parser{} },}p := parserPool.Get().(*Parser)// 使用 pparserPool.Put(p) 注意:不要过早优化,先通过 pprof 确认分配是瓶颈。总结:关键原则目标方法消除类型断言泛型、预解析、类型标签结构体避免 reflect代码生成、专用解析器减少 interface{} 使用限制在边界层,内部用具体类型利用编译时信息让类型在编译期确定,而非运行时猜测最终建议:Go 的性能优势在于“简单即快”。与其依赖运行时灵活性,不如在设计阶段明确类型边界。最好的运行时类型检查,就是没有运行时类型检查。
  • [行业前沿] 如何优化FlexibleValue的性能表现?
    要优化 FlexibleValue(即封装 interface{} 用于灵活解析 JSON 等场景的自定义类型)的性能表现,核心目标是减少运行时开销、降低内存分配、避免不必要的类型断言和反射。以下是具体可行的优化策略,按优先级排序:1. 避免在热路径中重复调用 AsXxx() 方法问题:每次调用 AsInt()、AsString() 都会执行类型断言,若在循环或高频函数中调用,累积开销显著。优化:提前解析并缓存结果。type ParsedConfig struct { timeout int debug bool name string}func ParseFromFlexible(cfg map[string]FlexibleValue) (*ParsedConfig, error) { var p ParsedConfig if v, ok := cfg["timeout"].AsInt(); ok { p.timeout = v } else { return nil, errors.New("timeout must be int or numeric string") } if v, ok := cfg["debug"].AsBool(); ok { p.debug = v } else { return nil, errors.New("debug must be bool or 'true'/'false'") } // 后续业务逻辑直接使用 p.timeout,不再访问 FlexibleValue return &p, nil}✅ 效果:将动态类型检查从“运行时多次”变为“初始化一次”。2. 在 UnmarshalJSON 中预判并标准化内部存储类型问题:原始 interface{} 可能包含 float64、string、bool 等,导致每次 AsInt() 都要处理多种分支。优化:在反序列化时就将值转换为统一内部表示。例如,若字段主要用于整数,可尝试在解析时就转成 int64 并标记类型:type valueType intconst ( typeUnknown valueType = iota typeInt typeString typeBool)type FlexibleValue struct { typ valueType iVal int64 sVal string bVal bool}实现 UnmarshalJSON:func (f *FlexibleValue) UnmarshalJSON(data []byte) error { // 先尝试整数(包括字符串数字) if len(data) > 0 && data[0] != '"' { if v, err := strconv.ParseInt(string(data), 10, 64); err == nil { f.typ = typeInt f.iVal = v return nil } } // 尝试布尔 if bytes.Equal(data, []byte("true")) || bytes.Equal(data, []byte("false")) { f.typ = typeBool f.bVal = data[0] == 't' return nil } // 尝试布尔字符串 if s, err := strconv.Unquote(string(data)); err == nil { if b, err := strconv.ParseBool(s); err == nil { f.typ = typeBool f.bVal = b return nil } } // 默认作为字符串存储 s, err := strconv.Unquote(string(data)) if err != nil { return fmt.Errorf("invalid JSON value: %s", data) } f.typ = typeString f.sVal = s return nil}对应的访问方法变得极简:func (f FlexibleValue) AsInt() (int, bool) { if f.typ == typeInt { return int(f.iVal), true } return 0, false}效果:消除运行时类型断言;内存布局更紧凑(无 interface{} 的指针+类型信息);零堆分配(若值较小)。3. 使用 unsafe 或 encoding/json.RawMessage 延迟解析(高级)如果 FlexibleValue 仅在少数情况下被访问,可先保留原始 JSON 字节,延迟解析:type FlexibleValue struct { raw json.RawMessage parsed interface{} parsedOnce bool}首次调用 AsInt() 时才解析:func (f *FlexibleValue) ensureParsed() { if !f.parsedOnce { json.Unmarshal(f.raw, &f.parsed) f.parsedOnce = true }}但此法不减少总开销,仅推迟。适用于“多数字段不会被读取”的场景。 注意:仍存在 interface{} 开销,不如方案 2 彻底。4. 避免反射和通用工具函数不要写这样的通用转换函数:// 性能差,涉及 reflectfunc To[T any](v interface{}) (T, bool) { ... }Go 的泛型在运行时对 interface{} 转换仍需类型断言,且编译器难以优化。显式写 AsInt/AsString 更快。5. 减少内存分配:复用 buffer 和避免中间变量在 UnmarshalJSON 中:使用 []byte 直接比较,而非 string(data)(避免拷贝);用 bytes.Equal 判断布尔值;对字符串使用 strconv.Unquote 而非先 json.Unmarshal 到 string。示例:// 快if bytes.Equal(data, []byte("true")) { ... }// 慢(分配 string)if string(data) == "true" { ... }6. 考虑使用代码生成替代运行时判断对于固定字段集(如配置文件),可用工具(如 go generate + 模板)生成专用解析器,完全绕过 FlexibleValue。例如,定义 DSL://go:generate flexiblegen -type=AppConfig -fields=timeout:int,debug:bool,name:string生成的代码直接解析到具体类型,无任何 interface{}。适合结构固定的场景,性能接近手写解析。7. 基准测试验证优化效果使用 go test -bench 对比优化前后:func BenchmarkFlexibleValue_AsInt(b *testing.B) { fv := FlexibleValue{...} // 预设为整数 b.ResetTimer() for i := 0; i < b.N; i++ { _, _ = fv.AsInt() }}重点关注:ns/op(单次耗时);B/op(内存分配);allocs/op(分配次数)。理想情况下,优化后的 AsInt() 应接近 1–2 ns/op,0 allocs。最终建议:除非确实需要运行时灵活性,否则优先使用具体类型。若必须用 FlexibleValue,采用带类型标签的结构体代替 interface{} 是提升性能最有效的方式——它消除了运行时类型信息查询,使访问操作变成简单的字段读取,接近原生性能。
  • [行业前沿] 使用自定义类型封装interface{}有哪些注意事项?
    在 Go 中使用自定义类型封装 interface{}(例如 FlexibleValue、Any、Dynamic 等)是一种常见的灵活设计模式,尤其适用于配置解析、JSON 兼容处理或通用数据容器。但这种做法若不加约束,容易引入运行时错误、性能开销或可维护性问题。以下是主要注意事项:1. 必须提供安全的访问方法,禁止直接暴露内部 interface{}错误做法:type FlexibleValue struct { Data interface{} // 公开字段}// 外部直接断言if s, ok := fv.Data.(string); ok { ... }问题:所有调用方需自行处理类型断言,逻辑重复;一旦底层存储格式变化(如 JSON 数字存为 float64),所有调用点都可能出错。正确做法:封装访问器方法:func (f FlexibleValue) AsString() (string, bool)func (f FlexibleValue) AsInt() (int, bool)func (f FlexibleValue) AsBool() (bool, bool)这样将类型转换逻辑集中管理,后续修改只需调整方法内部。2. 明确支持的类型范围,避免“什么都接受”不要假设 interface{} 能处理任意输入。应在 UnmarshalJSON 中限制合法类型:func (f *FlexibleValue) UnmarshalJSON(data []byte) error { var v interface{} if err := json.Unmarshal(data, &v); err != nil { return err } switch val := v.(type) { case string, bool, float64, nil: f.data = v return nil default: return fmt.Errorf("unsupported type: %T", val) }}否则,像 []interface{} 或 map[string]interface{} 这类复杂结构可能被意外接受,导致后续 AsInt() 等方法静默失败。3. 注意 JSON 数字默认是 float64Go 的 encoding/json 将所有 JSON 数字解析为 float64,即使看起来是整数:{"value": 42}反序列化后 v 是 float64(42),不是 int。因此,在 AsInt() 中必须处理 float64:func (f FlexibleValue) AsInt() (int, bool) { switch v := f.data.(type) { case float64: if v == float64(int(v)) { // 检查是否为整数值 return int(v), true } case int: return v, true } return 0, false}同样,AsFloat() 应同时支持 float64 和能转为数字的字符串。4. 避免在热路径中频繁类型断言interface{} 的类型断言涉及运行时类型检查,虽快但非零成本。如果在高频循环中使用,应考虑:提前转换并缓存结果;或改用更具体的类型,而非通用封装。例如,不要在每帧渲染中调用 config.Value.AsFloat(),而应在初始化时解析一次。5. 实现 MarshalJSON 以保持对称性如果支持反序列化,也应支持序列化,否则行为不对称:func (f FlexibleValue) MarshalJSON() ([]byte, error) { return json.Marshal(f.data)}这样 json.Marshal 能正确输出原始值。6. 考虑零值和 nil 行为interface{} 的零值是 nil,但用户可能期望空字符串或 0。需明确定义:func (f FlexibleValue) IsEmpty() bool { return f.data == nil || (f.data == "" && reflect.TypeOf(f.data).Kind() == reflect.String)}或者在构造时统一规范化零值。7. 不要用于跨包公开 API 的核心参数在公共库的导出 API 中,尽量避免让用户面对 FlexibleValue。应提供明确的类型或选项模式:// 不推荐func NewClient(config map[string]FlexibleValue) (*Client, error)// 推荐type Config struct { Timeout time.Duration Debug bool Tags []string}func NewClient(cfg Config) (*Client, error)FlexibleValue 更适合内部解析层或配置加载器,而非业务接口。8. 测试覆盖所有支持的类型组合由于行为依赖运行时类型,必须编写全面的单元测试:tests := []struct{ input string expectInt int valid bool}{ {`42`, 42, true}, {`"42"`, 42, true}, {`42.5`, 0, false}, {`"abc"`, 0, false}, {`true`, 0, false},}特别注意边界值:0、""、null、-0、科学计数法等。9. 警惕内存分配每次 json.Unmarshal 到 interface{} 都会分配 map/slice/string,可能增加 GC 压力。在性能敏感场景,可考虑:使用 json.RawMessage 延迟解析;或预定义具体结构体,仅对个别字段使用柔性类型。10. 文档说明支持的类型和转换规则在类型注释中明确写出:// FlexibleValue 支持以下 JSON 类型:// - string → AsString()// - number → AsInt(), AsFloat()// - boolean → AsBool()// 字符串 "true"/"false" 也可用于 AsBool()type FlexibleValue struct { data interface{}}避免使用者猜测行为。 最佳实践:仅在确实需要类型灵活性的地方使用(如配置加载、第三方数据适配);封装完整的访问/验证/序列化逻辑;限制支持的类型集;充分测试 + 清晰文档。记住:灵活性不应以牺牲可预测性为代价。在 Go 的静态类型哲学下,interface{} 应是例外,而非默认选择。
  • [技术干货] Go中如何定义接口支持多种字段类型?
    在 Go 中,接口(interface)本身不直接“定义字段”,而是定义方法集合。但如果你希望某个结构体字段能接受多种类型(比如一个配置项可以是字符串、整数或布尔值),可以通过以下几种方式实现,核心思路是用接口类型作为字段,并配合自定义反序列化逻辑。方法一:使用 interface{} 字段 + 类型断言(简单但不安全)type Config struct { Value interface{} `json:"value"`}data := `{"value": 42}`var cfg Configjson.Unmarshal([]byte(data), &cfg)// 使用时需类型断言if v, ok := cfg.Value.(float64); ok { fmt.Println("int-like:", int(v))} else if v, ok := cfg.Value.(string); ok { fmt.Println("string:", v)}问题:JSON 中的数字默认解析为 float64,不是 int;没有验证,非法值也会被接受;使用繁琐,容易出错。方法二:定义接口 + 多个实现类型(推荐用于行为抽象)如果“多种类型”代表不同行为,可定义接口和具体实现:type Validator interface { Validate() error}type StringValue stringfunc (s StringValue) Validate() error { if s == "" { return errors.New("string is empty") } return nil}type IntValue intfunc (i IntValue) Validate() error { if i < 0 { return errors.New("int must be non-negative") } return nil}type Config struct { Item Validator `json:"item"`}但这样无法直接用 json.Unmarshal,因为 JSON 不知道该创建哪个具体类型。需要配合 自定义 UnmarshalJSON:func (c *Config) UnmarshalJSON(data []byte) error { var raw struct { Item json.RawMessage `json:"item"` } if err := json.Unmarshal(data, &raw); err != nil { return err } // 先尝试解析为字符串 var s string if err := json.Unmarshal(raw.Item, &s); err == nil { c.Item = StringValue(s) return nil } // 再尝试整数 var i float64 if err := json.Unmarshal(raw.Item, &i); err == nil { c.Item = IntValue(i) return nil } return errors.New("unsupported type for item")}使用:data1 := `{"item": "hello"}`data2 := `{"item": 100}`var cfg1, cfg2 Configjson.Unmarshal([]byte(data1), &cfg1)json.Unmarshal([]byte(data2), &cfg2)cfg1.Item.Validate() // 调用 StringValue 的 Validatecfg2.Item.Validate() // 调用 IntValue 的 Validate这种方式类型安全,且支持多态行为。方法三:使用自定义类型封装 interface{}(兼顾灵活性与安全性)定义一个统一类型,内部用 interface{} 存储,但提供安全访问方法:type FlexibleValue struct { data interface{}}func (f *FlexibleValue) UnmarshalJSON(data []byte) error { return json.Unmarshal(data, &f.data)}func (f FlexibleValue) AsString() (string, bool) { if s, ok := f.data.(string); ok { return s, true } return "", false}func (f FlexibleValue) AsInt() (int, bool) { if num, ok := f.data.(float64); ok { return int(num), true } return 0, false}func (f FlexibleValue) AsBool() (bool, bool) { if b, ok := f.data.(bool); ok { return b, true } // 支持字符串 "true"/"false" if s, ok := f.data.(string); ok { if b, err := strconv.ParseBool(s); err == nil { return b, true } } return false, false}结构体中使用:type Config struct { Timeout FlexibleValue `json:"timeout"` Debug FlexibleValue `json:"debug"`}解析和使用:data := `{"timeout": "30", "debug": true}`var cfg Configjson.Unmarshal([]byte(data), &cfg)if timeout, ok := cfg.Timeout.AsInt(); ok { fmt.Println("Timeout:", timeout)}if debug, ok := cfg.Debug.AsBool(); ok { fmt.Println("Debug:", debug)}优点:解析简单,无需复杂逻辑;访问时明确知道类型是否匹配;可扩展支持更多类型转换。方法四:使用泛型辅助函数(Go 1.18+)虽然不能直接让接口字段“自动适配”,但可以用泛型简化类型提取:func GetAs[T any](v interface{}, zero T) (T, bool) { if val, ok := v.(T); ok { return val, true } return zero, false}// 使用if timeout, ok := GetAs[int](cfg.Timeout.data, 0); ok { // ...}但依然需要内部存储为 interface{}。总结Go 中没有“字段支持多种类型”的直接语法,但可通过以下方式实现类似效果:如果只是存储和读取,用 interface{} + 安全访问方法(方法三)最实用;如果不同类型有不同行为,用 接口 + 多实现 + 自定义 Unmarshal(方法二)更面向对象;避免裸用 interface{} 而不做封装,否则容易引发 panic 或逻辑错误。关键原则:在解析时明确处理类型歧义,在使用时提供安全的访问接口。这样既能兼容灵活输入,又能保证程序健壮性。
  • [技术干货] Go 中避免在 JSON 反序列化时因字段类型不匹配导致的静默失败
    Go 中避免在 JSON 反序列化时因字段类型不匹配导致的静默失败在 Go 中使用 encoding/json 包解析 JSON 数据时,如果结构体字段类型与 JSON 值类型不一致,程序不会报错,而是将字段设为零值。这种“静默失败”行为容易掩盖数据问题,导致后续逻辑出错却难以排查。例如,有如下结构体和 JSON:type Config struct { Timeout int `json:"timeout"` Enabled bool `json:"enabled"` Name string `json:"name"`}data := `{"timeout": "30", "enabled": "true", "name": "app"}`var cfg Configerr := json.Unmarshal([]byte(data), &cfg)if err != nil { log.Fatal(err)}fmt.Printf("%+v\n", cfg)输出结果是:{Timeout:0 Enabled:false Name:app}尽管 JSON 中 timeout 和 enabled 都有值,但因为它们是字符串而非数字和布尔值,Unmarshal 无法赋值,于是直接跳过,字段保持零值。而程序没有报错,开发者可能误以为配置正确。这个问题在对接第三方 API 或读取用户输入的配置文件时尤其常见,因为对方可能用字符串表示数字或布尔值。解决方法之一是使用自定义类型实现 json.Unmarshaler 接口。例如,定义一个能同时接受整数和字符串的 IntField 类型:type IntField intfunc (i *IntField) UnmarshalJSON(data []byte) error { if len(data) == 0 { return nil } // 尝试解析为整数 if data[0] >= '0' && data[0] <= '9' || data[0] == '-' { var v int if err := json.Unmarshal(data, &v); err == nil { *i = IntField(v) return nil } } // 否则尝试解析为字符串再转整数 var s string if err := json.Unmarshal(data, &s); err != nil { return fmt.Errorf("cannot unmarshal %s into IntField", data) } v, err := strconv.Atoi(s) if err != nil { return fmt.Errorf("cannot convert string %q to int: %w", s, err) } *i = IntField(v) return nil}然后在结构体中使用:type Config struct { Timeout IntField `json:"timeout"` Enabled BoolField `json:"enabled"` Name string `json:"name"`}同样,可以定义 BoolField 支持字符串 "true"/"false" 和布尔值:type BoolField boolfunc (b *BoolField) UnmarshalJSON(data []byte) error { if len(data) == 0 { return nil } // 先尝试标准布尔值 var v bool if err := json.Unmarshal(data, &v); err == nil { *b = BoolField(v) return nil } // 再尝试字符串 var s string if err := json.Unmarshal(data, &s); err != nil { return fmt.Errorf("cannot unmarshal %s into BoolField", data) } switch strings.ToLower(strings.TrimSpace(s)) { case "true", "1", "on", "yes": *b = true case "false", "0", "off", "no": *b = false default: return fmt.Errorf("invalid boolean string: %q", s) } return nil}现在重新解析之前的 JSON:data := `{"timeout": "30", "enabled": "true", "name": "app"}`var cfg Configerr := json.Unmarshal([]byte(data), &cfg)if err != nil { log.Fatal(err)}fmt.Printf("%+v\n", cfg)输出变为:{Timeout:30 Enabled:true Name:app}而且如果传入非法值,比如 "timeout": "abc",会返回明确错误:cannot convert string "abc" to int: strconv.Atoi: parsing "abc": invalid syntax这种方式不仅提高了容错性,还增强了错误提示的可读性。对于已有项目,如果不想修改结构体字段类型,也可以在反序列化后做二次校验:func validateConfig(cfg *Config) error { raw := make(map[string]interface{}) // 假设原始 JSON 已保存 // 或者重新 Marshal 再 Unmarshal 到 map // 此处略去细节 return nil}但这种方法复杂且低效,不如直接使用自定义类型。另外,Go 1.10+ 的 json.Decoder 提供了 DisallowUnknownFields 方法,但不能解决类型不匹配问题。目前标准库没有选项能开启“严格模式”让类型不匹配时报错。因此,最实用的做法就是为易出错的字段定义支持多格式的自定义类型。这类自定义类型可以集中管理,形成一个小工具包:// flexible/bool.gopackage flexibletype Bool bool// 实现 UnmarshalJSON...// flexible/int.gopackage flexibletype Int int// 实现 UnmarshalJSON...在需要兼容多种输入格式的配置解析场景中,这种小封装能显著提升程序的健壮性。最后提醒:不要为了兼容而过度放宽类型。如果 API 文档明确规定 timeout 是整数,就应拒绝字符串形式,避免隐藏协议不一致的问题。只有在确实存在历史数据或第三方不可控输入时,才启用这种柔性解析。
  • [技术干货] Go 中使用 time.Ticker 时避免 goroutine 泄漏的小技巧
    Go 中使用 time.Ticker 时避免 goroutine 泄漏的小技巧在 Go 语言中,time.Ticker 常用于周期性执行任务,比如定时上报状态、轮询检查或定期清理缓存。很多开发者会这样写:ticker := time.NewTicker(5 * time.Second)go func() { for { select { case <-ticker.C: doSomething() } }}()这段代码看起来没问题,但如果程序运行一段时间后需要停止这个定时任务,或者所在的函数/结构体被销毁,这个 goroutine 就可能永远无法退出,造成 goroutine 泄漏。问题的核心在于:for 循环没有退出条件,ticker 也没有被正确关闭。一个常见的错误做法是只在主逻辑结束时调用 ticker.Stop(),但没有通知 goroutine 退出:// 错误示例func startWorker() { ticker := time.NewTicker(5 * time.Second) go func() { for { select { case <-ticker.C: doSomething() } } }() // 假设这里做些其他事情 time.Sleep(30 * time.Second) // 只 Stop ticker,但 goroutine 仍在运行 ticker.Stop()}即使 ticker 被 Stop,goroutine 仍然卡在 for 循环里,因为它没有收到任何退出信号。正确的做法是引入一个 done channel 来协调退出:func startWorker(ctx context.Context) { ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() go func() { defer fmt.Println("worker exited") for { select { case <-ctx.Done(): return case <-ticker.C: doSomething() } } }()}这里使用 context.Context 作为取消信号源。调用方可以通过 cancel() 或超时自动触发 ctx.Done(),从而安全退出 goroutine。如果不想引入 context,也可以自己创建一个 chan struct{}:func startWorker() (stop func()) { ticker := time.NewTicker(5 * time.Second) done := make(chan struct{}) go func() { defer close(done) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: doSomething() } } }() return func() { close(done) <-done // 等待 goroutine 真正退出 }}使用方式:stop := startWorker()time.Sleep(20 * time.Second)stop() // 安全停止这种方式明确表达了“启动-停止”的生命周期,且保证 goroutine 一定会退出。还有一种更简洁的写法,适用于不需要外部主动停止、只需随父作用域结束而结束的场景:func runPeriodicTask(duration time.Duration) { ticker := time.NewTicker(duration) defer ticker.Stop() // 使用 goroutine + 匿名函数 + defer 关闭通道 done := make(chan bool) go func() { defer close(done) for range ticker.C { doSomething() } }() // 模拟主逻辑运行 30 秒 time.Sleep(30 * time.Second) // ticker.Stop() 会在 defer 中自动调用 // 此时 for range 会因 ticker.C 关闭而退出}注意:time.Ticker 的 C 通道在调用 Stop() 后并不会被关闭,所以上面这种依赖通道关闭的方式其实不成立。这是个常见误解。实际上,time.Ticker.Stop() 只是停止发送时间事件,C 通道依然可读,但不会再有新值。因此 for range ticker.C 会一直阻塞在最后一次读取之后,不会自动退出。所以最可靠的方式仍然是显式使用 done channel 或 context。一个容易忽略的细节是:即使使用了 context,也要确保 ticker 在退出时被 Stop,否则底层 timer 不会被释放,造成资源浪费。完整推荐写法:func startPeriodicJob(ctx context.Context, interval time.Duration) { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: doSomething() } }}// 调用ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()startPeriodicJob(ctx, 5*time.Second)这种写法没有额外的 goroutine,直接在当前 goroutine 中运行循环,由 context 控制生命周期,既简洁又安全。如果确实需要在后台运行,再包装一层 goroutine 即可:func RunBackgroundJob(ctx context.Context, interval time.Duration) { go func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: doSomething() } } }()}关键点始终是:每个 ticker 必须配对一个 Stop,每个 goroutine 必须有明确的退出路径。在实际项目中,这类定时任务常封装在结构体中:type Worker struct { ticker *time.Ticker ctx context.Context cancel context.CancelFunc}func NewWorker(interval time.Duration) *Worker { ctx, cancel := context.WithCancel(context.Background()) return &Worker{ ticker: time.NewTicker(interval), ctx: ctx, cancel: cancel, }}func (w *Worker) Start() { go func() { defer w.ticker.Stop() for { select { case <-w.ctx.Done(): return case <-w.ticker.C: doSomething() } } }()}func (w *Worker) Stop() { w.cancel()}这样使用者只需调用 worker.Stop(),就能确保资源被正确清理。总之,在 Go 中使用 time.Ticker 时,不要只关注“开始”,更要设计好“结束”。一个小小的 done channel 或 context,就能避免潜在的 goroutine 泄漏问题。
  • [技术干货] 了解项目的需求分析及拆解、手动测试开发最小可行产品
    一、项目需求分析及拆解项目案例在写代码之前做需求分析,明白写什么,清晰的判断出来写的对不对,不然就白忙活了。首先需要梳理需求,根据第一篇文章写的。主要需求:当pytest执行结束之后,自动将结果发送到钉钉、微信、飞鼠、email等。不能指着一句话写代码,首先要对需求进行梳理,需要不断的抛出问题,结果包含什么?比如:希望pytest的结果包含什么呢?结合实际业务需求:测试开始时间(必有)测试结束时间测试执行时长测试用例总数量测试用例成功的数量测试用例失败的数量测试用例通过率测试报告地址等等 结合团队、环境根据实际开发。发送到“哪里”?根据实际情况发送到微信、钉钉、也可以扩展的比如说发展到推特、微博之类的。思考需求内容,是不同的场景,比如说:给钉钉发送消息呢?首先需要注册个钉钉号-钉钉号加自己号好友-通过消息对话框发送消息。另外方案:可以通过钉钉的群来发送,适用于发送给多人。 实现具体化。微信也是如此:个人、群、公众号。来了解发送到什么地方?怎么去发送?需要加好友、在手机或电脑发送好友消息。群的话不需要加好友用群的机器人发消息、通过接口发送消息。公众号也许要调用接口,前提是必须关注公众号才会发送内容。EMAIL通过SMTP服务器发送右键即可。什么时候发送?每次测试结束还是有用例失败?写代码之前分析需求,找产品经理确认需求再去做。比如说选择刚开始的:测试开始时间、测试执行时长、测试用例总数量、测试用例成功的数量、测试用例失败的数量、测试用例通过率。发送到群当中,使用机器人方式发送,选择而每次测试结束后发。二、编码思路编写伪代码,将需求思路捋一遍,保证代码正确。Pytest在运行时会调用很多hook,那我们需要哪些呢?根据分析来确定hook。比如:测试开始时自动执行,记录开始时间。测试结束时自动执行,记录结束时间,计算执行时长,发送结果。怎么去知道有多少个测试用例呢?需要hook,再收集完测试用例之后自动执行,记录总的用例数量。每一个用例执行结束之后自动执行,记录当前用例的结果。发送到什么地方需要用API,如果说发送到微信,只能用企业微信了,如果说发送到钉钉、飞书是提供相关接口的。以微信为例,在群当中添加机器人的名字后会出现接口和接口的说明。如图:​补充:通过EMAIL的SMTP协议来发送,所以我们需要SMTP的服务器、账号、链接代码实现。先写测试用例在写代码,比如说记录时间,怎么确定是记录到了呢?需要测试用hook确实得到了相关结果,保证产品交互。但是不可能写完所有代码后再去测试,这就缺少了全局观,不知道代码细节所以更偏向黑盒测试,但测试开发更偏向于白盒测试,所以用例需要在写代码之前或交互之前完成,先设计测试用例,分为俩个测试:单元测试:测试开始时间、执行时长,是否准确。用例执行结果,是否准确。用例数量,是否准确。用例通过率,是否准确。API或SMTP,是否可用。集成测试(满足整体需求):测试结束时,在微信群中,收到结果通知。通过单元测试判断做的某一些事情是否准确,通过集成测试来判断单元测试是否都完成并准确后才用到的,验证整个项目是否满足需求。三、编码从单元测试角度分析来编码,在之前plugin.py中已经完成了第一步,为了能够测试它需要写一个测试用例,如图:​设置代码的相应时长为2.5s,所以再2.5之后会完用例执行。在plugin中编写date时间,自动为它生成一个执行耗时,放到执行之后,打印出来时间。如图:  ​    ​单元测试,通过断言来判断花费时间,是否正确。如图:    ​运行pytest即可查看,是否正确,如果错了就会报错。验证了起始时间、结束时间、用例执行时间是准确的。用例执行结果,如果只有一个用例可能会报错,可以多写几个用例,正常来说是三个用例,俩个通过一个失败。如图:            ​检查用例数量是否正确,通过断言,assert来判断,数量是否等于3.,在收集完之后在判断,首先需要导入putest,进入之后找到所有hook,可以通过左下角的类和变量来快速找到参数,收集相关的并翻译,插件其实也可以对项目进行筛选和排序。在所有用例完成之后调用钩子,来证明所有用例收集之后结束。写钩子中的session是空函数,名字、参数、结果,参数是session包含了很多信息用例之类的,收集到的用例在什么地方呢?用例在完成之后执行,包含了全部的用例。如图:​​​如果涉及到了长周期的项目,怎么知道完成的定位?用例就是代表了要做的事情,数量就代表了工作的进度,用例的执行结果?通过断言来判断。如图:​执行的过程都会报错,不会让你把工作疏忽掉,其实也就间接的定位了工作进程,有用例就可以提醒什么地方没有做到位。这叫做测试驱动开发、开发进度、质量,下一步重点都是测试用例来把握。每一个用例执行结束之后自动执行,记录当前结果。在钩子中,名字、参数、返回值比较重要,没有返回值就看其余的。当有很多钩子选择时选择参数才是对的,logreport就是结果,找到runtest开头,report的,对钩子可以先翻译,有个基本的了解。如图:         ​三个用例为什么这么多结果,其实是每个用例分了三个部分,setup、调用、teardown阶段、其实在call就是调用阶段就可以知道是通过还是失败。加一个判断,如图:         ​做一个数据的记录,记录多少个失败多少个成功,修改断言处的pass为passed,fail为failed,并统计一下一共有多少个数量,在date当中设一个初始值,当某一个用例出现结果之后呢就会修改当中的相关的用例类型的数量         ​就是说本次用例如果通过就让结果统计的数据+1。,没有前面的报错说明结果是ok的,单元测试到此就已经测试通过了。测试通过率就不好写断言了,通过date来计算值。计算百分比也可以通过GPT来算如图:         ​         ​ 修改2f后加入%,上述代码66.66改为66.67.​单元测试中的接口API地址,在测试中设置url请求地址,查看后是发送json的请求,所以首先导入requests,并在pdm后安装requests不需要加-d。可以在项目的配置文件中查到requests的版本号是多少,确定了确实存在这个东西。这样在安装时候才能自动下载安装。如图:​​根据手册中的内容是发送post请求,比如说requests.post中,用普通文本的类型来实现,可以先测试下hello,world如图:​结果就是群里面发送了相关信息,证明了API是可用的。机器人说明当中还可以艾特某人、markdown格式。里面可能带字体颜色、语法。如图:        ​也可以自己建一个mkdown,md文件来写内容,比如说pytest自动化测试结果,需要测试时间、用例数量、执行时长、测试通过、测试失败、测试通过率、测试报告地址:http://baidu.com 如图:​               也可以通过加一个标签或mkdown的形式来体现测试失败的样子。颜色自己改如图:​           将写好的代码放到.py测试文件中,并将content内容放到下方的content。如图:​​        集成测试。我们执行pytest,客户可以直接收到结果,集成不太方便自动化,将接口请求代码和它进行集成。将.py的代码复制到plugin中去,并把import放到data上面去,将测试时间放到下方的测试时间中去,加一个f用一个花括号来替代并把所需要发送的内容依次替代即可。注意百分号如果加在双引号里面则后续不用修改,如果百分号加在外面,后面的也需要加百分号并加在外面。如图:​​​      补充:在这里可以去掉换行,否在影响发送效果,执行时间可以进一步的优化。可以删掉s,优化毫秒。如图:​先pdm check 优化代码、检查代码质量赶快commit提交!!!备注随便写,可以获取必要数据并发送到企业微信中去。如果需要严格的验证可以pdm build打包出来,下方会出现安装包,使用全新的环境来安装安装包,来模拟全新的使用和环境的电脑。如图:​报错问题是因为版本号:可以pip install 后再卸载​​​这次运行就可以啦~完美 撒花 结束!
  • [技术干货] 手把手教你如何使用git来建立代码仓库、三大工具的使用方法?
    一、创建一个新的插件:项目为单位的管理模式直接新建项目步骤:1:创建新的项目2:创建虚拟环境 venv3:安装pdm 自行安装4:使用我们的pdm来初始化项目  新建项目指定项目的位置,然后我们创建pytest插件约定一定要pytest-开头,创建一个结果通知插件 自动将整个pytest测试结果通过钉钉、飞书方式发送出去名字建立:  输入pdm init 输入y安装插件 后默认即可回车就可以了。如图:  手动创建目录src存放项目源码和tests存放测试用例,写的插件、项目都要有对应的单元测试用例,创建pythonpackage包 再python当中你的包名不可以出现+-号,名为pytest_result_sender。Pyproject.toml是项目源码,项目源码当中介绍 插件名字文本号或作者以及依赖还可以添加插件的入口点并指定源码再src中如图:项目创建就ok了。2:编写代码:举个简单例子,新建python文件创建插件文件再plugin.py中写hook,比如首先在,From datetime import datetime是生成当前实践Def pytest_configure():#会在配置加载完毕之后执行,也就是再测试用例前面执行Print(f”{datetime.now()}”pytest开始执行)Def pytest_unconfigure():Print(f”{datetime.now()}”pytest结束执行)再所由用例之前执行这个代码如图实现最终效果:二、测开平台之企业级项目开发实战篇1.建立git仓库跟踪代码更新进行版本控制2.使用black+iosrt的进行代码质量控制3.项目需求分析梳理及拆解设计测试用例4.测试驱动开发完成mvp最小化可行产品1.使用git进行代码版本的控制使用git不仅仅是把他存起来,托管,也是通过git方式来跟踪代码变化,防止代码写错或bug、删掉代码如何找回呢?都是通过该控制工具实现。使用方法:1:初始化仓库Git init项目目录在什么地方就在这个地方执行命令即可。仓库不是源码的一部分而是将源码放到仓库中,从代码变化到仓库变化。会将仓库内容保存到git中,使用俩种方式实现内容分割,.git代表隐藏、也可以通过右键查看是否为隐藏文件。如果仓库已经创建再去执行也可以,如图所示:2.将代码放到仓库当中安装git时可以右键git gui 通过使用git自带的界面。如图所示。来对内容进行控制,初始化项目之后也可以在上面的菜单栏当中点击git的按钮里面也有很多东西来修改。删除.git文件后会发现无法使用git命令。如图所示:1)Git gui页面介绍画红色圈圈地方是没有保存到仓库文件,但是已经提交了。下方的四个按钮第一个是扫描,第二个是存储,第三个是签名,第四个是提交,第五个是push。推到远程仓库当中。可以每个按钮按一遍就知道怎么操作了,可以通过提交历史来查看第一次代码的提交。注意:忽略配置是不关心不重要不希望加到代码仓库中比如说:pycharm编译器里面东西,预编译文件等。让我们的git忽略掉它,对它视而不见有俩种方法:第一种就是将需要的文件选中到代码仓库,但是有点笨,而且不好拿捏哪个是忽略的配置第二种是配置文件在pycharm中在pycharm根目录中创建一个git忽略文件,如图所示。里面对编译文件、扩展文件、虚拟环境,写到这个文件中的内容是不要的,写完后在ui界面重新扫描下会发现文件就少了很多,如图所示。只剩下了gt的忽略文件,项目配置文件、源码等等。忽略了:ide、python的预编译文件就是pyc解位的文件、各种临时文件、包括还有我们的打包编译文件等等。这些都可以直接拿来用就不用自己写了。如何查看自己删除掉的代码呢?可以将上方写的plugin.py下方的代码库删掉后点击下方的log日志查看。如图所示。就可以看到上面的文件下写着在plugin.py下删除了右侧的代码。然后再ui界面来备注下:清空掉了代码内容 点击commit提交这地方就是清空掉了,这时候便是入库存储起来了,后续文件的修改需求也可以这么做,如果有一天后悔了这么改可以回滚回来吗?是可以的,不需要备份!怎么找回之前删除的代码呢?比如说要找plugn的文件可以选中文件后再编辑页右键选中git再选中show diff 显示版本之间的差异,下方可以选中历史,比如说再三分钟或俩分钟之前是什么样的,可以回滚操作,选中要回滚的时间,右键选中showdiff,修改上方的行参数来回退到之前版本,或再git中选中resethead如图所示。将版本号再右下方带有数字放到head中,选择head方式来reser重置,代码就回来了。             2)回退版本   Reset –head 再不同版本之间随便跳跃比如将来代码中出现bug,不知道怎么修改可以回退到上一个没有bug的版本,保证系统正常,主要是来记录代码和遇到突发状况可     以实现版本回退,多个版本之间可以比较版本之间的差异。用git来实现代码的控制,为了确保代码有备份能控制可回退:一定要及时的commit!如果没有commit代码而     是直接reset代码就会丢失。   注意:一定要及时commit!!!2.使用black+iosrt代码的质量控制和之前的代码版本控制,改了什么怎么样改回来,这里更注重代码不能随便的写,由于每个人可以写出不同的风格所以要求质量控制,通过使用三件套black、isort、flake8来进行代码的质量控制,保证每个人的代码质量、风格等接近。Pdm是全项目的管理不仅仅用到安装使用工具时首先安装依赖Pdm add 要安装的东西即可 和pytest同理 但是不能这样去做,我们安装了以来但是使用这些的人需要安装这样工具吗?不需要。我们写插件要用到这种东西但是不能强加给使用框架或插件的人,应该怎么办?在开发依赖后加-dPdm add black iosrt flake8 -dPdm add pytest 叫运行依赖什么叫运行依赖?运行这个软件或系统就要安装pytest,开发依赖顾名思义就是在开发中需要而不是在终端用户中用到,所以不会安装。安装如图:    三种工具之间的角色:Black::不妥协的代码格式化,让每一个开发的代码形成统一的风格标准。在下方输入 black 文件名字回车就会发现代码被格式化了,如图所示。    Iosrt:对import语句进行排序 用法同理 black。导入的规则:          导入系统内置模块、第三方模块随后是自己写的模块。Flake8:代码质量分析      主要是检测代码中潜在的bug,怎么解决bug,分析什么变量、导入什么模块、在什么地方使用模块、使用的方式对不对合不合理进行分析。用法同理black。上面三个工具对代码进行修改和质量检查避免有很多坑,对多个文件怎么使用工具呢?配置脚手架让它自动执行,在project.toml中去写,代码如下:    创建check会自动调用这三个工具,并创建flake文件让它在检查时可以将不必要文件进行排除。    这时候在命令行中敲入:pdm check 就会自动的检查所有的文件,如图所示。    提示的地方可能是代码的错误问题,需要自行修改在修改完后再次执行会发现没有任何错误提示,就是搞定了。代码项目配置好后可以保存项目修改,点击git后点击commit。在文件下方也可以输入备注信息来区分,可以通过下方git查看保存信息。
  • [技术干货] 了解pytest插件机制、定制思路及hook常用插件清单
    一、了解hook清单方式、查看、翻译的方式有哪些: 接着上回说的pytest的hook清单是什么?怎么去查看hook清单呢?如何使用hook清单呢?下面小编就为大家来解析下。  hook清单又叫钩子函数,在pytest_的函数当中都是空的,是为了提供了名字、参数、返回值来为插件使用,其实也是hook支持的插件机制,查看hook清单的方法有俩种:    方法一:查看源码  在hookspec.py中可以查看到所有的约定。如图:               方法二:查看pytest官方文档:https://docs.pytest.org/en/7.3.x/reference/referece.html#hooks                    文档从介绍pytest-收集测试用例-执行测试用例-断言-报告-调试阶段,来具体分析。二、查找到的hook清单源码如何翻译及获取?  源码翻译和上述介绍的方法是对应的,第一种方法介绍的是再软件上查看hook清单可以选择钩子后右键add....输入“翻译下方内容”即可。第二种方法是浏览文档后在文档中进行翻译,右键选中进行翻译即可,直接看到中文内容扫清了学习障碍。看到的hook都是pytest本身,在安装第三方插件后,如果插件提供新的hook内部是无法看到的 需要通过源码的方式去动态获取,看到的hook清单都是pytest 插件的hook并没有体现需要动态获取。三、如何使用hook有哪些规则?     1: 被动的调用:          比如说在项目中创建conftest.py文件实现hook函数编写,编写测试用例,定义函数并没有执行函数所以print无法打印输出结果是空。可以直接使用pytest会发现函数被调用,是由pytest在执行过程中调用函数才能输出,这就是被动的调用不需要做什么,只需要知道hook名字创建同名函数等待被调用即可。          代码如下图所示:                   2:掌握主动         很多插件可以改变pytest原有的运行过程和运行结果,所以是掌握主动,完全改变运行的流程顺序和结果,比如创建hook后不想按照原有方式执行可以在前面加一个装饰器,@pytest为什么加装饰器呢?实际上按照我们插件的机制规则和系统规则每一个hook都要加一个装饰器,如果用简单的用法可以不加装饰器,高级用法加装饰器,在装饰器中加上参数trelast=true 。如图所示:                  如果去掉装饰器就会被调用,说明作为一个钩子来讲能不能被调用自己可以说的算,除了自己可以不被调用之外,还可以修改tryfrist后加一个return “泽宇李真帅”发现框架不被执行。如图所示:                 Pytest要不要执行怎么执行执行结果是什么由我自己代码说的算,就是说做什么返回什么,可以说对一些结果的串改,结果的串改是比较粗放的,修改效果也是一点不够高雅或细致,可以做一些酷炫效果,比如:        新建一个测试用例 test_api,Def  test_apiO: Assert 1==0断言,输入pytest后再文件中找到并执行测试用例,结果是断言失败是因为1不等于0?测试开发的能力是可以深度的控制和改变我们框架的执行过程,上方的用例如何从测试失败变成测试通过再不改变代码的情况下,通过hook方式进行修改,修改是非常的细致化,相对比被动调用是简单的,       3:完全控制的方式及步骤:     首先创建个函数  如图:         1:添加装饰器 @pytest.hookmpl(hookwrapper=true)以一个包装器的方式来运行         2:def 函数: print(“用例开始执行”)——前置         3:yield关键字  outcome=yield 因为是生成器 将结果保存再outcome         4:print(‘用例执行结束’)——后置 用例执行之后         5:修改俄式用例 print(“修改测试用例为测试结果为:通过”)         6:outcome.force_result(1)即可将失败换位passed     结果就是,断言1==0 改成了测试通过,根本不关心1还是0 ,根本就是我们改变了框架运行逻辑。四、断言失败和成果的标准:  测试通过的1或0都可以 为什么写任何数字都可以,0和1都代码通过呢?什么代表失败呢?从通过改失败是怎么改的呢?标准是怎么定呢?Pytest中,什么是测试通过的标准,什么是测试失败的标准?怎么判定呢?   测试失败的标准有三种:      断言异常就会判定为测试失败      Setup异常:判断测试出错      没有异常:判断测试通过  如何把测试通过的用例修改为测试失败呢?比如说,Assert 1==1 断言测试,如果没有钩子(函数)的话测试是通过的,用钩子来修改为失败这里需要添加断言。比如 assert_false 引发断言异常 被判定为失败,这就是框架里面东西  使用框架的都要掌握 测开和自动化测试都需要掌握。
  • [技术干货] 什么是pytest,手把手教你搞定pytest的上线与发布
    一:介绍pytest岗位及区别  伴随着人工智能的兴起,自动化测试和测试开发都需要掌握pytest框架,说到这肯定有同学好奇,都是要掌握pytest架构为什么还分为俩个岗位呢?这里我们就聊聊二者的本质区别:  1:使用pytest   测试用例的编写人员        测试框架的维护人员 配置 消除错误提示         扮演使用者的角色 根据需求编写用例代码2:维护pytest        Pytset核心开发人员  Pytest插件开发人员        更多关心pytest内部原理 怎么样添加功能或修改bug  扮演开发角色二:了解pytest架构及查看方法、特点  综上所述可以看到二者的本质区别,那pytest都可以满足我们的什么需求呢?小编在这里列举了一些比如:失败重新、并发执行、用例排序、生成报告、统计覆盖率、发送邮件等等都是通过pytest中插件实现 ,所以说为什么要很好的掌握pytest架构呢?其实就是要挑战年薪百万,Pytest不仅仅是框架,pytest本身是多个插件组合而成,从pytest源码来学习测试开发来了解其功能。  查看源码方法:         在pycharm中导入:import pytest              输入:pytest.main()函数 同时按下F4来查看pytest源码。          可以看到左边的目录下有大量的文件,大部分文件都有函数且以 pytest_开头 所以说是 由pytest插件来构成。   pytest特点:            1:包含大量插件。            2:pytest中的main函数为入口点,来执行pytest下的main函数 真正执行的是 config。。。和判断try...expert。            3:加载配置            4:执行hook            5:返回exitcode让函数结束执行   注意:main入口点就是调用插件所以说pytest本身就是个插件。            可以理解为pytest框架就是由N个插件堆积起来的,所以相对pytest更深入的了解和开发就需要掌握pytest架构。三:掌握pytest搭建测试平台思路   如何搭建一个测试平台呢?            1:用django在线编辑excel、yaml文件            2:pytest读取和执行excel、yaml文件,生成我们的调试报告、日志记录。            3:用pytest加载yaml文件 生成了 日志和报告                       查看接口请求日志 生成alure的报告 报告可以选用网页报告 放到django中             4:用django来在线展示测试结果和测试报告注意:allure由ajango提供支持http协议才可以查看报告信息。             综上所述,pytest是一个插件系统,因为具备插件系统的pluggy可以使插件之间相互配合。pluggy基本原理:       pytest的源码当中可以看到pytest_这些东西很重要就是插件一个约定       1:声明一个函数,称之为hook 不会执行 只用作声明。       2:软件运行的过程中 会自动的调用hook。       3:插件会按照hook的方式 实现函数,被软件自动的调用。       插件成为了软件的一部分所以可以自动的调用,插件的特点可以加入也可以退出可以把hook理解成约定 插件是按照约定想来就来想走就走  软件是陪着插件和hook到最后 运行环境 插件系统,查看系统的前提条件是一定要有约定  有约定按照约定进行即可 约定就是hook。总结:       1:深入掌握pytest的必经之路       2:测试开发的标志       3:解决项目需求的技术储备       4:搭建测试平台必备的基础综上几步可以来搭建测试平台是非常容易的,但是在pytest执行过程中用到很多插件比如:anyio处理异步、xdist读取excel文件、allure生成报告、多个插件和pytest本身配合来实现。通过插件的方式来扩展功能。
  • [技术干货] 详解Django中CSRF和CORS的区别【转】
    一、CSRF:保护机制Django预防CSRF攻击的方法是在用户提交的表单中加入一个csrftoken的隐含值,这个值和服务器中保存的csrftoken的值相同,这样做的原理如下:1、在用户访问django的可信站点时,django反馈给用户的表单中有一个隐含字段csrftoken,这个值是在服务器端随机生成的,每一次提交表单都会生成不同的值2、当用户提交django的表单时,服务器校验这个表单的csrftoken是否和自己保存的一致,来判断用户的合法性3、当用户被csrf攻击从其他站点发送精心编制的攻击请求时,由于其他站点不可能知道隐藏的csrftoken字段的信息这样在服务器端就会校验失败,攻击被成功防御二、CORS:跨域访问举例:前端和后端分别是两个不同的端⼝前端:127.0.0.1:8081后端:192.168.17.129:8880现在,前端与后端分别是不同的端⼝,这就涉及到跨域访问数据的问题,因为浏览器的同源策略,默认是不⽀持两个不同域名间相互访问数据,⽽我们需要在两个域名间相互传递数据,这时我们就要为后端添加跨域访问的⽀持。django后端设置:1、使用django-cors-headers扩展 a、安装1pip install django-cors-headersb、添加子应用12345INSTALLED_APPS = [     ...     'corsheaders',     ...]c、中间件配置1234MIDDLEWARE = [     'corsheaders.middleware.CorsMiddleware',     ... ]d、添加白名单12345678910# 设置CORS⽩名单CORS_ORIGIN_WHITELIST = (     'http://127.0.0.1:8081',    'http://127.0.0.1:8080',     'http://localhost:8080',     'http://www.nagle.cn:8080',     'http://api.nagle.cn:8083',) CORS_ALLOW_CREDENTIALS = True # 允许携带cookie凡是出现在⽩名单中的域名,都可以访问后端接⼝CORS_ALLOW_CREDENTIALS 指明在跨域访问中,后端是否⽀持对cookie的操作。2、跨域实现流程 a、浏览器会第一次先发送OPTIONS请求询问后端是否允许跨域,后端查询白名单中是否有这个域名 b、如果域名在白名单列表中则响应结果中告知浏览器允许跨域 c、浏览器第二次发送POST请求,携带用户登录数据到后端,完成登录验证操作
  • [技术干货] Python包管理工具pip用法详解(转载)
    pip提供我们各色各样的软件(第三方库),而这些第三方库又可以给我们实现各种各样不同的功能,科学计算、画图、操作文件、聊天……我们可以通过Cmd终端、Pycharm、Jupyter三种平台使用pip安装这些第三方库。官方Python 第三方库软件包地址:PyPI · Python 包索引Anaconda,Conda,Pip的关系Anaconda是一个python发行版。软件发行版是在系统上提前编译和配置好的软件包集合, 装好了后就可以直接用。Conda是一个包管理器。包管理器是自动化软件安装,更新,卸载的一种工具。Conda,有命令”conda install”, “conda update”, “conda remove”, 所以很明显, conda是包管理器。Conda和Anaconda名字相似,但没有必然关系, 你可以不安装Anaconda的同时, 使用Conda安装和管理软件。Conda是一个通用的包管理器,当初设计来管理任何语言的包。所以用来管理python包当然也是绰绰有余。Conda 和 pip 目标并不相同, 只有小部分子集有交集有竞争关系:比如python包的安装和环境隔离。pip可以允许你在任何环境中安装python包,而conda允许你在conda环境中安装任何语言包(包括c语言或者python)。一、Pip介绍pip是Python包管理工具,可以通过命令行的方式安装、卸载、更新三方库,先来看看具体有哪些指令:1、常用指令pip help / pip -h:查看pip的所有指令信息pip install 库名:安装第三发库pip install django==1.10.0:后面可以用==号指定包的版本pip --default-timeout=100 install -U django==1.10.0:万能安装第三方库,增加延迟,添加管理员权限安装pip install –-upgrade/ -U 库名:更新第三方库pip uninstall 库名:卸载第三方库pip list / pip freeze:列举当前项目路径安装的所有的包pip show:查看已经安装的包的信息,如pip show django查看django的具体信息,pip show --files django查看django的所有文件pip freeze > requirements.txt:将项目目录下安装的所有包信息输出到requirements.txt文件中。pip freeze -r requirements.txt:读取requirements.txt文件中的包信息,安装所有包。这样先将项目的三方包版本信息保存在requirements.txt文件中,切换到不同的环境,还可以安装该文件的所有三方包。2、pip更新:pip可以自己更新自己pip install -U pip3、基本使用(以django包为例)1、安装django软件pip install django #最新版本2、安装具体版本软件pip install django==1.11.8 # 指定版本pip install 'django>=1.11.0' # 大于某个版本3、查看具体安装文件pip show --files django4、列出软件包清单pip list5、查看哪些软件需要更新pip list --outdated6、升级软件包pip install --upgrade django7、卸载软件包pip uninstall django8、Requirements文件安装依赖软件Requirements文件 一般记录的是依赖软件列表,通过pip可以一次性安装依赖软件包:pip freeze > requirements.txtpip install -r requirements.txt9、查看软件包信息pip show django10、搜索pip search django
  • [问题求助] 【openGauss】请问Django可以用openGauss数据库吗?
    请问Django可以用openGauss数据库吗?有无参考文章
  • [技术干货] 正确的理解和使用Django信号(Signals)
    Django 提供一个了“信号分发器”机制,允许解耦的应用在框架的其它地方发生操作时会被通知到。 通俗而讲Django信号的工作原理就是当某个事件发生的时候会发出一个信号(signals), 而监听这个信号的函数(receivers)就会立即执行。Django信号的应用场景很多,尤其是用于不同模型或程序间的联动。常见例子包括创建User对象实例时创建一对一关系的UserProfile对象实例,或者每当用户下订单时触发给管理员发邮件的动作。今天小编我就分享下如何正确使用Django的信号(signals)。 Django信号的一个简单例子假设我们有一个如下User模型,我们希望每次有User对象新创建时都打印出有新用户注册的提示信息,我们可以使用Django信号(signals)轻松实现。我们的信号发送者sender是User模型,每当User模型执行post_save动作时就会发出信号。此时我们自定义的create_user函数一旦监听到User发出的post_save信号就会执行,先通过if created判断对象是新创建的还是被更新的;如果对象是新创建的,就会打印出提示信息。# models.py12345678910111213141516from django.db import models from django.db.models import signalsfrom django.dispatch import receiver class User(models.Model):    name = models.CharField(max_length=16)    gender = models.CharField(max_length=32, blank=True) def create_user(sender, instance, created, **kwargs):     if created:         print("New user created!") post_save.connect(create_user, sender=User)在上例中我们使用了信号(post_save)自带的connect的方法将自定义的函数与信号发出者(sender)User模型进行了连接。在实际应用中一个更常用的方式是使用@receiver装饰器实现发送者与监听函数的连接,如下所示。@receiver(post_save, sender=User)读起来的意思就是监听User模型发出的post_save信号。123456789101112131415from django.db import models from django.db.models.signals import post_savefrom django.dispatch import receiver class User(models.Model):    name = models.CharField(max_length=16)    gender = models.CharField(max_length=32, blank=True) @receiver(post_save, sender=User)def create_user(sender, instance, created, **kwargs):     if created:         print("New user created!") 利用Django信号实现不同模型的联动更新我们再来看一个复杂一点的例子。我们有一个Profile模型,与User模型是一对一的关系。我们希望创建User对象实例时也创建Profile对象实例,而使用post_save更新User对象时不创建新的Profile对象。这时我们就可以自定义create_user_profile和save_user_profile两个监听函数,同时监听sender(User模型)发出的post_save信号。由于post_save可同时用于模型的创建和更新,我们用if created这个判断来加以区别。12345678910111213141516from django.db import modelsfrom django.db.models.signals import post_savefrom django.dispatch import receiver class Profile(models.Model):    user = models.OneToOneField(User, on_delete=models.CASCADE)    birth_date = models.DateField(null=True, blank=True) @receiver(post_save, sender=User)def create_user_profile(sender, instance, created, **kwargs):   if created:       Profile.objects.create(user=instance) @receiver(post_save, sender=User)def save_user_profile(sender, instance, **kwargs):    instance.profile.save() Django常用内置信号之前的例子中我们使用的都是post_save信号,即在模型调用save()方法后才发送信号。Django其它常用内置信号还包括:django.db.models.signals.pre_save & post_save在模型调用 save()方法之前或之后发送。django.db.models.signals.pre_init& post_init在模型调用_init_方法之前或之后发送。django.db.models.signals.pre_delete & post_delete在模型调用delete()方法或查询集调用delete() 方法之前或之后发送。django.db.models.signals.m2m_changed在模型多对多关系改变后发送。django.core.signals.request_started & request_finished Django建立或关闭HTTP 请求时发送。 如何正确放置Django信号的监听函数代码在之前案例中,我们将Django信号的监听函数写在了models.py文件里。当一个app的与信号相关的自定义监听函数很多时,此时models.py代码将变得非常臃肿。一个更好的方式把所以自定义的信号监听函数集中放在app对应文件夹下的signals.py文件里,便于后期集中维护。假如我们有个account的app,包含了User和Pofile模型,我们不仅需要在account文件夹下新建signals.py,还需要修改account文件下apps.py和__init__.py,以导入创建的信号监听函数。# account/signals.py123456789101112131415161718192021from django.db.models.signals import post_savefrom django.dispatch import receiverfrom .models import User, Profile   @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs):   if created:       Profile.objects.create(user=instance)   @receiver(post_save, sender=User) def save_user_profile(sender, instance, **kwargs):     instance.profile.save()# account/apps.py1234567from django.apps import AppConfig class AccountConfig(AppConfig):    name = 'account'     def ready(self):        import account.signals# account/__init__.py1default_app_config = 'account.apps.AccountConfig'