2023年4月

一、个人最近工作变化

我经历了可能很多技术人都会经历的过程,从一名纯粹的技术人员转型成为了一名技术管理

image

我已经不再单纯了,经历了从做好自己的事,搞定一个复杂模块和系统,到带领一个小团队,攻克一个个复杂项目,再到带领几十号人的技术团队,感觉很充实,个人的综合能力也在飞速提升;实现了从乘客到驾驶员的转变,项目管理、团队管理、沟通协作、战略视角等都得到了长足的发展。
最近开始重新定义自己3-5年职业计划,发现自己离技术好像有点太远了,并不是要自己去手码每一行代码,只是还是需要保持技术的热爱,对行业发展的敏锐;所以未来会基于技术能力板块做一次全身体检,用以应对未来快速变化的世界。
包括编程能力、数据库能力、运维管理、架构等方面。

作为自己编写时间最长的一门开发语言-.Net,我又重新翻开了他。

二、Framework、Net Core、.NET 时过境迁

Framework:爱你定格在4.8

2002 年 2 月 23 日最早的 .NET Framework 1.0 发布,终止于 2022 年 8 月 9 日发布的 .NET Framework 4.8.1 版本。
CLS、CTS、CLR这些终将被遗忘在风中...现在市面上还是有非常多的系统基于framework开发的
framework:基于windows的一种采用虚拟机运行的编程平台,无法跨平台,虽然有一些跨平台技术,但是整体支持并不是很好
image

.Net Foundation:.Net变革大脑

意识到仅仅在windows平台进行独秀已远远不够,微软开始规划自己的开源和跨平台之路,2014年成立了Net基金会,独立于微软运营。

.NET Foundation 官方地址:
https://dotnetfoundation.org/

重新统一的 .NET

那微软的跨平台和开源该如何做呢?这么多年的技术债务和框架想要一下子出个王炸来统一技术路线显然不够现实,所以微软来了一个“并驾齐驱,三马齐驰”方式,看下如下的一个时间线
image
Framework与.NetCore终将退出舞台,等到版本的维护期过后;未来是.NET的,而.NET将是我们大家的开源。
image

三、个人思考

技术更新迭代的速度非常之快,让我们应接不暇
世界格局岛链化,冲突不断,充斥各种新闻...
美联储加息,经济复苏缓慢,企业不景气,裁员不断...
AI、chatGPT蜂拥而至,让人感觉到焦虑...
是否存在中年危机,不敲代码的自己有时感觉不踏实...
非常多的变化充斥在周围,分享一篇《尼泊尔祈祷文》- 共勉

上帝
请赐予我平静,接受哪些我无法改变的
请赐予我勇气,去改变我能改变的
请赐予我智慧,分辨二者的区别

能改变的只有自己,让自己变强大,保持学习,终生成长 !!!

我只是学到的知识做了内化,感谢如下文章作者
framwork版本:
https://dotnet.microsoft.com/zh-cn/download/dotnet-framework
.net 版本:
https://dotnet.microsoft.com/zh-cn/download/dotnet
.net 资料:
https://learn.microsoft.com/zh-cn/dotnet/core/introduction
.net 资料:
https://learn.microsoft.com/zh-cn/docs/
博客:
https://zhuanlan.zhihu.com/p/569249475?utm_id=0

ExceptionHandler的作用

ExceptionHandler是Spring框架提供的一个注解,用于处理应用程序中的异常。当应用程序中发生异常时,ExceptionHandler将优先地拦截异常并处理它,然后将处理结果返回到前端。该注解可用于类级别和方法级别,以捕获不同级别的异常。

在Spring中使用ExceptionHandler非常简单,只需在需要捕获异常的方法上注解@ExceptionHandler,然后定义一个方法,该方法将接收异常并返回异常信息,并将该异常信息展示给前端用户。

ExceptionHandler的使用

说明:针对可能出问题的Controller,新增注解方法@ExceptionHandler,下面是一个基本的ExceptionHandler示例

@RestController
public class ExceptionController {
	
    @ExceptionHandler(Exception.class)
    public ResponseEntity<String> handleException(Exception ex) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("An error occurred: " + ex.getMessage());
    }
    @RequestMapping("/test")
    public String test() throws Exception {
        throw new Exception("Test exception!");
    }
}

在上面的示例中,我们定义了一个叫做
ExceptionController
的类,该类是一个
@RestController
注解的控制器,它包括一个可以产生异常的请求处理程序,一个用于捕获和处理异常的@ExceptionHandler方法。

@RequestMapping注解配置了一个名为“/test”的API,该API将抛出一个异常,该异常将由我们上面的ExceptionHandler进行处理。当请求“/test”时,Controller方法将引发异常并触发@ExceptionHandler方法。

在上面的@ExceptionHandler方法中,我们通过ResponseEntity将异常信息提供给客户端,HTTP状态码设置为500。这使客户端了解已发生错误,并能够在日志中记录异常信息以便日后调试。

总之,使用ExceptionHandler能够更好的掌控应用的异常信息,使得应用在发生异常的时候更加可控,并且更加容易进行调试

ExceptionHandler的注意事项

  • Controller
    类下多个
    @ExceptionHandler
    上的异常类型不能出现一样的,否则运行时抛异常。

  • @ExceptionHandler
    下方法返回值类型支持多种,常见的ModelAndView,@ResponseBody注解标注,ResponseEntity等类型都OK.

源码分析介绍

原理说明-doDispatch

代码片段位于:
org.springframework.web.servlet.DispatcherServlet#doDispatch

执行
@RequestMapping
方法抛出异常后,Spring框架
try-catch
的方法捕获异常, 正常逻辑发不发生异常都会走
processDispatchResult
流程 ,区别在于异常的参数是否为null .

	HandlerExecutionChain mappedHandler = null;
	Exception dispatchException = null;
	ModelAndView mv = null;
    try{
        //根据请求查找handlerMapping找到controller
        mappedHandler=getHandler(request); 
        //找到处理器适配器HandlerAdapter
        HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); 
        if(!mappedHandler.applyPreHandle(request,response)){ 
            //拦截器preHandle
            return ;
        }      
        //调用处理器适配器执行@RequestMapping方法
        mv=ha.handle(request,response); 
        //拦截器postHandle
        mappedHandler.applyPostHandle(request,response,mv);  
    }catch(Exception ex){
        dispatchException=ex;
    }
    //将异常信息传入了
    processDispatchResult(request,response,mappedHandler,mv,dispatchException) 

原理说明-processDispatchResult

代码片段位于:
org.springframework.web.servlet.DispatcherServlet#processDispatchResult

如果
@RequestMapping
方法抛出异常,拦截器的postHandle方法不执行,进入processDispatchResult,判断入参dispatchException,不为null , 代表发生异常,调用processHandlerException处理。

原理说明-processHandlerException

代码片段位于:
org.springframework.web.servlet.DispatcherServlet#processHandlerException
在这里插入图片描述

this当前对象指dispatchServlet,handlerExceptionResolvers可以看到三个HandlerExceptionResolver,这三个是Spring框架帮我们注册的,遍历有序集合handlerExceptionResolvers,调用接口的resolveException方法。

在这里插入图片描述

注册的第一个
HandlerExceptionResolver.ExceptionHandlerExceptionResolver
, 继承关系如下面所示。

在这里插入图片描述

原理说明-AbstractHandlerExceptionResolver

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#resolveException

在这里插入图片描述
这里AbstractHandlerExceptionResolver的shouldApplyTo都返回true, logException用来记录日志、prepareResponse方法,用来设置response的Cache-Control。
在这里插入图片描述
异常处理方法就位于doResolveException
在这里插入图片描述
在这里插入图片描述

注意:AbstractHandlerExceptionResolver和AbstractHandlerMethodExceptionResolver名字看起来非常相似,但是作用不同,一个是面向整个类的,一个是面向方法级别的。

原理说明-AbstractHandlerMethodExceptionResolver

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#shouldApplyTo

接口方法实现AbstractHandlerExceptionResolver的resolveException,先判断shouldApplyTo,AbstractHandlerExceptionResolver 和子类AbstractHandlerMethodExceptionResolver都实现了shouldApplyTo方法,子类的shouldApplyTo都调用父类AbstractHandlerExceptionResolver的shouldApplyTo.

父类AbstractHandlerExceptionResolver的shouldApplyTo方法.

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver#shouldApplyTo

Spring初始化的时候并没有额外配置 , 所以mappedHandlers和mappedHandlerClasses都为null, 可以在这块扩展进行筛选 ,AbstractHandlerExceptionResolver提供了setMappedHandlerClasses 、setMappedHandlers用于扩展。

doResolveException

代码片段位于:org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException
在这里插入图片描述
Spring请求方法执行一样的处理方式,设置argumentResolvers、returnValueHandlers,之后进行调用异常处理方法。
在这里插入图片描述

获取@ExceptionHandler

@ExceptionHandler的方法入参支持:Exception ;SessionAttribute 、 RequestAttribute注解、 HttpServletRequest 、HttpServletResponse、HttpSession。
在这里插入图片描述
@ExceptionHandler方法返回值常见的可以是: ModelAndView 、@ResponseBody注解、ResponseEntity。

getExceptionHandlerMethod方法

getExceptionHandlerMethod说明: 获取对应的@ExceptionHandler方法,封装成ServletInvocableHandlerMethod返回。

exceptionHandlerCache是针对Controller层面的@ExceptionHandler的处理方式,而exceptionHandlerAdviceCache是针对@ControllerAdvice的处理方式. 这两个属性都位于ExceptionHandlerExceptionResolver中。

在这里插入图片描述

ExceptionHandlerMethodResolver,缓存A之前没存储过Controller的class ,所以新建一个ExceptionHandlerMethodResolver 加入缓存中,ExceptionHandlerMethodResolver 的初始化工作一定做了某些工作。
在这里插入图片描述

resolveMethod方法

根据异常对象让 ExceptionHandlerMethodResolver 解析得到 method , 匹配到异常处理方法就直接封装成对象 ServletInvocableHandlerMethod ; 就不会再去走@ControllerAdvice里的异常处理器了,这里说明了。

在这里插入图片描述

resolveMethodByExceptionType根据当前抛出异常寻找 匹配的方法,并且做了缓存,以后遇到同样的异常可以直接走缓存取出
在这里插入图片描述
resolveMethodByExceptionType方法,尝试从缓存A:exceptionLookupCache中根据异常class类型获取Method ,初始时候肯定缓存为空 ,就去遍历ExceptionHandlerMethodResolver的mappedMethods(上面提及了key为异常类型,value为method,exceptionType为当前@RequestMapping方法抛出的异常,判断当前异常类型是不是@ExceptionHandler中value声明的子类或本身,满足条件就代表匹配上了;
在这里插入图片描述

可能存在多个匹配的方法,使用ExceptionDepthComparator排序,排序规则是按照继承顺序来(继承关系越靠近数值越小,当前类最小为0,顶级父类Throwable为int最大值),排序之后选取继承关系最靠近的那个,并且ExceptionHandlerMethodResolver的exceptionLookupCache中,key为当前抛出的异常,value为解析出来的匹配method.

全局级别异常处理器实现HandlerExceptionResolver接口

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {

    @Override

    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",ex.getMessage());
        return new ModelAndView("error",mmp);
    }
}
  • 使用方式: 只需要将该Bean加入到Spring容器,可以通过Xml配置,也可以通过注解方式加入容器;
    方法返回值不为null才有意义,如果方法返回值为null,可能异常就没有被捕获.

  • 缺点分析:比如这种方式全局异常处理返回JSP、velocity等视图比较方便,返回json或者xml等格式的响应就需要自己实现了.如下是我实现的发生全局异常返回JSON的简单例子.

public class MyHandlerExceptionResolver implements HandlerExceptionResolver {
    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("发生全局异常!");
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",ex.getMessage());
        response.addHeader("Content-Type","application/json;charset=UTF-8");
        try {
            new ObjectMapper().writeValue(response.getWriter(),ex.getMessage());
            response.getWriter().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ModelAndView();
    }
}

全局级别异常处理器@ControllerAdvice+@ExceptionHandler使用方法

用法说明:这种情况下 @ExceptionHandler与第一种方式用法相同,返回值支持ModelAndView,@ResponseBody等多种形式。

@ControllerAdvice
public class GlobalController {
    @ExceptionHandler(RuntimeException.class)
    public ModelAndView fix1(Exception e){
        System.out.println("全局的异常处理器");
        ModelMap mmp=new ModelMap();
        mmp.addAttribute("ex",e);
        return new ModelAndView("error",mmp);
    }
}
  • 方式一:提到ExceptionHandlerExceptionResolver不仅维护@Controller级别的@ExceptionHandler,同时还维护的@ControllerAdvice级别的@ExceptionHandler代码片段位于:
    在这里插入图片描述
    isApplicableToBeanType方法是用来做条件判断的,@ControllerAdvice注解有很多属性用来设置条件,
    basePackageClasses、assignableTypes、annotations等,比如我限定了annotations为注解X, 那标注了@X 的ControllerA就可以走这个异常处理器,ControllerB就不能走这个异常处理器。

现在问题的关键就只剩下了exceptionHandlerAdviceCache是什么时候扫描@ControllerAdvice的,下面的逻辑和@ExceptionHandler的逻辑一样了,exceptionHandlerAdviceCache初始化逻辑:

代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#afterPropertiesSet,afterPropertiesSet是Spring bean创建过程中一个重要环节。
在这里插入图片描述


代码片段位于:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache

在这里插入图片描述

ControllerAdviceBean.findAnnotatedBeans方法查找了SpringMvc父子容器中标注 @ControllerAdvice 的bean, new ExceptionHandlerMethodResolver初始化时候解析了当前的@ControllerAdvice的bean的@ExceptionHandler,加入到ExceptionHandlerExceptionResolver的exceptionHandlerAdviceCache中,key为ControllerAdviceBean,value为ExceptionHandlerMethodResolver . 到这里exceptionHandlerAdviceCache就初始化完毕。

Spring父子容器中所有@ControllerAdivce的bean的方法

代码片段位于:org.springframework.web.method.ControllerAdviceBean#findAnnotatedBeans

遍历了SpringMVC父子容器中所有的bean,标注ControllerAdvice注解的bean加入集合返回。

比较说明

@Controller+@ExceptionHandler、HandlerExceptionResolver接口形式、@ControllerAdvice+@ExceptionHandler优缺点说明:

调用优先级

  • @Controller+@ExceptionHandler
    优先级最高
  • @ControllerAdvice+@ExceptionHandler
    略低
  • HandlerExceptionResolver
    最低。

三种方式并存的情况 优先级越高的越先选择,而且被一个捕获处理了就不去执行其他的

三种方式都支持多种返回类型

  • @Controller+@ExceptionHandler、@ControllerAdvice+@ExceptionHandler可以使用Spring支持的@ResponseBody、ResponseEntity。

  • HandlerExceptionResolver方法声明返回值类型只能是 ModelAndView,如果需要返回JSON、xml等需要自己实现.。

缓存利用

  • @Controller+@ExceptionHandler的缓存信息在ExceptionHandlerExceptionResolver的exceptionHandlerCache,@ControllerAdvice+@ExceptionHandler的缓存信息在ExceptionHandlerExceptionResolver的exceptionHandlerAdviceCache中,

  • HandlerExceptionResolver接口是不做缓存的,在异常报错的情况下才会走自己的HandlerExceptionResolver实现类,多少有点性能损耗.

原文链接:
Go 语言切片是如何扩容的?

在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。

切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片是一种引用类型,它有三个属性:
指针

长度

容量

底层源码定义如下:

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}
  1. 指针:
    指向 slice 可以访问到的第一个元素。
  2. 长度:
    slice 中元素个数。
  3. 容量:
    slice 起始元素到底层数组最后一个元素间的元素个数。

比如使用
make([]byte, 5)
创建一个切片,它看起来是这样的:

声明和初始化

切片的使用还是比较简单的,这里举一个例子,直接看代码吧。

func main() {
    var nums []int  // 声明切片
    fmt.Println(len(nums), cap(nums)) // 0 0
    nums = append(nums, 1)   // 初始化
    fmt.Println(len(nums), cap(nums)) // 1 1

    nums1 := []int{1,2,3,4}    // 声明并初始化
    fmt.Println(len(nums1), cap(nums1))    // 4 4

    nums2 := make([]int,3,5)   // 使用make()函数构造切片
    fmt.Println(len(nums2), cap(nums2))    // 3 5
}

扩容时机

当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用
append
函数向切片中添加元素时。

扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。

需要注意的是,由于
扩容会分配新数组并复制元素,因此可能会影响性能
。如果你知道要添加多少元素,可以使用
make
函数预先分配足够大的切片来避免频繁扩容。

接下来看看
append
函数,签名如下:

func Append(slice []int, items ...int) []int

append
函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:

追加多个值:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    fmt.Println("初始切片:", s)

    s = append(s, 4, 5, 6)
    fmt.Println("追加多个值后的切片:", s)
}

输出结果为:

初始切片: [1 2 3]
追加多个值后的切片: [1 2 3 4 5 6]

再来看一下直接
追加一个切片:

package main

import "fmt"

func main() {
    s1 := []int{1, 2, 3}
    fmt.Println("初始切片:", s1)

    s2 := []int{4, 5, 6}
    s1 = append(s1, s2...)
    fmt.Println("追加另一个切片后的切片:", s1)
}

输出结果为:

初始切片: [1 2 3]
追加另一个切片后的切片: [1 2 3 4 5 6]

再来看一个
发生扩容
的例子:

package main

import "fmt"

func main() {
    s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片
    fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s)

    for i := 1; i <= 5; i++ {
        s = append(s, i) // 向切片中添加元素
        fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
    }
}

输出结果为:

初始状态: len=0 cap=3 []
添加元素1: len=1 cap=3 [1]
添加元素2: len=2 cap=3 [1 2]
添加元素3: len=3 cap=3 [1 2 3]
添加元素4: len=4 cap=6 [1 2 3 4]
添加元素5: len=5 cap=6 [1 2 3 4 5]

在这个例子中,我们创建了一个长度为
0
,容量为
3
的切片。然后,我们使用
append
函数向切片中添加
5
个元素。

当我们添加第
4
个元素时,切片的长度超过了其容量。此时,切片会自动扩容。新的容量是原始容量的两倍,即
6

表面现象已经看到了,接下来,我们就深入到源码层面,看看切片的扩容机制到底是什么样的。

源码分析

在 Go 语言的源码中,切片扩容通常是在进行切片的
append
操作时触发的。在进行
append
操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用
growslice
函数进行扩容。

growslice
函数定义在 Go 语言的 runtime 包中,它的调用是在编译后的代码中实现的。具体来说,当执行
append
操作时,编译器会将其转换为类似下面的代码:

slice = append(slice, elem)

在上述代码中,如果切片容量不足以容纳新的元素,则会调用
growslice
函数进行扩容。所以
growslice
函数的调用是
由编译器在生成的机器码中实现的,而不是在源代码中显式调用的

切片扩容策略有两个阶段,go1.18 之前和之后是不同的,这一点在 go1.18 的 release notes 中有说明。

下面我用 go1.17 和 go1.18 两个版本来分开说明。先通过一段测试代码,直观感受一下两个版本在扩容上的区别。

package main

import "fmt"

func main() {
    s := make([]int, 0)

    oldCap := cap(s)

    for i := 0; i < 2048; i++ {
        s = append(s, i)

        newCap := cap(s)

        if newCap != oldCap {
            fmt.Printf("[%d -> %4d] cap = %-4d  |  after append %-4d  cap = %-4d\n", 0, i-1, oldCap, i, newCap)
            oldCap = newCap
        }
    }
}

上述代码先创建了一个空的 slice,然后在一个循环里不断往里面
append
新元素。

然后记录容量的变化,每当容量发生变化的时候,记录下老的容量,添加的元素,以及添加完元素之后的容量。

这样就可以观察,新老 slice 的容量变化情况,从而找出规律。

运行结果(
1.17 版本
):

[0 ->   -1] cap = 0     |  after append 0     cap = 1   
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256 
[0 ->  255] cap = 256   |  after append 256   cap = 512 
[0 ->  511] cap = 512   |  after append 512   cap = 1024
[0 -> 1023] cap = 1024  |  after append 1024  cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1696
[0 -> 1695] cap = 1696  |  after append 1696  cap = 2304

运行结果(
1.18 版本
):

[0 ->   -1] cap = 0     |  after append 0     cap = 1
[0 ->    0] cap = 1     |  after append 1     cap = 2   
[0 ->    1] cap = 2     |  after append 2     cap = 4   
[0 ->    3] cap = 4     |  after append 4     cap = 8   
[0 ->    7] cap = 8     |  after append 8     cap = 16  
[0 ->   15] cap = 16    |  after append 16    cap = 32  
[0 ->   31] cap = 32    |  after append 32    cap = 64  
[0 ->   63] cap = 64    |  after append 64    cap = 128 
[0 ->  127] cap = 128   |  after append 128   cap = 256 
[0 ->  255] cap = 256   |  after append 256   cap = 512 
[0 ->  511] cap = 512   |  after append 512   cap = 848 
[0 ->  847] cap = 848   |  after append 848   cap = 1280
[0 -> 1279] cap = 1280  |  after append 1280  cap = 1792
[0 -> 1791] cap = 1792  |  after append 1792  cap = 2560

根据上面的结果还是能看到区别的,具体扩容策略下面边看源码边说明。

go1.17

扩容调用的是
growslice
函数,我复制了其中计算新容量部分的代码。

// src/runtime/slice.go

func growslice(et *_type, old slice, cap int) slice {
    // ...

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.cap < 1024 {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // ...

    return slice{p, old.len, newcap}
}

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于等于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

go1.18

// src/runtime/slice.go

func growslice(et *_type, old slice, cap int) slice {
    // ...

    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        const threshold = 256
        if old.cap < threshold {
            newcap = doublecap
        } else {
            // Check 0 < newcap to detect overflow
            // and prevent an infinite loop.
            for 0 < newcap && newcap < cap {
                // Transition from growing 2x for small slices
                // to growing 1.25x for large slices. This formula
                // gives a smooth-ish transition between the two.
                newcap += (newcap + 3*threshold) / 4
            }
            // Set newcap to the requested cap when
            // the newcap calculation overflowed.
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    // ...

    return slice{p, old.len, newcap}
}

和之前版本的区别,主要在扩容阈值,以及这行代码:
newcap += (newcap + 3*threshold) / 4

在分配内存空间之前需要先确定新的切片容量,运行时根据切片的当前容量选择不同的策略进行扩容:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  3. 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是
    newcap + 3*threshold
    ,直到新容量大于期望容量;

内存对齐

分析完两个版本的扩容策略之后,再看前面的那段测试代码,就会发现扩容之后的容量并不是严格按照这个策略的。

那是为什么呢?

实际上,
growslice
的后半部分还有更进一步的优化(内存对齐等),靠的是
roundupsize
函数,在计算完
newcap
值之后,还会有一个步骤计算最终的容量:

capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)

这个函数的实现就不在这里深入了,先挖一个坑,以后再来补上。

总结

切片扩容通常是在进行切片的
append
操作时触发的。在进行
append
操作时,如果切片容量不足以容纳新的元素,就需要对切片进行扩容,此时就会调用
growslice
函数进行扩容。

切片扩容分两个阶段,分为 go1.18 之前和之后:

一、go1.18 之前:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

二、go1.18 之后:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于阈值(默认 256)就会将容量翻倍;
  3. 如果当前切片的长度大于等于阈值(默认 256),就会每次增加 25% 的容量,基准是
    newcap + 3*threshold
    ,直到新容量大于期望容量;

以上就是本文的全部内容,如果觉得还不错的话欢迎
点赞

转发

关注
,感谢支持。


参考文章:

推荐阅读:

A
bp(net core)+easyui+efcore实现仓储管理系统目录

1.在Visual Studio 2022的解决方案资源管理器中,选中“ABP.TPLMS.Web.Mvc”项目,然后单击鼠标右键,在弹出菜单中选中“设为启动项目”。按F5运行应用程序。

2.在浏览器将呈现登录页面,然后输入管理员用户名进行登录。浏览器跳转到首页面。如下图。

3.在主界面的菜单中,选择“Business->Module”菜单项,浏览器中呈现一个模块信息列表页面,我们发现此页面的顶部与右边的菜单部分缺失css,样式不好看。如下图。

4. 在Visual Studio 2017的“解决方案资源管理器”中,右键单击在领域层“ABP.TPLMS.Web.Mvc”项目中的Views\Module目录。 找到Index.cshmtl文件,修改顶部的代码与按钮的代码。具体代码如下:

@using ABP.TPLMS.Web.Startup

@model ABP.TPLMS.Web.Models.Module.EditModuleModalViewModel

@{
ViewData["Title"] = PageNames.Module;
}

@section scripts
{
<scriptsrc="~/view-resources/Views/Module/Index.js"asp-append-version="true"></script>}<sectionclass="content-header">
<divclass="container-fluid">
<divclass="row">
<divclass="col-sm-6">
<h1>@L("Module")</h1>
</div>
<divclass="col-sm-4 text-sm-right">
<aid="RefreshButton"href="javascript:void(0);" ><iclass="fas fa-redo-alt"></i></a>
</div>
<divclass="col-sm-2">
<aasp-action="Create"class="btn bg-blue float-sm-right">
<iclass="fa fa-plus-square"></i>@L("Create")</a>
</div>
</div>
</div>
</section>
<divclass="row clearfix">
<divclass="col-lg-12 col-md-12 col-sm-12 col-xs-12">
<divclass="card">
<divclass="body table-responsive">
<tableclass="table">
<thead>
<tr>
<th>@Html.DisplayNameFor(model => model.Module.Name)</th>
<th>@Html.DisplayNameFor(model => model.Module.DisplayName)</th>
<th>@Html.DisplayNameFor(model => model.Module.HotKey)</th>
<th>@Html.DisplayNameFor(model => model.Module.IconName)</th>
<th>@Html.DisplayNameFor(model => model.Module.RequiredPermissionName)</th>
<th>@Html.DisplayNameFor(model => model.Module.Status)</th>
<th></th>

</tr>
</thead>

<tbody>@foreach (var item in Model.Modules)

{
<tr>
<td>@Html.DisplayFor(modelItem => item.Name)</td>
<td>@Html.DisplayFor(modelItem => item.DisplayName)</td>
<td>@Html.DisplayFor(modelItem => item.HotKey)</td>
<td>@Html.DisplayFor(modelItem => item.IconName)</td>
<td>@Html.DisplayFor(modelItem => item.RequiredPermissionName)</td>
<td>@Html.DisplayFor(modelItem => item.Status)</td>
<td>

<aasp-action="Edit"class="btn btn-sm bg-secondary"asp-route-id="@item.Id"><iclass="fas fa-pencil-alt"></i>@L("Edit")</a>

<aasp-action="Delete"class="btn btn-sm bg-danger"asp-route-id="@item.Id"><iclass="fas fa-trash"></i>@L("Delete")</a>

</td>
</tr>}</tbody>
</table>
</div>
</div>
</div>
</div>
 

5.在Visual Studio 2022中按F5运行应用程序。在浏览器将呈现登录页面,然后输入管理员用户名进行登录。浏览器跳转到首页面。

6.在主界面的菜单中,选择“Business->模块管理”菜单项,浏览器中呈现一个模块信息列表页面,如下图。

7.Business菜单已经添加到左侧菜单栏中,不过这个Business菜单好像没有英文名称,即没有英文名称也没有中文名称。模块管理的列表页面中也没有。如下图。


8.使用鼠标在页面中右上角处的语言选项选择了“简体中文”这一选项,其他菜单都跟随语言的变化而变化了,我们新增加的Business菜单和Module模块没有变化。如下图。

9. 在Visual Studio 2022的“解决方案资源管理器”中,找到“ABP.TPLMS.Core”项目中的Localization目录。 使用鼠标双击打开TPLMS.xml与TPLMS-zh-Hans.xml两个文件。如下图。

10. 在Visual Studio 2022的“解决方案资源管理器”中,打开TPLMS.xml与TPLMS-zh-Hans.xml两个文件。分别添加关于Business菜单和Module模块的英文与中文信息。如下图1与图2.。


图1  英文

图2 中文

11. 在Visual Studio 2022中按F5,运行我们的应用程序。在登录界面登录之后。当我们改变页面中右上角处的语言选项时,Business菜单和Module模块跟随语言的变化而变化了,当语言选择为中文时,菜单名称由“Business”变为“业务管理”,Module变为了“模块管理”。如下图。

一、简介

本文博主给大家讲解如何在自己开源的电商项目
newbee-mall-pro
中应用协同过滤算法来达到给用户更好的购物体验效果。

newbee-mall-pro
项目地址:


二、协同过滤算法

协同过滤算法是一种基于用户或者物品的相似度来推荐商品的方法,它可以有效地解决商城系统中的信息过载问题。协同过滤算法的实践主要包括以下几个步骤:

  1. 数据收集和预处理。这一步需要从商城系统中获取用户的行为数据,如浏览、购买、评价等,然后进行一些必要的清洗和转换,以便后续的分析和计算。
  2. 相似度计算。这一步需要根据用户或者物品的特征或者行为,采用合适的相似度度量方法,如余弦相似度、皮尔逊相关系数、Jaccard指数等,来计算用户之间或者物品之间的相似度矩阵。
  3. 推荐生成。这一步需要根据相似度矩阵和用户的历史行为,采用合适的推荐策略,如基于邻域的方法、基于模型的方法、基于矩阵分解的方法等,来生成针对每个用户的个性化推荐列表。
  4. 推荐评估和优化。这一步需要根据一些评价指标,如准确率、召回率、覆盖率、多样性等,来评估推荐系统的效果,并根据反馈信息和业务需求,进行一些参数调整和算法优化,以提高推荐系统的性能和用户满意度。


在原有的商城首页
为你推荐
栏目是使用后台配置的商品列表,基于人为配置。在项目商品用户持续增长的情况下,不一定能给用户推荐用户可能想要的商品。

因此在
v2.4.1
版本中,商城首页
为你推荐
栏目添加了协同过滤算法。按照UserCF基于用户的协同过滤、ItemCF基于物品的协同过滤。 实现了两种不同的推荐逻辑。

  • UserCF
    :基于用户的协同过滤。当一个用户A需要个性化推荐的时候,我们可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的,而用户A没有听说过的物品推荐给A。
    userCF.png
    假设用户 A 喜欢物品 A、物品 C,用户 B 喜欢物品 B,用户 C 喜欢物品 A 、物品 C 和物品 D;从这些用户的历史喜好信息中,我们可以发现用户 A 和用户 C 的口味和偏好是比较类似的,同时用户 C 还喜欢物品 D,那么我们可以推断用户 A 可能也喜欢物品 D,因此可以将物品 D 推荐给用户 A。具体代码在
    ltd.newbee.mall.recommend.core.UserCF
    中。

  • itemCF
    :基于物品的协同过滤。预先根据所有用户的历史偏好数据计算物品之间的相似度,然后把与用户喜欢的物品相类似的物品推荐给用户。
    itemCF.png
    假如用户A喜欢物品A和物品C,用户B喜欢物品A、物品B和物品C,用户C喜欢物品A,从这些用户的历史喜好中可以认为物品A与物品C比较类似,喜欢物品A的都喜欢物品C,基于这个判断用户C可能也喜欢物品C,所以推荐系统将物品C推荐给用户C。 具体代码在
    ltd.newbee.mall.recommend.core.ItemCF
    中。

三、推荐算法代码实践

3.1 数据收集和预处理


newbee-mall-pro
中,我们基于用户下单的商品数据进行收集和预处理。

/**
 * 根据所有用户购买商品的记录进行数据手机
 *
 * @return List<RelateDTO>
 */
@Override
public List<RelateDTO> getRelateData() {
    List<RelateDTO> relateDTOList = new ArrayList<>();
    // 获取所有订单以及订单关联商品的集合
    List<Order> newBeeMallOrders = orderDao.selectOrderIds();
    List<Long> orderIds = newBeeMallOrders.stream().map(Order::getOrderId).toList();
    List<OrderItemVO> newBeeMallOrderItems = orderItemDao.selectByOrderIds(orderIds);
    Map<Long, List<OrderItemVO>> listMap = newBeeMallOrderItems.stream()
            .collect(Collectors.groupingBy(OrderItemVO::getOrderId));
    Map<Long, List<OrderItemVO>> goodsListMap = newBeeMallOrderItems.stream()
            .collect(Collectors.groupingBy(OrderItemVO::getGoodsId));
    // 遍历订单,生成预处理数据
    for (Order newBeeMallOrder : newBeeMallOrders) {
        Long orderId = newBeeMallOrder.getOrderId();
        for (OrderItemVO newBeeMallOrderItem : listMap.getOrDefault(orderId, Collections.emptyList())) {
            Long goodsId = newBeeMallOrderItem.getGoodsId();
            Long categoryId = newBeeMallOrderItem.getCategoryId();
            RelateDTO relateDTO = new RelateDTO();
            ...
            relateDTOList.add(relateDTO);
        }
    }
    return relateDTOList;
}

3.2 相似度计算

在推荐算法中,相似度建立是一个非常重要的过程,它标志着算法准不准确,能不能给用户带来好的推荐体验。在
newbee-mall-pro
中,我们将用户之间下单的商品进行相似度计算,因为如果两个用户购买了同一个商品,那么我们认为这两个用户之间是存在联系并且都存在付费行为。

// 遍历订单商品
for (OrderItemVO newBeeMallOrderItem : listMap.getOrDefault(orderId, Collections.emptyList())) {
    Long goodsId = newBeeMallOrderItem.getGoodsId();
    Long categoryId = newBeeMallOrderItem.getCategoryId();
    RelateDTO relateDTO = new RelateDTO();
    relateDTO.setUserId(newBeeMallOrder.getUserId());
    relateDTO.setProductId(goodsId);
    relateDTO.setCategoryId(categoryId);
    // 通过计算商品购买次数,来建立相似度
    List<OrderItemVO> list = goodsListMap.getOrDefault(goodsId, Collections.emptyList());
    int sum = list.stream().mapToInt(OrderItemVO::getGoodsCount).sum();
    relateDTO.setIndex(sum);
    relateDTOList.add(relateDTO);
}

通过余弦相似度算法计算用户与商品之间的相似度,从而为用户推荐最相似的商品。当两个用户购买了同一个商品时,我们就认为两个用户产生了关联,因此针对两个用户购买的同一个商品进行相似度计算,来建立用户之间的相似度。

余弦相似度是一种用于衡量两个向量之间的相似度的方法,它通过计算两个向量的夹角的余弦值来得到。在商城系统中,余弦相似度可以用于实现基于内容的推荐算法,即根据用户的历史购买或浏览行为,为用户推荐与其兴趣相似的商品。具体来说,可以将每个商品表示为一个特征向量,例如商品的类别、价格、评分等,然后将每个用户表示为一个偏好向量,例如用户购买或浏览过的商品的特征向量的加权平均。这样,就可以利用余弦相似度来计算用户和商品之间的相似度,从而为用户推荐最相似的商品。

计算相关系数,传入用户ID或者物品ID,计算相似度

/**
 * 计算相关系数并排序
 *
 * @param key  基于用户协同代表用户id,基于物品协同代表武平id
 * @param map  预处理数据集
 * @param type 类型0基于用户推荐使用余弦相似度 1基于物品推荐使用余弦相似度
 * @return Map<Double, Long>
 */
public static Map<Double, Long> computeNeighbor(Long key, 
                          Map<Long, List<RelateDTO>> map, int type) {
    Map<Double, Long> distMap = new TreeMap<>();
    List<RelateDTO> items = map.get(key);
    map.forEach((k, v) -> {
        // 排除此用户
        if (!k.equals(key)) {
            // 计算关系系数
            double coefficient = relateDist(v, items, type);
            distMap.put(coefficient, k);
        }
    });
    return distMap;
}

计算两个用户间的相关系数

/**
 * 计算两个序列间的相关系数
 *
 * @param xList
 * @param yList
 * @param type  类型0基于用户推荐使用余弦相似度 1基于物品推荐使用余弦相似度 2基于用户推荐使用皮尔森系数计算
 * @return
 */
private static double relateDist(List<RelateDTO> xList, 
                              List<RelateDTO> yList, Integer type) {
    List<Integer> xs = Lists.newArrayList();
    List<Integer> ys = Lists.newArrayList();
    xList.forEach(x -> yList.forEach(y -> {
        if (type == 0) {
            // 基于用户推荐时如果两个用户购买的商品相同,则计算相似度
            if (x.getProductId().longValue() == y.getProductId().longValue()) {
                xs.add(x.getIndex());
                ys.add(y.getIndex());
            }
        } else if (type == 1) {
            // 基于物品推荐时如果两个用户id相同,则计算相似度
            if (x.getUserId().longValue() == y.getUserId().longValue()) {
                xs.add(x.getIndex());
                ys.add(y.getIndex());
            }
        }
    }));
    if (ys.size() == 0 || xs.size() == 0) {
        return 0d;
    }
    // 余弦相似度计算
    return cosineSimilarity(xs, ys);
}

余弦相似度计算

/**
 * 来计算向量之间的余弦相似度,
 * 也就是计算两个用户或者两个物品之间的相似度
 * @param xs
 * @param xs
 * @return double
 */
private static double cosineSimilarity(List<Integer> xs, 
                                                List<Integer> ys) {
    double dotProduct = 0;
    double norm1 = 0;
    double norm2 = 0;
    for (int i = 0; i < xs.size(); i++) {
        Integer x = xs.get(i);
        Integer y = ys.get(i);
        dotProduct += x * y;
        norm1 += Math.pow(x, 2);
        norm2 += Math.pow(y, 2);
    }
    return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}

3.3 推荐生成

基于用户协同的推荐生成,我们可以先找到和目标用户有相似兴趣的其他用户,然后把其他用户喜欢的,而目标用户没有买过的物品推荐给目标用户。

public class UserCF {
    /**
     * 物用户协同推荐
     *
     * @param userId 用户ID
     * @param num    返回数量
     * @param list   预处理数据
     * @return 商品id集合
     */
    public static List<Long> recommend(Long userId, Integer num,
                                       List<RelateDTO> list, Integer type) {
        // 对每个用户的购买商品记录进行分组
        Map<Long, List<RelateDTO>> userMap = list.stream()
                .collect(Collectors.groupingBy(RelateDTO::getUserId));
        // 获取其他用户与当前用户的关系值
        Map<Double, Long> userDisMap = CoreMath.computeNeighbor(userId, userMap, type);
        List<Long> similarUserIdList = new ArrayList<>();
        List<Double> values = new ArrayList<>(userDisMap.keySet());
        values.sort(Collections.reverseOrder());
        List<Double> scoresList = values.stream().limit(3).toList();
        // 获取关系最近的用户
        for (Double aDouble : scoresList) {
            similarUserIdList.add(userDisMap.get(aDouble));
        }
        List<Long> similarProductIdList = new ArrayList<>();
        for (Long similarUserId : similarUserIdList) {
            // 获取相似用户购买商品的记录
            List<Long> collect = userMap.get(similarUserId).stream()
                    .map(RelateDTO::getProductId).toList();
            // 过滤掉重复的商品
            List<Long> collect1 = collect.stream()
                    .filter(e -> !similarProductIdList.contains(e)).toList();
            similarProductIdList.addAll(collect1);
        }
        // 当前登录用户购买过的商品
        List<Long> userProductIdList = userMap.getOrDefault(userId,
                        Collections.emptyList()).stream().map(RelateDTO::getProductId).toList();
        // 相似用户买过,但是当前用户没买过的商品作为推荐
        List<Long> recommendList = new ArrayList<>();
        for (Long similarProduct : similarProductIdList) {
            if (!userProductIdList.contains(similarProduct)) {
                recommendList.add(similarProduct);
            }
        }
        Collections.sort(recommendList);
        return recommendList.stream().distinct().limit(num).toList();
    }
}

基于物品协同的推荐生成,找出与目标用户购买过的商品中最相似的前几个商品中目标用户也没有买过的商品推荐给用户。

public class ItemCF {

    /**
     * 物品协同推荐
     *
     * @param userId 用户ID
     * @param num    返回数量
     * @param list   预处理数据
     * @return 商品id集合
     */
    public static List<Long> recommend(Long userId, Integer num, 
                                        List<RelateDTO> list) {
        // 按物品分组
        Map<Long, List<RelateDTO>> userMap = list.stream()
                .collect(Collectors.groupingBy(RelateDTO::getUserId));
        List<Long> userProductItems = userMap.get(userId).stream()
                .map(RelateDTO::getProductId).toList();
        Map<Long, List<RelateDTO>> itemMap = list.stream()
                .collect(Collectors.groupingBy(RelateDTO::getProductId));
        List<Long> similarProductIdList = new ArrayList<>();
        Multimap<Double, Long> itemTotalDisMap = TreeMultimap.create();
        for (Long itemId : userProductItems) {
            // 获取其他物品与当前物品的关系值
            Map<Double, Long> itemDisMap = CoreMath.computeNeighbor(itemId, itemMap, 1);
            itemDisMap.forEach(itemTotalDisMap::put);
        }

        List<Double> values = new ArrayList<>(itemTotalDisMap.keySet());
        values.sort(Collections.reverseOrder());
        List<Double> scoresList = values.stream().limit(num).toList();
        // 获取关系最近的用户
        for (Double aDouble : scoresList) {
            Collection<Long> longs = itemTotalDisMap.get(aDouble);
            for (Long productId : longs) {
                if (!userProductItems.contains(productId)) {
                    similarProductIdList.add(productId);
                }
            }
        }
        return similarProductIdList.stream().distinct().limit(num).toList();
    }
}

3.4 推荐评估和优化


newbee-mall-pro
中可以针对
为你推荐
栏目中推荐的商品做曝光率、点击率、下单数等作为监控指标来评估推荐效果。

四、用户协同和物品协同应用场景

用户协同和物品协同都是两种常用的推荐系统算法,它们分别利用用户之间和物品之间的相似度来给用户提供个性化的推荐。用户协同和物品协同的应用场景有以下几种:

  • 用户协同适用于用户数量相对较少,用户兴趣相对稳定,物品数量相对较多,物品更新频率较高的场景。例如,电影推荐、音乐推荐、图书推荐等。
  • 物品协同适用于用户数量相对较多,用户兴趣相对多变,物品数量相对较少,物品更新频率较低的场景。例如,新闻推荐、广告推荐、社交网络推荐等。
  • 用户协同和物品协同也可以结合起来,形成混合推荐系统,以提高推荐的准确性和覆盖率。例如,电商平台可以根据用户的购买历史和评价,以及物品的属性和销量,综合使用用户协同和物品协同来给用户推荐商品。

商城系统使用用户协同还是物品协同,这是一个需要根据具体情况进行选择的问题。用户协同是指根据用户之间的相似度,为用户推荐他们可能感兴趣的物品。物品协同是指根据物品之间的相似度,为用户推荐与他们已经购买或浏览过的物品相似的物品。两种方法各有优缺点,需要综合考虑商城系统的目标、规模、数据量、稀疏度等因素。一般来说,如果商城系统的目标是增加用户的多样性和探索性,那么用户协同可能更合适,因为它可以为用户提供更广泛的选择。如果商城系统的目标是增加用户的满意度和忠诚度,那么物品协同可能更合适,因为它可以为用户提供更精准的推荐

在一般商城系统中,初期用户数量少可以使用用户协同,后期用户数远超商品数,使用物品协同会更好些,这两者也可以结合使用。推荐算法是不会一成不变的,它需要根据某些指标数据不断优化调整升值甚至重构使用另外的算法。

五、冷启动问题

商城协同算法冷启动问题是指在商城系统中,当新用户或新商品加入时,由于缺乏足够的交互数据,导致协同过滤算法无法为其提供准确的推荐结果。


newbee-mall-pro
就是指新用户还未下单

这种问题会影响商城的用户体验和转化率,因此需要有效的解决方案。一种常见的方法是使用流行度算法。

利用基于流行度的算法非常简单粗暴,类似于各大新闻、微博热榜、商城等,根据PV、UV、点击率、搜索率、下单商品排行等数据来按某种热度排序来推荐给用户。

总结


到这里,本文所分享
推荐算法在商城系统实践
就全部介绍完了,希望对大家实现推荐系统落地有所帮助,喜欢的朋友们可以点赞加关注