如何用策略模式干掉 if-else日常编码过程中遇到很多的 if-else ,代码扩展性和阅读性会受到影响,代码中常常使用策略模式代替 if-else代码示例:/** * 上传文件 * * @param storageType 文件存储方式 * @param file 文件 */ public void uploadFile(String storageType, String file) { if (storageType.equals(LOCAL)) { System.out.println("文件" + file + "已上传到 本地服务器"); } else if (storageType.equals(FTP)) { System.out.println("文件" + file + "已上传到 ftp服务器"); } else if (storageType.equals(FASTDFS)) { System.out.println("文件" + file + "已上传到 fastdfs服务器"); } else if (storageType.equals(HDFS)) { System.out.println("文件" + file + "已上传到 hdfs服务器"); } else { System.out.println("输入的文件存储类型错误"); } }第一步:创建基类如果看到上面代码,要怎么优化,抽象一层, 搞个 Base 类:public abstract class StorageStrategy{ public abstract void uploadFile(String file); }第二步:实现基类让所有业务实现基类接口public class FtpStorageStrategy extends StorageStrategy { @Override public void uploadFile(String file) { System.out.println("文件" + file + "已上传到 ftp服务器"); } }第三步:查表方式选择基类既然事查表,当然需要在将创建的处理类,放到一个容器中,这里使用 map,将所有的策略放到一个容器中。public class Factory { private static Map<String,StorageStrategy> strategyMap = new ConcurrentHashMap<>(); public static void register(String name,StorageStrategy strategy){ strategyMap.put(name,strategy); } public static StorageStrategy getInvokeStrategy(String name){ return strategyMap.get(name); } }第四步:使用public class Client { public static void main(String[] args) { // 获取策略 StorageStrategy strategy = Factory.strategyMap.get("FTP") // 执行策略 strategy.uploadFile("策略模式.txt"); }
go-echartschart 包是一个简单的本地图表库,支持时间序列和连续折线。是数据可视化第三方库。安装$ go get -u github.com/go-echarts/go-echarts/... # 因为 gomod 的特殊的版本管理方式,使用 go get 方式并不能直接使用 v2 go-echarts 🐶 # 不过可以通过以下方法使用新版本... $ cd $go-echarts-project $ mkdir v2 && mv charts components datasets opts render templates types v2go.modrequire github.com/go-echarts/go-echarts/v2echart 特性简洁的 API 设计,使用如丝滑般流畅囊括了 25+ 种常见图表,应有尽有高度灵活的配置项,可轻松搭配出精美的图表详细的文档和示例,帮助开发者更快地上手项目多达 400+ 地图,为地理数据可视化提供强有力的支持直方图:package go_chart import ( "math/rand" "os" "testing" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/opts" ) //generate random data for bar chart func generateBarItems() []opts.BarData { items := make([]opts.BarData, 0) for i := 0; i < 7; i++ { items = append(items, opts.BarData{Value: rand.Intn(300)}) } return items } func TestBar(t *testing.T) { // create a new bar instance bar := charts.NewBar() // set some global options like Title/Legend/ToolTip or anything else bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{ Title: "My first bar chart generated by go-echarts", Subtitle: "It's extremely easy to use, right?", })) // Put data into instance bar.SetXAxis([]string{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}). AddSeries("Category A", generateBarItems()). AddSeries("Category B", generateBarItems()) // Where the magic happens f, _ := os.Create("bar.html") bar.Render(f) }运行结果:饼图package go_chart import ( "math/rand" "os" "testing" "github.com/go-echarts/go-echarts/v2/charts" "github.com/go-echarts/go-echarts/v2/opts" ) var ( itemCntPie = 4 seasons = []string{"Spring", "Summer", "Autumn ", "Winter"} ) func generatePieItems() []opts.PieData { items := make([]opts.PieData, 0) for i := 0; i < itemCntPie; i++ { items = append(items, opts.PieData{Name: seasons[i], Value: rand.Intn(100)}) } return items } func pieBase() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{Title: "basic pie example"}), ) pie.AddSeries("pie", generatePieItems()) return pie } func pieShowLabel() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{Title: "label options"}), ) pie.AddSeries("pie", generatePieItems()). SetSeriesOptions(charts.WithLabelOpts( opts.Label{ Show: true, Formatter: "{b}: {c}", }), ) return pie } func pieRadius() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{Title: "Radius style"}), ) pie.AddSeries("pie", generatePieItems()). SetSeriesOptions( charts.WithLabelOpts(opts.Label{ Show: true, Formatter: "{b}: {c}", }), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"40%", "75%"}, }), ) return pie } func pieRoseArea() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{ Title: "Rose(Area)", }), ) pie.AddSeries("pie", generatePieItems()). SetSeriesOptions( charts.WithLabelOpts(opts.Label{ Show: true, Formatter: "{b}: {c}", }), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"40%", "75%"}, RoseType: "area", }), ) return pie } func pieRoseRadius() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{ Title: "Rose(Radius)", }), ) pie.AddSeries("pie", generatePieItems()). SetSeriesOptions( charts.WithLabelOpts(opts.Label{ Show: true, Formatter: "{b}: {c}", }), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"30%", "75%"}, RoseType: "radius", }), ) return pie } func pieRoseAreaRadius() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{ Title: "Rose(Area/Radius)", }), ) pie.AddSeries("area", generatePieItems()). SetSeriesOptions( charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"30%", "75%"}, RoseType: "area", Center: []string{"25%", "50%"}, }), ) pie.AddSeries("pie", generatePieItems()). SetSeriesOptions( charts.WithLabelOpts(opts.Label{ Show: true, Formatter: "{b}: {c}", }), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"30%", "75%"}, RoseType: "radius", Center: []string{"75%", "50%"}, }), ) return pie } func pieInPie() *charts.Pie { pie := charts.NewPie() pie.SetGlobalOptions( charts.WithTitleOpts(opts.Title{ Title: "pie in pie", }), ) pie.AddSeries("area", generatePieItems(), charts.WithLabelOpts(opts.Label{ Show: true, Formatter: "{b}: {c}", }), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"50%", "55%"}, RoseType: "area", }), ) pie.AddSeries("radius", generatePieItems(), charts.WithPieChartOpts(opts.PieChart{ Radius: []string{"0%", "45%"}, RoseType: "radius", }), ) return pie } type PieExamples struct{} //func TestPie(t *testing.T) { // //(PieExamples) // page := components.NewPage() // page.AddCharts( // pieBase(), // pieShowLabel(), // pieRadius(), // pieRoseArea(), // pieRoseRadius(), // pieRoseAreaRadius(), // pieInPie(), // ) // f, err := os.Create("examples/html/pie.html") // if err != nil { // panic(err) // } // page.Render(io.MultiWriter(f)) //} func TestPie(t *testing.T) { // create a new bar instance pie := pieBase() // Where the magic happens f, _ := os.Create("pie.html") pie.Render(f) }
MQ 使用场景: 应用解耦、消峰填谷,消息分发。ConsumerRocketMQ Consumer 分为 Pull Consumer 和 Push Consumer ,其实就是推拉消费者。Pull ConsumerPush ConsumerDefaultMQPushConsumerDefaultMQPushCOnsumerImpl 通过 start 方法启动org.apache.rocketmq.client.consumer.DefaultMQPushConsumerDefaultMQPushConsumer#start 启动代码:public void start() throws MQClientException { setConsumerGroup(NamespaceUtil.wrapNamespace(this.getNamespace(), this.consumerGroup)); this.defaultMQPushConsumerImpl.start(); if (null != traceDispatcher) { try { traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel()); } catch (MQClientException e) { log.warn("trace dispatcher start failed ", e); } } }先简化说一下启动流程:启动步骤如下, 启动准备,从nameserver 获取 topic 路由信息,检查 Consumer 配置,向每个 broker 发心跳,触发一次 rebalance.public synchronized void start() throws MQClientException { // 1. 启动准备工作 switch (this.serviceState) {...} // 2. 从 NameServer 更新 topic 路由信息 this.updateTopicSubscribeInfoWhenSubscriptionChanged(); // 3. 检查 consumer 配置 this.mQClientFactory.checkClientInBroker(); // 4. 向每个broker 发送心跳 this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); // 5. 立即触发一次 reblance this.mQClientFactory.rebalanceImmediately(); }启动准备工作中有 CREATE_JUST状态判断。CREATE_JUST 的意思是 Service just created,not start 创建服务,没有启动。start 启动核心代码在 DefaultMQPushConsumerImpl#startpublic synchronized void start() throws MQClientException { switch (this.serviceState) { case CREATE_JUST: log.info("the consumer [{}] start beginning. messageModel={}, isUnitMode={}", this.defaultMQPushConsumer.getConsumerGroup(), this.defaultMQPushConsumer.getMessageModel(), this.defaultMQPushConsumer.isUnitMode()); this.serviceState = ServiceState.START_FAILED; // 1. 检查必要的参数consumerGroup、消费模式、consumerFromWhere、负载策略等配置 this.checkConfig(); // 2. 拷贝订阅信息,监听重投队列%RETRY%TOPIC this.copySubscription(); if (this.defaultMQPushConsumer.getMessageModel() == MessageModel.CLUSTERING) { this.defaultMQPushConsumer.changeInstanceNameToPID(); } // 3. 获取MQ 实例,先从缓存中国取,没有则创建 this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQPushConsumer, this.rpcHook); // 4. 设置 重负载的消费组 this.rebalanceImpl.setConsumerGroup(this.defaultMQPushConsumer.getConsumerGroup()); //设置消费模式 this.rebalanceImpl.setMessageModel(this.defaultMQPushConsumer.getMessageModel()); // 设置负载策略 this.rebalanceImpl.setAllocateMessageQueueStrategy(this.defaultMQPushConsumer.getAllocateMessageQueueStrategy()); this.rebalanceImpl.setmQClientFactory(this.mQClientFactory); // 实例化消息拉取的包装类并注册消息过滤的消息类 this.pullAPIWrapper = new PullAPIWrapper( mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup(), isUnitMode()); this.pullAPIWrapper.registerFilterMessageHook(filterMessageHookList); // 选择对应的 offset 实现类,并加载 offset 集群,广播消息的 offset 从消费者本地获取,集群模式从 Brocker 维护。 if (this.defaultMQPushConsumer.getOffsetStore() != null) { this.offsetStore = this.defaultMQPushConsumer.getOffsetStore(); } else { switch (this.defaultMQPushConsumer.getMessageModel()) { case BROADCASTING: this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); break; case CLUSTERING: this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); break; default: break; } this.defaultMQPushConsumer.setOffsetStore(this.offsetStore); } this.offsetStore.load(); // 启动消息消费者服务 if (this.getMessageListenerInner() instanceof MessageListenerOrderly) { this.consumeOrderly = true; this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner()); } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) { this.consumeOrderly = false; this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner()); } this.consumeMessageService.start(); // 8. 启动 MQ ClinentInstance boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this); if (!registerOK) { this.serviceState = ServiceState.CREATE_JUST; this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown()); throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup() + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null); } mQClientFactory.start(); log.info("the consumer [{}] start OK.", this.defaultMQPushConsumer.getConsumerGroup()); this.serviceState = ServiceState.RUNNING; break; case RUNNING: case START_FAILED: case SHUTDOWN_ALREADY: throw new MQClientException("The PushConsumer service state not OK, maybe started once, " + this.serviceState + FAQUrl.suggestTodo(FAQUrl.CLIENT_SERVICE_NOT_OK), null); default: break; } //9. 从nameserver拉取 topic 订阅消息 this.updateTopicSubscribeInfoWhenSubscriptionChanged(); //10. 向 broker 校验客户端 this.mQClientFactory.checkClientInBroker(); // 11. 给所有的 broker发送心跳。 this.mQClientFactory.sendHeartbeatToAllBrokerWithLock(); // 12. 负载队列 this.mQClientFactory.rebalanceImmediately(); }咱们细化下启动过程1. 检查必要的参数consumerGroup、消费模式、consumerFromWhere、负载策略等配置 2. 拷贝订阅信息,监听重投队列%RETRY%TOPIC 3. 获取MQ实例,先从缓存中取,没有则创建 4. 设置重负载的消费组、消费模式、负载策略等 5. 实例化消息拉取的包装类并注册消息过滤的钩子 选择对应的OffsetStore实现类,并加载offset。广播消息从本地获取offset (Consumer本地维护),6. 集群消息从远程获取 (broker端维护) 7. 启动消息消费服务 7.1 并发消费:15分钟执行一次,过期消息(15分钟还未被消费的消息)一次最多清理16条过期消息,将过期消息发回给broker 7.2 顺序消费:20s定时锁定当前实例消费的所有队列,上锁成功将ProcessQueue的lock属性设置为true 8. 启动MQClientInstance 8.1 启动请求和响应的通道 8.2 开启定时任务 8.2.1 定时30s拉取最新的broker和topic的路由信息 8.2.2.定时30s向broker发送心跳包 8.2.3 定时5s持久化consumer的offset 8.2.4 定时1分钟,动态调整线程池线程数量 8.3 启动消息拉取服务 8.4 每20s重新负载一次 9. 从nameserver拉取topic的订阅信息 10. 向broker校验客户端 11. 给所有的broker的master发送心跳包 、上传filterClass的源文件给FilterServer 12. 立即负载队列内容比较多,主要关心如下几点:拷贝订阅消息构建主体订阅消息 SubscriptionData 并加入到 RebalanceImpl 的订阅消息中,订阅关系主要来自两个:通过调用 DefaltMQPushConsumerImpl#subscribe 方法订阅主题消息,RocketMQ 消息重试以消费组为单位,而不是主题,消息重试主题为 %RETRY%+消费组. 消费者在启动的时候会自动订阅该主题,参与该主题的消息队列负载。private void copySubscription() throws MQClientException { try { Map<String, String> sub = this.defaultMQPushConsumer.getSubscription(); if (sub != null) { for (final Map.Entry<String, String> entry : sub.entrySet()) { final String topic = entry.getKey(); final String subString = entry.getValue(); SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(topic, subString); this.rebalanceImpl.getSubscriptionInner().put(topic, subscriptionData); } } if (null == this.messageListenerInner) { this.messageListenerInner = this.defaultMQPushConsumer.getMessageListener(); } switch (this.defaultMQPushConsumer.getMessageModel()) { case BROADCASTING: break; case CLUSTERING: final String retryTopic = MixAll.getRetryTopic(this.defaultMQPushConsumer.getConsumerGroup()); SubscriptionData subscriptionData = FilterAPI.buildSubscriptionData(retryTopic, SubscriptionData.SUB_ALL); this.rebalanceImpl.getSubscriptionInner().put(retryTopic, subscriptionData); break; default: break; } } catch (Exception e) { throw new MQClientException("subscription exception", e); } }初始化消息进度其实说的就是加载 offset , 如果消息消费是集群模式,那么消费进度(offset)保存在 Broker 上;如果是广播方式,消息进度存储在消费端。switch (this.defaultMQPushConsumer.getMessageModel()) { case BROADCASTING: this.offsetStore = new LocalFileOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); break; case CLUSTERING: this.offsetStore = new RemoteBrokerOffsetStore(this.mQClientFactory, this.defaultMQPushConsumer.getConsumerGroup()); break; default: break; } this.defaultMQPushConsumer.setOffsetStore(this.offsetStore);创建消费者线程服务根据消费者是否顺序消费, 创建消费端消费线程服务。consumeMessageService 负责消息消费,内部维护的是个线程池。if (this.getMessageListenerInner() instanceof MessageListenerOrderly) { this.consumeOrderly = true; this.consumeMessageService = new ConsumeMessageOrderlyService(this, (MessageListenerOrderly) this.getMessageListenerInner()); } else if (this.getMessageListenerInner() instanceof MessageListenerConcurrently) { this.consumeOrderly = false; this.consumeMessageService = new ConsumeMessageConcurrentlyService(this, (MessageListenerConcurrently) this.getMessageListenerInner()); } this.consumeMessageService.start();向MQClientInstance 注册消费者,并启动向 MQClientInstance 注册消费者,并启动MQClientInstance。在一个 JVM 中所有的生产者和消费者都持有同一个 MQClientInstance, MQClientInstance, 只会启动一次。boolean registerOK = mQClientFactory.registerConsumer(this.defaultMQPushConsumer.getConsumerGroup(), this); if (!registerOK) { this.serviceState = ServiceState.CREATE_JUST; this.consumeMessageService.shutdown(defaultMQPushConsumer.getAwaitTerminationMillisWhenShutdown()); throw new MQClientException("The consumer group[" + this.defaultMQPushConsumer.getConsumerGroup() + "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL), null); } mQClientFactory.start();总结介绍了一下 MQ 消费者启动过程,校验配置,获取订阅消息,设置消费者的消费模式,负载策略。加载消息进度,启动消息消费者服务,并向MQClientInstance注册消费者Group和并启动MQClientInstance,从 nameserver 拉取 topic 订阅信息,向 Broker 发送心跳包。
ping 问题分析ping 是什么ping 是常用的网络管理命令,ping也属于一个通信协议,是TCP/IP协议的一部分,适用于windows和linux以及unix。根据reply 反馈结果,来检查网络是否通畅或者网络连接的速度(time)是否正常。主要是端对端的,针对目标ip或者目标网址。OSI物理层: 物理层负责最后将信息编码成电流脉冲或其它信号用于网上传输;eg:RJ45等将数据转化成0和1;数据链路层: 数据链路层通过物理网络链路􏰁供数据传输。不同的数据链路层定义了不同的网络和协 议特征,其中包括物理编址、网络拓扑结构、错误校验、数据帧序列以及流控;可以简单理解为:规定了0和1的分包形式,确定了网络数据包的形式;网络层: 网络层负责在源和终点之间建立连接;可以理解为,此处需要确定计算机的位置,怎么确定?IPv4,IPv6!传输层 传输层向高层􏰁提供可靠的端到端的网络数据流服务。可以理解为:每一个应用程序都会在网卡注册一个端口号,该层就是端口与端口的通信!常用的(TCP/IP)协议;会话层 会话层建立、管理和终止表示层与实体之间的通信会话;建立一个连接(自动网络寻址);表示层: 表示层􏰁供多种功能用于应用层数据编码和转化,以确保以一个系统应用层发送的信息 可以被另一个系统应用层识别;可以理解为:解决不同系统之间的通信,eg:Linux下的QQ和Windows下的QQ可以通信;应用层: OSI 的应用层协议包括文件的传输、访问及管理协议(FTAM) ,以及文件虚拟终端协议(VIP)和公用管理系统信息(CMIP)等;规定数据的传输协议;ping 原理ping命令使用的是检测源和目的ip间导通性测试的icmp协议,属于三层网络ip层协议。ping的过程,无论是源主机发出request请求还是目的主机回reply的过程,都是首先根据目的ip查找本地路由表,确定下一跳的出口,然后根据下一跳的ip在arp缓存里确定是否有下一跳ip的mac地址,没有就发出arp请求去查找。有的话,二层和ip层组包发出。源主机ping发出含一串数据的包(如123456789abcde之类)request消息,封装在二层上,对方收到后,把这串包原路反射送回来,源pc收到后,认为是对方可达。所以它涉及二层的mac地址和ip层的交互。当对方存在问题时(ip地址不存在,没有路由等),对方的ip或者经过的网络节点会返回icmp的差错消息给终端源ip。使用的端口和ip要根据节点的路由表进行确定,发起方根据返回结果来产生回显,若在ping的request消息发出,等待一段时间,win默认是5s,没有收到回复,发起方显示超时time out(linux环境默认定时器是1秒,这种情况没有任何显示)ping 不通的可能原因ping 不通的现象C:\Users> ping 192.168.4.41 正在 Ping 192.168.4.41 具有 32 字节的数据: 请求超时。 请求超时。 ... 192.168.4.41 的 Ping 统计信息: 数据包: 已发送 = 4,已接收 = 0,丢失 = 4 (100% 丢失),对方关机/ip不存在网段不同,通过路由也无法找到防火墙设置,过滤了ping发出的ICMP数据包,导致无反馈,time outIp地址设置错误,对于多个网卡的服务器来说,每个网口的ip配置必须不能在同一个网段,否则会造成路由不知选择哪一个出口网线故障未设置网关,这个对于小网128网段,走路由器的,如果未配置将无法路由总有分成几类问题:电脑配置故障;物理线路故障;ARP问题;VLAN问题;路由问题;访问控制。电脑配置故障使用ipconfig /all观察本地网络设置是否正确以太网适配器 本地连接: 连接特定的 DNS 后缀 . . . . . . . : huawei.com 描述. . . . . . . . . . . . . . . : Xen Net Device Driver 物理地址. . . . . . . . . . . . . : 28-6E-D4-88-B7-19 DHCP 已启用 . . . . . . . . . . . : 是 自动配置已启用. . . . . . . . . . : 是 本地链接 IPv6 地址. . . . . . . . : fe80::dd9a:f549:2b85:b027%13(首选) IPv4 地址 . . . . . . . . . . . . : 192.168.1.5(首选) 子网掩码 . . . . . . . . . . . . : 255.255.255.0 ...默认网关. . . . . . . . . . . . . : 192.168.1.1ping 对方ping –a 命令,可探测对方,将ip地址解析为主机名。如果存在,说明该主机ip存在,从而去检查防火墙是否关闭;ping 127.0.0.1Ping 127.0.0.1地址检查本地的TCP/IP协议有没有设置好。ping 127.0.0.1 ,若无法ping通,则本地tcp/ip协议栈有问题 若提示为:no route to host,则说明网卡不能正常工作 若提示为:transmit failed error code,则说明网卡驱动有问题 若提示为:time out 说明路由器中有该路由,但是由于其他原因导致包无法传送 若提示为:destination host unreachable 说明路由器中无该路由ping 本机(ipv4)Ping本机IP地址,这样是为了检查本机的IP地址设置和网卡安装配置是否有误。C:\Users> ping 192.168.1.5 正在 Ping 192.168.1.5 具有 32 字节的数据: 来自 192.168.1.5 的回复: 字节=32 时间<1ms TTL=128 来自 192.168.1.5 的回复: 字节=32 时间<1ms TTL=12 ...请求超时,则表明网卡安装或配置有故障。将网线断开再次执行此命令, 如果显示正常,则说明本机使用的IP地址可能与另一台正在使用的机器IP地址重复了。如果仍然不正常,则表明本机网卡安装或配置有故障,需继续检查相关网络配置。ping 网关Ping本网网关或本网IP地址,这样是为了检查硬件设备是否有故障,也可以检查本机与本地网络连接是否正常。C:\Users> ping 192.168.1.11 正在 Ping 192.168.1.11 具有 32 字节的数据: 来自 192.168.1.11 的回复: 字节=32 时间<1ms TTL=128 来自 192.168.1.11 的回复: 字节=32 时间<1ms TTL=128 ...Ping不通物理链路故障故障分析光纤或网线连接的端口和网络链路部署要求不一致;光模块波长参数与实际需求不一致;设备的通信接口损坏;物理连接线老化、破损;接口被阻塞。常见物理链路故障判断方法如下:查看设备端口指示灯状态,如果是常灰,说明无连接。此时需要更换接口或者网线再进行尝试。通过display interface interface-type interface-number命令检查接口的状态,依据端口状态判断故障原因,从而进行解决。通过display stp brief、display rrpp verbose和display smart-link group all命令,检查设备上是否运行了STP、RRPP或SMART LINK等二层协议,确认Ping业务经过的物理接口是否被阻塞。如果端口被阻塞,需要修改相关的配置。Ping不通ARP问题故障分析通过前面的Ping不通故障定位思路可以判断出是否是由于ARP问题引起Ping不通。设备在封装ICMP报文时需要MAC地址,如果对应的MAC地址不存在,则需要进行ARP学习,ARP学习失败会导致Ping报文被丢弃,从而Ping不通。常见ARP问题导致Ping不通,都是因为设备未正确进行ARP学习。常见ARP问题判断方法如下:通过display arp interface interface-type interface-number命令,检查直连地址的ARP是否学习正常。通过display mac-address interface-type interface-number命令查看MAC表项,确认MAC地址的出端口和ARP的物理出端口是否一致。如果ARP未正常的学习,首先检查接口配置、VLAN配置、VLANIF接口配置、IP地址配置等是否正确,其次检查ARP和ARP安全的配置是否限制了ARP的学习。Ping不通VLAN问题故障分析通过前面的Ping不通故障定位思路可以判断出是否是由于VLAN问题引起Ping不通。常见VLAN问题有以下三种:接口未加入已经规划好的VLAN;接口的链路类型配置不正确;VLANIF接口的状态不为UP,或配置的IP地址不正确。常见VLAN问题判断方法如下:通过display port vlan interface-type interface-number命令,查看接口加入的VLAN。接口所属的VLAN一般在网络规划的时候已经完成,如果配置错误,需要重新配置。通过display port vlan interface-type interface-number命令,查看接口的链路类型。不同的链路类型对通过的报文处理方式不同,配置的时候需要关注,如果配置错误,需要重新配置。通过display interface brief和display ip interface brief interface-type interface-number命令,查看接口状态和接口与IP的对应关系。配置VLANIF接口,并将实际物理口加入指定VLAN后,需要保证VLANIF接口状态为UP,才能进行通信。如果配置错误,需要重新配置。Ping不通路由问题故障分析通过前面的Ping不通故障定位思路可以判断出是否是由于路由问题引起Ping不通。常见路由问题有以下三种:没有去目的网段的路由;目的设备没有回程路由;设备的路由表已经超规格;路由配置错误。常见路由问题判断方法是通过display ip routing-table命令查看路由表信息,看其中是否存在到目的网段的路由,如果没有路由需要重新配置路由。配置路由时不仅需要关注本端去对端的路由,还需要注意对端的回程路由。设备支持配置多种路由协议,可以依据实际情况进行选择。Ping不通访问控制故障分析通过前面的Ping不通故障定位思路可以判断出是否是由于访问控制引起Ping不通。为了保护设备安全或业务需要,会经常在设备上配置访问控制,如果Ping报文正好处于访问控制之列,则可能会导致Ping不通。常见访问控制有限制报文类型、过滤源地址或目的地址等。常见访问控制判断方法如下:在接口下进行抓包,分析获取报文的信息,然后查看相应的配置。通过display current-configuration interface interface-type interface-number命令,检查接口上是否存在访问控制的相关配置。由于访问控制一般都是依据设备安全或业务需要而配置的,虽然Ping不通但是不影响业务的运行。修改此类问题需要慎重,以免影响设备的正常使用常见的网络层协议
Go 数据库操作异常处理插入操作第一种写法err := db.Model(&XXX{}).Create(order).Error if err != nil { logs.CtxError(ctx, "Create XXX failed, err:%v", err.Error()) return err }第二种写法db := db.Model(&XXX{}).Create(order) if db.Error != nil { logs.CtxError(ctx, "Create XXX failed, err:%v", db.Error) return db.Error }上述两种写法说明:两种写法都没啥问题,第一种写法, 如果只插入一条数据,可以使用第一种写法简单;第二种写法可以拿到执行的 *DB ,方便后续的 DB 操作更新操作db := db.Model(&Voucher{}).Where(whereMap).Updates(updateMap) if db == nil { return 0, errors.New(fmt.Sprintf("UpdateVoucherWithState failed, db is null, jrUid:%s, voucherNo:%s", jrUid, voucherNo)) } err := db.Error if err != nil { logs.CtxError(ctx, "UpdateXXX failed err:%v", err) } if db.RowsAffected != 1{ logs.CtxWarn(ctx, "UpdateXXX failed RowsAffected:%v", db.RowsAffected) // TODO 根据业务自身特性单独处理 }说明:update 方法将返回执行完之后的 *DB, 需要通过指针对象才能获取正确的 RowAffected。事务处理tx, txErr = model.BeginTransactionNotShard(ctx) if txErr != nil { logs.CtxError(ctx, "db BeginTransaction err: %v", txErr) return constant.GenRetCode(constant.DBError, txErr.Error()) } defer func() { if err := recover(); err != nil { logs.CtxError(ctx, "panic:%+v", err) tx.Rollback(ctx, tx) } else if err != nil { // logs.CtxError tx.Rollback() } }() if err := tx.Error; err != nil { return err } if err:= tx.Create(&XXX).Error; err != nil { return err } return tx.Commit().Error事务的提交也可能会有 error, 要判断是否正确 commit需要判断 tx.Error,因为事务的提交可能会有 error查询的异常处理if err := db.Where("name = ?", "jianzhu").First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordFound) { // TODO 业务处理查询不到数据的情况 } if err != nil { // do something ... } }其实要注意的是,没查询到结果,也会返回一个Errorgorm 的ErrRecordNotFound也好理解,假设根据身份证号查询公民信息,如果是一个无效的身份证ID,那必然无法查询到结果, 其实就是查询不到结果,会返回一个错误。当然 GORM 提供了一个处理 RecordNotFound 错误的快捷方式,如果发生了多个错误,它将检查每个错误,如果它们中的任何一个是RecordNotFound 错误。//检查是否返回 RecordNotFound 错误 db.Where("name = ?", "hello world").First(&user).RecordNotFound() if db.Model(&user).Related(&credit_card).RecordNotFound() { // 数据没有找到 } if err := db.Where("name = ?", "jinzhu").First(&user).Error; gorm.IsRecordNotFoundError(err) { // 数据没有找到 }当一个程序中使用两个不同的数据库时, 重写方法DefaultTableNameHandler()会影响到两个数据库中的表名。其中一个数据库需要设置表前缀时,访问另一个数据库的表也可能会被加上前缀。因为是包级别的方法,整个代码里只能设置一次值。
前言越高级,越复杂的查询,也同时意味着高耗,但是平时有一些数据少,但是业务复杂的场景,可以使用下。这里主要说明的是 go 中使用 gorm 进行查询。gorm import 依赖"database/sql" "fmt" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql"将查询结果放到一个 struct 中// 根据主键查询第一条记录 db.First(&user) //// SELECT * FROM users ORDER BY id LIMIT 1; // 随机获取一条记录 db.Take(&user) //// SELECT * FROM users LIMIT 1; // 根据主键查询最后一条记录 db.Last(&user) //// SELECT * FROM users ORDER BY id DESC LIMIT 1; // 查询所有的记录 db.Find(&users) //// SELECT * FROM users; // 查询指定的某条记录(仅当主键为整型时可用) db.First(&user, 10) //// SELECT * FROM users WHERE id = 10;查询条件是map 或者 struct 查询有时候代码是可需要映射到一个 map 结构, 不需要映射到一个 结构体中,可以写成如下:for update在涉及并发的场景,往往需要加锁互斥,和 Java类似, Go 中也有加行锁的方式,加 for update 即可。一般写法如下:// 为查询 SQL 添加额外的 SQL 操作 db.Set("gorm:query_option", "FOR UPDATE").First(&user, 10) //// SELECT * FROM users WHERE id = 10 FOR UPDATE;示例代码:err := db.Model(&XXX{}).Set("gorm:query_option", "FOR UPDATE").Where("XXX=?", XXX).First(&XXX).Error if err != nil { if err == gorm.ErrRecordNotFound { logs.Warn("xxx") return nil, nil } logs.Error("XXX") }Count 查询有时候,我们需要进行简单的数据统计, 比如查询到结果有多少行,var count int64 db.Model(&User{}).Where("name = ?","jinzhu").Or("name = ?","jinzhu 2").Count(&count) // SELECT count(*) FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2' db.Model(&User{}).Where("name = ?","jinzhu").Count(&count) // SELECT count(*) FROM users WHERE name = 'jinzhu'; (count)分组计数有时候也需要分组统计行数// 分组计数 users :=[]User{ {Name:"name1"}, {Name:"name2"}, {Name:"name3"}, {Name:"name3"}, } DB.Model(&User{}).Group("name").Count(&count) count // => 3去重统计// 去重计数 DB.Model(&User{}).Distinct("name").Count(&count) // SELECT COUNT(DISTINCT(`name`)) FROM `users`Group & Having有时候我们会使用到数据统计的功能, 比如根据数据库字段 batch_no 进行分组,然后统计总金额,总笔数。分组查询统计一般的写法如下:db.Table("orders").Select("date(created_at) as date, sum(amount) as total").Group("date(created_at)").Having("sum(amount) > ?", 100).Scan(&results)代码示例:type Sum struct { BatchNo string `gorm:"column:batch_no" json:"batch_no"` TotalCounts int64 `gorm:"column:total_counts" json:"total_counts"` TotalAmounts int64 `gorm:"column:total_amounts" json:"total_amounts"` } var result []*Sum db := db.Model(&Voucher{}).Select("batch_no, count(1) as totalCounts, sum(amount) as totalAmounts") status := []string{10,20,40} db = db.Where("no >= ?", startVoucherNo) db = db.Where("no <= ?", endVoucherNo) db = db.Where("batch_no IN ?", batchNos) db = db.Where("status IN ?", status) if shardingKey >= 0 { db = db.Where("sharding_key = ", shardingKey) } db = db.Group("batch_no") err := db.Scan(&result).Error if err != nil { if err == gorm.ErrRecordNotFound { logs.CtxWarn("xxx") return nil, nil } logs.CtxError(ctx, "xxx", err) }Join 查询一般来说,很少使用关联查询,但是如果要使用关联查询,可以如下:db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results) // 多连接及参数 db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Joins("JOIN credit_cards ON credit_cards.user_id = users.id").Where("credit_cards.number = ?", "411111111111").Find(&user)查询指定函数Scopes允许你指定常用的查询,可以在调用方法时引用这些查询, 也就是说,可以在查询中使用函数。举个例子:func AmountGreaterThan1000(db *gorm.DB)*gorm.DB { return db.Where("amount > ?",1000) } func PaidWithCreditCard(db *gorm.DB)*gorm.DB { return db.Where("pay_mode_sign = ?","C") } func PaidWithCod(db *gorm.DB)*gorm.DB { return db.Where("pay_mode_sign = ?","C") } db.Scopes(AmountGreaterThan1000,PaidWithCreditCard).Find(&orders) // 查找所有金额大于 1000 的信用卡订单 db.Scopes(AmountGreaterThan1000,PaidWithCod).Find(&orders) // 查找所有金额大于 1000 的货到付款订单
golang 字符串比较字符串比较, 可以直接使用 == 进行比较, 也可用用 strings.Compare 比较go 中字符串比较有三种方式:== 比较strings.Compare 比较strings.EquslFold 比较#### 代码示例 ```go fmt.Println("go"=="go") fmt.Println("GO"=="go") fmt.Println(strings.Compare("GO","go")) fmt.Println(strings.Compare("go","go")) fmt.Println(strings.EqualFold("GO","go"))上述代码执行结果如下:true false -1 0 trueCompare 和 EqualFold 区别EqualFold 是比较UTF-8编码在小写的条件下是否相等,不区分大小写// EqualFold reports whether s and t, interpreted as UTF-8 strings, // are equal under Unicode case-folding. func EqualFold(s, t string) bool要注意的是 Compare 函数是区分大小写的, == 速度执行更快/ Compare is included only for symmetry with package bytes. // It is usually clearer and always faster to use the built-in // string comparison operators ==, <, >, and so on. func Compare(a, b string) int忽略大小写比较有时候要忽略大小写比较, 可以使用strings.EqualFold 字符串比较是否相等源码实现// EqualFold reports whether s and t, interpreted as UTF-8 strings, // are equal under Unicode case-folding, which is a more general // form of case-insensitivity. func EqualFold(s, t string) bool { for s != "" && t != "" { // Extract first rune from each string. var sr, tr rune if s[0] < utf8.RuneSelf { sr, s = rune(s[0]), s[1:] } else { r, size := utf8.DecodeRuneInString(s) sr, s = r, s[size:] } if t[0] < utf8.RuneSelf { tr, t = rune(t[0]), t[1:] } else { r, size := utf8.DecodeRuneInString(t) tr, t = r, t[size:] } // If they match, keep going; if not, return false. // Easy case. if tr == sr { continue } // Make sr < tr to simplify what follows. if tr < sr { tr, sr = sr, tr } // Fast check for ASCII. if tr < utf8.RuneSelf { // ASCII only, sr/tr must be upper/lower case if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' { continue } return false } // General case. SimpleFold(x) returns the next equivalent rune > x // or wraps around to smaller values. r := unicode.SimpleFold(sr) for r != sr && r < tr { r = unicode.SimpleFold(r) } if r == tr { continue } return false } // One string is empty. Are both? return s == t }通过源码可看到 if 'A' <= sr && sr <= 'Z' && tr == sr+'a'-'A' 可以看到不区分大小写的实现。看个完整测试代码:// Golang program to illustrate the // strings.EqualFold() Function package main // importing fmt and strings import ( "fmt" "strings" ) // calling main method func main() { // case insensitive comparing and returns true. fmt.Println(strings.EqualFold("Geeks", "Geeks")) // case insensitive comparing and returns true. fmt.Println(strings.EqualFold("computerscience", "computerscience")) }执行结构true true
Go 语言中有 goto 这个功能,这个功能会影响代码的可读性, 会让代码结构看起来比较乱。Go语言也支持label(标签)语法:分别是break label和 goto label 、continue label最近有次阅读代码,就看到了这样的 case , 那就说一下这个功能吧。gotogoto 可以无条件的跳转执行的位置,但是不能跨函数,需要配合标签使用。package gotocase import ( "fmt" "testing" ) func TestGoto(t *testing.T) { fmt.Println(1) goto three //跳转 fmt.Println(2) // 这行将会被跳过 three: fmt.Println(3) }执行结果如下:=== RUN TestGoto 1 3 --- PASS: TestGoto (0.00s) PASSgoto 标签放上面,下面都可以的.看下面的例子func TestGoto1(t *testing.T) { one: fmt.Println(1) goto one //跳转 fmt.Println(2) // 这行将会被跳过 fmt.Println(3) }执行结果, 不断循环打印。11 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1breakbreak 一般用来跳出当前所在的循环, 但是我们有业务场景,需要使用到 跳出带外层循环怎么办?break label 跳出循环不再执行for循环里的代码。可以使用 break 加标签的方式,举个例子。func TestBreak(t *testing.T) { OUTER: for { fmt.Println(1) for { fmt.Println(2) break OUTER } } fmt.Println(3) }break标签只能用于for循环,不能和switch使用,在其他语言里switch与break是搭档执行结果=== RUN TestBreak 1 2 3 --- PASS: TestBreak (0.00s) PASS这里要注意 一点break label,break 的跳转标签(label)必须放在循环语句for前.比如说, 下面的代码是不允许的func TestBreak1(t *testing.T) { for { fmt.Println(1) for { fmt.Println(2) break OUTER } } OUTER: fmt.Println(3) }IDE 也会告诉你异常continuecontinue label 这个功能和 break 优点类似,区别在于 break 是强制终止, continue 是继续循环下一个迭代。看个用例:func TestContinue(t *testing.T) { a := 10 Label: for a < 20 { if a == 15 { a++ //fmt.Println(a) continue Label } fmt.Println(a) a++ } }执行结果:=== RUN TestContinue 10 11 12 13 14 16 17 18 19 --- PASS: TestContinue (0.00s) PASS
ThreadLocal 实现原理在ThreadLocal的get(),set()的时候都会清除线程ThreadLocalMap里所有key为null的value。而ThreadLocal的remove()方法会先将Entry中对key的弱引用断开,设置为null,然后再清除对应的key为null的value。每一个Thread维护一个ThreadLocalMap映射表,映射表的key是ThreadLocal实例,并且使用的是ThreadLocal的弱引用 ,value是具体需要存储的Object。下面用一张图展示这些对象之间的引用关系,实心箭头表示强引用,空心箭头表示弱引用。ThreadLocal local = new ThreadLocal(); local.set("当前线程名称:"+Thread.currentThread().getName());//将ThreadLocal作为key放入threadLocals.Entry中 Thread t = Thread.currentThread();//注意断点看此时的threadLocals.Entry数组刚设置的referent是指向Local的,referent就是Entry中的key只是被WeakReference包装了一下 local = null;//断开强引用,即断开local与referent的关联,但Entry中此时的referent还是指向Local的,为什么会这样,当引用传递设置为null时无法影响传递内的结果 System.gc();//执行GC t = Thread.currentThread();//这时Entry中referent是null了,被GC掉了,因为Entry和key的关系是WeakReference,并且在没有其他强引用的情况下就被回收掉了 //如果这里不采用WeakReference,即使local=null,那么也不会回收Entry的key,因为Entry和key是强关联 //但是这里仅能做到回收key不能回收value,如果这个线程运行时间非常长,即使referent GC了,value持续不清空,就有内存溢出的风险 //彻底回收最好调用remove //即:local.remove();//remove相当于把ThreadLocalMap里的这个元素干掉了,并没有把自己干掉 System.out.println(local);使用InheritableThreadLocal,ThreadLocal threadLocal = new InheritableThreadLocal(),这样在子线程中就可以通过get方法获取到主线程set方法设置的值了。ThreadLocalMap 结构static class ThreadLocalMap { /** * 键值对实体的存储结构 */ static class Entry extends WeakReference<ThreadLocal<?>> { // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,必须为 2 的幂 private static final int INITIAL_CAPACITY = 16; // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂 private Entry[] table; // ThreadLocalMap 元素数量 private int size = 0; // 扩容的阈值,默认是数组大小的三分之二 private int threshold; }跟 Java 不同的是,采用的是线性探测法。private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // 以上代码,将entry的value赋值为null,这样方便GC时将真正value占用的内存给释放出来;将entry赋值为null,size减1,这样这个slot就又可以重新存放新的entry了 // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); // 从staleSlot后一个index开始向后遍历,直到遇到为null的entry (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { // 如果entry的key为null,则清除掉该entry e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { // key的hash值不等于目前的index,说明该entry是因为有哈希冲突导致向后移动到当前index位置的 tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) // 对该entry,重新进行hash并解决冲突 h = nextIndex(h, len); tab[h] = e; } } } return i; // 返回经过整理后的,位于staleSlot位置后的第一个为null的entry的index值 }在从第i个entry向后遍历的过程中,找到对应的key的entry就直接返回,如果遇到key为null的entry,则调用expungeStaleEntry方法进行清理。ThreadLocalMap set方法private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }ThreadLocalMap remove 方法找到准确的key对应的entry之后,调用Entry的clear方法,紧接着调用expungeStaleEntry,对key为null的entry进行清理。private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { // 考虑到可能的哈希冲突,一定要准确找到此key对应的entry e.clear(); // 调用Entry的clear方法,见代码2 expungeStaleEntry(i); // 又是这个清除key为null的entry的方法,见代码3 return; } } }平时怎么使用看个测试代码package thread; public class ThreadLocalDemo { /** * ThreadLocal变量,每个线程都有一个副本,互不干扰 */ public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); public static void main(String[] args) throws Exception { new ThreadLocalDemo().threadLocalTest(); } public void threadLocalTest() throws Exception { // 主线程设置值 THREAD_LOCAL.set("mainthreadvalue");// 设置的key 是主线程 String v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v); new Thread(new Runnable() { @Override public void run() { String v = THREAD_LOCAL.get(); // 获取的是key 是当前线程 渠道的是 null System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v); // 设置 threadLocal THREAD_LOCAL.set("inThreadValue"); // 设置的 key 是当前咸亨 v = THREAD_LOCAL.get(); System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v); System.out.println(Thread.currentThread().getName() + "线程执行结束"); } }).start(); // 等待所有线程执行结束 Thread.sleep(3000L); v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v); } }执行结果:Thread-0线程执行之前,main线程取到的值:mainthreadvalue Thread-0线程取到的值:null 重新设置之后,Thread-0线程取到的值为:inThreadValue Thread-0线程执行结束 Thread-0线程执行之后,main线程取到的值:mainthreadvalue面试指南面试官:那ThreadLocal中弱引用导致的内存泄漏是如何发生的?小白:如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,导致value对应的Object一直无法被回收,产生内存泄露。面试官:如何解决?小白:查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。解决办法是每次使用完ThreadLocal都调用它的remove()方法清除数据,或者按照JDK建议将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉。
1. 下载fedora的 epel 仓库yum install epel-release2. 安装 redisyum install redis3. 查看 redis 状态安装完毕后需要启动# 启动redis service redis start # 停止redis service redis stop # 查看redis运行状态 service redis status # 查看redis进程 ps -ef | grep redis4. 设置 redis 开启启动chkconfig redis on5. 进入 redis 服务# 进入本机redis redis-cli # 列出所有key keys *6. 防火墙开放端口# 开启6379 /sbin/iptables -I INPUT -p tcp --dport 6379 -j ACCEPT # 开启6380 /sbin/iptables -I INPUT -p tcp --dport 6380 -j ACCEPT # 保存 /etc/rc.d/init.d/iptables save # centos 7下执行 service iptables save7. 修改redis默认端口和密码默认端口一般是 6379 ,但也可以改成你想要的端口。打开配置文件vi /etc/redis.conf修改默认端口,查找 port 6379 修改为相应端口即可修改默认密码,查找 requirepass foobared 将 foobared 修改为你的密码使用配置文件启动redis-server /etc/redis.conf &使用端口登陆redis-cli -h 127.0.0.1 -p 6179输入刚才输入的密码auth 111如何关闭 redis使用命令关闭redis-cli -h 127.0.0.1 -p 6179 shutdown杀掉进程ps -ef | grep redis kill -9 XXX
安装环境linux版本:CentOS 8.+ x64 Mysql:8.01. 下载 MySQL 的 Yum 源下载MySQL的 Yum Repository。 一般需要根据 CentOS 版本选择 MySQL下载命令:wget https://dev.mysql.com/get/mysql80-community-release-el7-2.noarch.rpm2. 用 yum命令安装下载好的rpm包。yum -y install mysql80-community-release-el7-2.noarch.rpm3. 安装 MySQL Serveryum -y install mysql-community-server安装过程中可能遇到如下问题not found 问题解决办法yum module disable mysql后续继续执行yum -y install mysql-community-server** Error: GPG check FAILED 问题**yum -y install mysql-community-server --nogpgcheck安装完成之后如下:查看是否安装成功启动 MySQL 命令systemctl start mysqld.service查看 MySQL. 运行状态systemctl status mysqld.service其中Active后面代表状态启动服务后为active (running),停止后为inactive (dead)4. 登陆 MySQL初识时会给个固定密码,MySQL已经开始正常运行,要进入MySQL还得先找出此时root用户的密码,使用如下命令可以找出密码:grep "password" /var/log/mysqld.log操作如下:[root@iZbp19brmfd0q5cfdumjwlZ software]# grep "password" /var/log/mysqld.log 2022-01-22T04:02:58.667571Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: kpPz&&-ew4Bq登陆命令mysql -u root -p修改密码mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'new password';新密码设置的时候如果设置的过于简单会报错, 如果想改个简单秘密,需要进行如下操作:简单说明:show VARIABLES LIKE 'validate_password%';密码的长度是由 validate_password_length决定的 ·validate_password_length的 计算公式如下:validate_password_length = validate_password_number_count + validate_password_special_char_count + (2 * validate_password_mixed_case_count)validate_password_policy 代表密码策略:默认是1:符合长度,且必须含有数字,小写或大写字母,特殊字符。设置为0判断密码的标准就基于密码的长度了。要想密码简单,操作如下:mysql> set global validate_password.policy=0;validate_password_length代表密码长度,最小值为4mysql> set global validate_password.length=4;操作完成后,结果如下:此时可以设置一个很简单的密码,例如1234 abcd之类的。还有一个问题,就是因为安装了 Yum Repository,以后每次yum操作都会自动更新,需要把这个卸载掉:[root@localhost ~]# yum -y remove mysql80-community-release-el7-2.noarch执行 SQL 时候,还可能遇到一个问题:this is incompatible with sql_mode=only_full_group_by### Error querying database. Cause: java.sql.SQLSyntaxErrorException: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'yshopb2c.yx_store_order.create_time' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by ### The error may exist in co/yixiang/modules/order/service/mapper/StoreOrderMapper.java (best guess) ### The error may involve co.yixiang.modules.order.service.mapper.StoreOrderMapper.chartList-Inline ### The error occurred while setting parameters ### SQL: SELECT IFNULL(sum(pay_price),0) as num,DATE_FORMAT(create_time, '%m-%d') as time FROM yx_store_order where refund_status=0 and is_del=0 and paid=1 and pay_time >= ? GROUP BY DATE_FORMAT(create_time,'%Y-%m-%d') ORDER BY create_time ASC ### Cause: java.sql.SQLSyntaxErrorException: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'yshopb2c.yx_store_order.create_time' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'yshopb2c.yx_store_order.create_time' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_fu问题原因:通过查阅资料发现是因为下载安装的是最新版的mysql5.7.x版本,默认是开启了 only_full_group_by 模式的,但开启这个模式后,原先的类似 group by语句就报错,然后又把它移除了。就可以了。操作如下:找到 MySqL 配置文件my.cnf。增加一行 sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION[mysql] default-character-set=utf8 [mysqld] port = 3306 # Binary Logging log-bin=mysql-bin binlog-format=Row #Server ID server-id=201901 #basedir=D:\MySQL\mysql-5.7.14-winx64 #datedir=D:\MySQL\mysql-5.7.14-winx64\data max_connections=200 character-set-server=utf8 default-storage-engine=INNODB skip-grant-tables sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTI5. 设置 MySQL 外网访问sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION操作如下mysql> use mysql; mysql> update user set host="%" where user='root'; mysql> flush privileges;允许外网连接ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root1234.';mysql 配置说明1 /etc/my.cnf 这是mysql的主配置文件 2 /var/lib/mysql mysql数据库的数据库文件存放位置 3 /var/log mysql数据库的日志输出存放位置 4.service mysqld start #启动 5.service mysqld restart #重启 6.service mysqld stop # 停掉如果还是不能 方法,有可能是 阿里云 权限问题设置安全组, 首先检查你的阿里或腾讯的服务器控制台是否开启3306端口访问权限,怎么看安全组在哪,自行百度。连接成功后,结果如下:
测试分为4个层次单元测试:对代码进行测试集成测试:对一个服务的接口测试端到端测试(链路测试):从一个链路的入口输入测试用例,验证输出的系统的结果UI测试常犯的错误:没有断言。没有断言的单测是没有灵魂的。单测的特征:A:(Automatic,自动化):单元测试应该是全自动执行的,并且非交互式的I:(Independent,独立性):为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。R:(Repeatable,可重复):单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。单测代码 bug 总是在所难免, 越早发现问题解决成本越低, 单测可以尽早的暴露错误。提高代码之路,使得项目更高质量的交付。起码有三个优点:提高代码质量 编写单测是自测的一部分,编写新代码时增加相应的单测,可以帮助我们发现大部分的bug,有助于减少联调时的调整,提高联调效率。花更少的时间进行功能测试 功能测试成本相对较高,因为经常需要执行一系列操作以验证结果是否符合预期。如果问题如果发现了问题,沟通和复测往往要花费很多的时间。花更少的时间进行回归测试 回归测试是为了避免在对应用程序进行更改时引入bug。测试人员不仅要测试他们的新特性,还要测试以前存在的特性,以验证之前实现的特性是否仍然像预期的那样运行。通过单元测试,可以在每次构建之后,重新运行整个测试流程,以确保新代码不会破坏已有功能测试异常场景 一些异常的场景QA不好构造,比如并发出款是否资金安全,事务异常相关测试等等。而问题经常出现在这些异常的场景,可能引发线上问题甚至是事故。而单元测试可通过mock的方式方便的模拟各种异常场景。Go 单元测试工具gomonkey引入 gomonkey 有如下好处:隔离被测代码加速执行测试使执行变得确定模拟特殊情况功能列表支持为一个函数打一个桩支持为一个函数打一个特定的桩序列支持为一个成员方法打一个桩支持为一个成员方法打一个特定的桩序列支持为一个函数变量打一个桩支持为一个函数变量打一个特定的桩序列支持为一个接口打桩支持为一个接口打一个特定的桩序列支持为一个全局变量打一个桩函数打桩, 对变量的 mock 实现原理跟 gostub 一样都是通过 reflect 包实现的。除了 mock 变量,gomonkey 还可以直接 mock 导出函数/方法、mock 代码所在包的非导出函数Go monkey Permission Denied 解决方案:https://github.com/eisenxp/macos-golink-wrappermv $GOROOT/pkg/tool/darwin_amd64/link $GOROOT/pkg/tool/darwin_amd64/original_link cp https://github.com/eisenxp/macos-golink-wrapper/link $GOROOT/pkg/tool/darwin_amd64/link下载文件,然后再 cpwget https://raw.githubusercontent.com/eisenxp/macos-golink-wrapper/main/linkgomonkey 提供了如下 mock 方法:ApplyGlobalVar(target, double interface{}):使用 reflect 包,将 target 的值修改为 doubleApplyFuncVar(target, double interface{}):检查 target 是否为指针类型,与 double 函数声明是否相同,最后调用 ApplyGlobalVarApplyFunc(target, double interface{}):修改 target 的机器指令,跳转到 double 执行ApplyMethod(target reflect.Type, methodName string, double interface{}):修改 method 的机器指令,跳转到 double 执行ApplyFuncSeq(target interface{}, outputs []OutputCell):修改 target 的机器指令,跳转到 gomonkey 生成的一个函数执行,每次调用会顺序从 outputs 取出一个值返回ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell):修改 target 的机器指令,跳转到 gomonkey 生成的一个方法执行,每次调用会顺序从 outputs 取出一个值返回ApplyFuncVarSeq(target interface{}, outputs []OutputCell):gomonkey 生成一个函数顺序返回 outputs 中的值,调用 ApplyGlobalVargomonkey 打桩失败的可能原因gomonkey 不是并发安全的。如果有多协程并发对同一个目标的打桩的情况,则需要将之前的协程先优雅退出。打桩目标为内联的函数或成员方法。可通过命令行参数 -gcflags=-l (go1.10 版本之前)或-gcflags=all=-l(go1.10 版本及之后)关闭内联优化。gomonkey 对于私有成员方法的打桩失败。go1.6 版本的反射机制支持私有成员方法的查询,而 go1.7 及之后的版本却不支持,所以当用户使用 go1.7 及之后的版本时,gomonkey 对于私有成员方法的打桩会触发异常。goconvey为全局变量打一个桩package unittest import ( "testing" "github.com/agiledragon/gomonkey" "github.com/smartystreets/goconvey/convey" ) var num = 10 //全局变量 func TestApplyGlobalVar(t *testing.T) { convey.Convey("TestApplyGlobalVar", t, func() { convey.Convey("change", func() { patches := gomonkey.ApplyGlobalVar(&num, 150) defer patches.Reset() convey.So(num, convey.ShouldEqual, 150) }) convey.Convey("recover", func() { convey.So(num, convey.ShouldEqual, 10) }) }) }执行结果:=== RUN TestApplyGlobalVar .. 2 total assertions --- PASS: TestApplyGlobalVar (0.00s) PASS为一个函数打桩func networkCompute(a, b int) (int, error) { // do something in remote computer c := a + b return c, nil } func Compute(a, b int) (int, error) { sum, err := networkCompute(a, b) return sum, err } func TestFunc(t *testing.T) { // mock 了 networkCompute(),返回了计算结果2 patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) { return 2, nil }) defer patches.Reset() sum, err := Compute(1, 2) println("expected %v, got %v", 2, sum) if sum != 2 || err != nil { t.Errorf("expected %v, got %v", 2, sum) } }结果:=== RUN TestFunc expected %v, got %v 2 3 mock_func_test.go:91: expected 2, got 3 --- FAIL: TestFunc (0.00s) FAIL可以看到上面的结果,执行时失败的,mock 没有成功。有时会遇到mock失效的情况,这个问题一般是内联导致的。什么是内联?为了减少函数调用时的堆栈等开销,对于简短的函数,会在编译时,直接内嵌调用的代码。我们禁用下内联,然后执行,go test -v -gcflags=-l mock_func_test.go执行结果:=== RUN TestFunc expected %v, got %v 2 2 --- PASS: TestFunc (0.00s) PASS对于 go 1.10以下版本,可使用-gcflags=-l禁用内联,对于go 1.10及以上版本,可以使用-gcflags=all=-l。但目前使用下来,都可以。关于gcflags的用法,可以使用 go tool compile --help 查看 gcflags 各参数含义
局部变量定义:定义在{}里面的变量为局部变量 作用域:只能在{}里面有效;执行到定义的那句话,开始分配内存空间,离开作用域自动进行释放局部变量一定是在函数内部声明在哪个{}内部声明; 执行到定义的那句话,开始分配内存,只能在哪个{}内部访问,离开作用域自动进行释放看个例子package main import "fmt" func main() { //定义在{}里面的变量就是局部变量,只能在{}里面有效 //执行到定义变量那句话,才开始分配空间,离开作用域自动释放 //作用域,变量其作用的范围 if flag := 3; flag == 3 { fmt.Println("flag = ", flag) } //flag = 4 不能在if外面执行 报错:undefined: flag 未定义的标记 }全局变量定义:在函数外部的变量称为全局变量 作用域:同一个包内的任何地方小写,整个包可以访问大写,跨包可以访问package constant var A = 12123 var B = map[string]string{} var c = "xiaoming" func Init() { A = 1321312 B["default"] = "default" }测试:同一个包可以访问package constant import ( "fmt" "testing" ) func TestGlobal(t *testing.T) { //全局变量声明到函数外部,整个包都可以访问 //如果全局变量首字母大写,跨包也可以访问. fmt.Println(c) }执行结果:=== RUN TestGlobal xiaoming --- PASS: TestGlobal (0.00s) PASS测试:跨包访问package variable import ( "fmt" //"go/constant" "testing" "/GoProject/main/gobase/constant" ) func TestGlobal(t *testing.T) { constant.Init() fmt.Println(constant.A) fmt.Println(constant.B["default"]) fmt.Println(constant.c) // 会报错 }注释掉报错那一行,执行结果如下:=== RUN TestGlobal 1321312 default --- PASS: TestGlobal (0.00s) PASS全局变量要避免的坑:例如定义了一个全局变量, 然后又使用了 := 给全局变量赋值, 此时会出现问题。看下面的例子:package dbops import ( "database/sql" _ "github.com/go-sql-driver/mysql" "log" ) var ( dbConn *sql.DB err error ) func init() { dbConn, err := sql.Open("mysql","root:000000@tcp(localhost:3306)/server?charset=utf8") if err != nil{ panic(err.Error()) } log.Println(dbConn) } func main() { log.Println("查看全局变量dbConn:",dbConn) }执行结果如下:panic: runtime error: invalid memory address or nil pointer dereference [recovered] panic: runtime error: invalid memory address or nil pointer dereference [signal 0xc0000005 code=0x0 addr=0x0 pc=0x5b0a61]远远是因为 使用的是 := 对全局变量赋值,结果是全局变量未赋值是个 nil, init 中的 dConn 使用 := 生成的,这里的 dbConn 是局部变量, 全局变量 dbConn 并没有赋值,还是 nil。还有个坑,最近看代码, 发现 全局变量名字一样,然后,在 init 也初始化了, 然后跨包应用这个全局变量时,这个全局变量还是个 nil, 查了半天,才看到全局变量的名字是一样的,但是归属不同的 包 A, B , 包 A 初始化了,但是用的是 包 B 的全局变量的值, 包B 全局变量并没有被初始化!!!!
Go克隆几种方式序列化的方式实现深度拷贝最简单的方式是基于序列化和反序列化来实现对象的深度复制:func deepCopy(dst, src interface{}) error { var buf bytes.Buffer if err := gob.NewEncoder(&buf).Encode(src); err != nil { return err } return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst) }测试用例import ( "bytes" "encoding/gob" "encoding/json" "fmt" "log" "reflect" "testing" "github.com/mohae/deepcopy" ) type Basics struct { A string B int C []string } func TestClone1(t *testing.T) { var src2 = &Basics{ A: "hello world", B: 34, C: []string{"1213", "1312"}, } dst := new(Basics) if err := deepCopy(dst, src2); err != nil { log.Fatal(err) } log.Printf("%+v", dst) }执行结果:=== RUN TestClone1 2021/12/23 10:37:56 &{A:hello world B:34 C:[1213 1312]} --- PASS: TestClone1 (0.00s) PASS反射实现深度拷贝深度复制可以基于reflect包的反射机制完成, 但是全部重头手写的话会很繁琐./深度克隆,可以克隆任意数据类型 func DeepClone(src interface{}) (interface{}, error) { typ := reflect.TypeOf(src) if typ.Kind() == reflect.Ptr { typ = typ.Elem() dst := reflect.New(typ).Elem() b, _ := json.Marshal(src) if err := json.Unmarshal(b, dst.Addr().Interface()); err != nil { return nil, err } return dst.Addr().Interface(), nil } else { dst := reflect.New(typ).Elem() b, _ := json.Marshal(src) if err := json.Unmarshal(b, dst.Addr().Interface()); err != nil { return nil, err } return dst.Interface(), nil } }测试用例import ( "bytes" "encoding/gob" "encoding/json" "fmt" "log" "reflect" "testing" "github.com/mohae/deepcopy" ) type Basics struct { A string B int C []string } func TestDeepClone(t *testing.T) { var src = &Basics{ A: "hello world", B: 34, C: []string{"1213", "1312"}, } dst, _ := DeepClone(src) fmt.Println(dst) }执行结果:=== RUN TestDeepClone &{hello world 34 [1213 1312]} --- PASS: TestDeepClone (0.00s) PASS借助包深拷贝"github.com/mohae/deepcopy"使用方式如下:dst := deepcopy.Copy(src)测试用例import "github.com/mohae/deepcopy" type BasicsSmall struct { a string b int c []string } func TestDeepCopy(t *testing.T) { //src := &User{Name: "xiaomng", Age: 100} var src = &BasicsSmall{ a: "hello world", b: 34, c: []string{"1213", "1312"}, } dst := deepcopy.Copy(src) fmt.Println(dst) }测试结果:=== RUN TestDeepCopy &{ 0 []} --- PASS: TestDeepCopy (0.00s) PASS为啥会出现上面的结果,因为 struct 结构,首字母都是小写的!!!换个用例import "github.com/mohae/deepcopy" type User struct { Name string // 小写变量,不能被deepCopy函数拷贝成功 Age int } func TestDeepCopy(t *testing.T) { src := &User{Name: "xiaomng", Age: 100} //var src = &BasicsSmall{ // a: "hello world", // b: 34, // c: []string{"1213", "1312"}, //} dst := deepcopy.Copy(src) fmt.Println(dst) }执行结果:=== RUN TestDeepCopy &{xiaomng 100} --- PASS: TestDeepCopy (0.00s) PASS总结上述拷贝有个问题,结构体中有小写成员变量时,上述方式无效。
业务背景有这样的业务场景, 线上一个表 tablea, 生产环境还有一个镜像表 tablea_mirror, 现在 你需要当请求中有一些 tag 标识的时候,访问 tablea_mirror 表,有时候会用到 DefaultTableNameHandler先安装 sqlitehttps://wangxiaoming.blog.csdn.net/article/details/121884736代码可以使用 DefaultTableNameHandler 来实现加前缀或者后缀功能。import ( "code.byted.org/gopkg/gorm" "context" ) type dbStagingPostfixKeyType struct{} var dbStagingPostfixKey = dbStagingPostfixKeyType{} func WithDbStagingPostfix(ctx context.Context, postfix string) context.Context { return context.WithValue(ctx, dbStagingPostfixKey, postfix) } func ReWriteTableName() { gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { if v := db.Ctx.Value(dbStagingPostfixKey); v != nil { return defaultTableName + v.(string) } return defaultTableName } }测试代码package mysql import ( "fmt" "testing" "github.com/jinzhu/gorm" //_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/mattn/go-sqlite3" ) type Product struct { gorm.Model Code string Price uint } func (Product) TableName() string { return "hax_products" } func Test(t *testing.T) { db, err := gorm.Open("sqlite3", "test.db") if err != nil { panic("failed to connect database") } defer db.Close() gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { return "hax_" + defaultTableName } db.LogMode(true) // Migrate the schema db.AutoMigrate(&Product{}) db.Create(&Product{Code: "L1212", Price: 1000}) var product Product db.First(&product, 1) var products []Product db.Find(&products) fmt.Printf("Total count %d", len(products)) }执行结果:(/Users/xxx/go/src/xxx/xxx.xx/GoProject/mysql/sqllite_test.go:33) [2021-12-14 21:43:33] [1.38ms] INSERT INTO "hax_products" ("created_at","updated_at","deleted_at","code","price") VALUES ('2021-12-14 21:43:33','2021-12-14 21:43:33',NULL,'L1212',1000) [1 rows affected or returned ] (/Users/xxx/go/src/xxx/xxx.xx/GoProject/mysql/sqllite_test.go:35) [2021-12-14 21:43:33] [0.23ms] SELECT * FROM "hax_products" WHERE "hax_products"."deleted_at" IS NULL AND (("hax_products"."id" = 1)) ORDER BY "hax_products"."id" ASC LIMIT 1 [1 rows affected or returned ] ((/Users/xxx/go/src/xxx/xxx.xx/GoProject/mysql/sqllite_test.go:37) [2021-12-14 21:43:33] no such table: hax_hax_products ((/Users/xxx/go/src/xxx/xxx.xx/GoProject/mysql/sqllite_test.go:37) [2021-12-14 21:43:33] [0.10ms] SELECT * FROM "hax_hax_products" WHERE "hax_hax_products"."deleted_at" IS NULL [0 rows affected or returned ] Total count 0--- PASS: Test (0.00s) PASS根据执行结果,可以看到,创建语言与查询单条记录时表名为 hax_products 但是查询 多条记录时,却使用了表名hax_hax_products.这个就是坑1查询单个记录时使用了TableName()返回的表名,而在查询结果为Array时,表名在TableName()的基础上又添加了前缀。Gorm 结构体 一般分析如下 structtype DB struct (gorm/main.go)代表数据库连接,每次操作数据库会创建出clone对象。方法gorm.Open()返回的值类型就是这个结构体指针。type Scope struct (gorm/scope.go) 当前数据库操作的信息,每次添加条件时也会创建clone对象。type Callback struct (gorm/callback.go) 数据库各种操作的回调函数, SQL生成也是靠这些回调函数。每种类型的回调函数放在单独的文件里,比如查询回调函数在gorm/callback_query.go, 创建的在gorm/callback_create.godb.First() 代码分析First()方法位于gorm/main.go文件中, .callCallbacks(s.parent.callbacks.queries)调用了query回调函数。// file: gorm/main.go // First find first record that match given conditions, order by primary key func (s *DB) First(out interface{}, where ...interface{}) *DB { newScope := s.NewScope(out) newScope.Search.Limit(1) return newScope.Set("gorm:order_by_primary_key", "ASC"). inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db }Callback结构体中定义queries为函数指针数组, 而默认值的初始化在gorm/callback_query.go的init()方法中, 查询方法为queryCallback, 而queryCallback()方法又调用到scope.prepareQuerySQL(), scope中的方法真正生成SQL的地方。// file: gorm/callback.go type Callback struct { logger logger creates []*func(scope *Scope) updates []*func(scope *Scope) deletes []*func(scope *Scope) queries []*func(scope *Scope) rowQueries []*func(scope *Scope) processors []*CallbackProcessor } // file: gorm/callback_query.go // Define callbacks for querying func init() { DefaultCallback.Query().Register("gorm:query", queryCallback) DefaultCallback.Query().Register("gorm:preload", preloadCallback) DefaultCallback.Query().Register("gorm:after_query", afterQueryCallback) } // queryCallback used to query data from database func queryCallback(scope *Scope) { ... scope.prepareQuerySQL() ... }跟踪代码到scope.go文件, 函数TableName()是获取数据库表名的地方。它按如下顺序来确定表名:scope.Search.tableName 查询条件中设置了表名, 则直接使用scope.Value.(tabler) 值对象实现了tabler接口(方法TableName() string), 则从调用方法获取scope.Value.(dbTabler) 值对象实现了dbTabler接口(方法TableName(*DB) string), 则从调用方法获取若以上条件都不成立,则从scope.GetModelStruct()中获取对象的结构体信息,从结构体名生成表名具体可见 scope.go 源码// file: gorm/scope.go func (scope *Scope) prepareQuerySQL() { if scope.Search.raw { scope.Raw(scope.CombinedConditionSql()) } else { scope.Raw(fmt.Sprintf("SELECT %v FROM %v %v", scope.selectSQL(), scope.QuotedTableName(), scope.CombinedConditionSql())) } return } // QuotedTableName return quoted table name func (scope *Scope) QuotedTableName() (name string) { if scope.Search != nil && len(scope.Search.tableName) > 0 { if strings.Contains(scope.Search.tableName, " ") { return scope.Search.tableName } return scope.Quote(scope.Search.tableName) } return scope.Quote(scope.TableName()) } // TableName return table name func (scope *Scope) TableName() string { if scope.Search != nil && len(scope.Search.tableName) > 0 { return scope.Search.tableName } if tabler, ok := scope.Value.(tabler); ok { return tabler.TableName() } if tabler, ok := scope.Value.(dbTabler); ok { return tabler.TableName(scope.db) } return scope.GetModelStruct().TableName(scope.db.Model(scope.Value)) }对比以上条件, 示例中的Product结构体定义了方法TableName() string,符合条件2,那么db.First(&product, 1)使用的表名就是hax_products。db.Find() 代码分析Find()代码如下,与First()同样是使用了callbacks.queries回调方法,不同点在于设置了newScope.Search.Limit(1)只返回一个结果、增加了按id排序。// Find find records that match given conditions func (s *DB) Find(out interface{}, where ...interface{}) *DB { return s.NewScope(out).inlineCondition(where...).callCallbacks(s.parent.callbacks.queries).db }在debug模式下跟踪代码到scope.TableName()中时,两次查询的区别显示出来了:它们的结果值类型不同。db.First(&product, 1)的值类型为结构体的指针*Product,而db.Find(&products)的值类型是数组的指针*[]Product, 从而导致db.Find(&products)进入条件 scope.GetModelStruct().TableName(scope.db.Model(scope.Value)) },需要靠分析struct结构体来生成表名。// file: gorm/model_struct.go // TableName returns model's table name func (s *ModelStruct) TableName(db *DB) string { s.l.Lock() defer s.l.Unlock() if s.defaultTableName == "" && db != nil && s.ModelType != nil { // Set default table name if tabler, ok := reflect.New(s.ModelType).Interface().(tabler); ok { s.defaultTableName = tabler.TableName() } else { tableName := ToTableName(s.ModelType.Name()) db.parent.RLock() if db == nil || (db.parent != nil && !db.parent.singularTable) { tableName = inflection.Plural(tableName) } db.parent.RUnlock() s.defaultTableName = tableName } } return DefaultTableNameHandler(db, s.defaultTableName) }默认表名s.defaultTableName为空值时先进行求值,reflect.New(s.ModelType).Interface().(tabler)先判断是否实现了tabler接口,有则调用其TableName()取值;否则的话从结构体的名字来生成表名。结果返回之前再调用 DefaultTableNameHandler(db, s.defaultTableName)方法。这个ModelStruct的TableName方法与scope.TableName() 中的逻辑两个不一致的地方:scope.TableName()会判断是否实现tabler与dbTabler两个接口,而这里只判断了tablerscope.TableName()是将tableName结果直接返回的, 而这里多调用了DefaultTableNameHandler()。因为逻辑 scope.TableName()的存在, 当重写DefaultTableNameHandler()方法时, 就会出现表前缀再次被添加了表名前。问题2DefaultTableNameHandler()在多数据库时出现混乱通过以上代码的分析,于是发现了另一个坑:当一个程序中使用两个不同的数据库时, 重写方法DefaultTableNameHandler()会影响到两个数据库中的表名。其中一个数据库需要设置表前缀时,访问另一个数据库的表也可能会被加上前缀。因为是包级别的方法,整个代码里只能设置一次值。// file: gorm/model_struct.go // DefaultTableNameHandler default table name handler var DefaultTableNameHandler = func(db *DB, defaultTableName string) string { return defaultTableName }总结当给结构体实现了TableName()方法时,就不要设置DefaultTableNameHandler了。保持所有Model的表名生成方式一致,要么全部使用自动生成的表名,要么全部实现tabler接口(实现- TableName()方法)当需要使用多个数据库时,要避免设置DefaultTableNameHandler强烈建议:所有Model结构体全部实现tabler接口
如果直接下载的话,报错如下:go get github.com/mattn/go-sqlite3 go get: module github.com/mattn/go-sqlite3: reading https://athens.azurefd.net/github.com/mattn/go-sqlite3/@v/list: 504 Gateway Timeout第一步Mac OS X 1. 经过 Homebrewn 安装:html brew install pkgconfig brew install sqlite3执行结果如下:➜ ~ brew install pkgconfig Warning: pkg-config 0.29.2_3 is already installed and up-to-date. To reinstall 0.29.2_3, run: brew reinstall pkg-config ➜ ~ brew install sqlite3 Warning: sqlite 3.37.0 is already installed and up-to-date. To reinstall 3.37.0, run: brew reinstall sqlite第二步brew link pkgconfig --force brew link sqlite3 --force第三步go get github.com/mattn/go-sqlite3结果如下:go: downloading github.com/mattn/go-sqlite3 v1.14.9sqllite 测试用例package mysql import ( "fmt" "testing" "github.com/jinzhu/gorm" //_ "github.com/jinzhu/gorm/dialects/sqlite" _ "github.com/mattn/go-sqlite3" ) type Product struct { gorm.Model Code string Price uint } func (Product) TableName() string { return "hax_products" } func Test(t *testing.T) { db, err := gorm.Open("sqlite3", "test.db") if err != nil { panic("failed to connect database") } defer db.Close() gorm.DefaultTableNameHandler = func(db *gorm.DB, defaultTableName string) string { return "hax_" + defaultTableName } db.LogMode(true) // Migrate the schema db.AutoMigrate(&Product{}) db.Create(&Product{Code: "L1212", Price: 1000}) var product Product db.First(&product, 1) var products []Product db.Find(&products) fmt.Printf("Total count %d", len(products)) }macos 安装有问题,可以参考 https://segmentfault.com/q/1010000000162180 这个解决
什么是循环依赖其实就 package A 引入了 package B ,然后 package B 又引入了 package A ,因此形成了循环依赖。现象如下:测试代码package A import ( "strings" B “/GoProject/main/gobase/cycle/b" ) func Foo(a string) (string) { return B.Add(a) } func Minus(a string) (string) { return strings.Trim(a, "\t") }package B import A "GoProject/main/gobase/cycle/a" func Goo(a string) (string) { return A.Minus(a) } func Add(a string) (string) { return a + "----" }运行测试代码:package cycle import ( "testing" A "GoProject/main/gobase/cycle/a" ) func TestCycle(t *testing.T) { A.Foo("good") }运行结果:packageGoProject/main/gobase/cycle (test) imports /GoProject/main/gobase/cycle/a importsGoProject/main/gobase/cycle/b imports GoProject/main/gobase/cycle/a: import cycle not allowed FAIL外观模式实现我们之前的java设计模式中介绍到了外观模式,发现这在很有用 我首先将包A,B中的方法抽象成接口,将方法先隔离出来package service type A interface { Minus(s string) (string) } type B interface { Add(s string) (string) }然后我A,B实现接口。为了容易处理,定义两个结构体进行处理。package A import ( "strings" "github.com/hundred666/GoTest/service" ) type AImpl struct { b service.B } func (a *AImpl) Foo(s string) (string) { return a.b.Add(s) } func (a *AImpl) Minus(s string) (string) { return strings.Trim(s, "\t") }B的设计如下:package B import "github.com/hundred666/GoTest/service" type BImpl struct { a service.A } func (b *BImpl) Goo(a string) (string) { return b.a.Minus(a) } func (b *BImpl) Add(a string) (string) { return a + "----" }实现了方法,得能够将实例化的变量分别放入A,B结构体中,因此A需要实现以下方法func NewA() *AImpl { return new(AImpl) } func (a *AImpl) SetB(b service.B) { a.b = b }B需要实现以下方法func NewB() *BImpl { return new(BImpl) } func (b *BImpl) SetA(a service.A) { b.a = a }需要调用的时候就可以去调用了package main import ( "github.com/hundred666/GoTest/B" "github.com/hundred666/GoTest/A" "fmt" ) func main() { b := B.NewB() a := A.NewA() a.SetB(b) r := a.Foo("aa") fmt.Println(r) }
前言plantUML 是门语言,这个,掌握了达到所见即所得的效果,即用编写语言的方式,就可以画出需要的时序图,流程图,用例图等。这里抛砖引玉,希望大家都能学习下,平时做系统设计的时候都能用得上。时序图简单例子你可以用-> 来绘制参与者之间传递的消息,而不必显式地声明参与者。你也可以使用 --> 绘制一个虚线箭头。另外,你还能用 <- 和 <--,这不影响绘图,但可以提高可读性。注意:仅适用于时序图,对于其它示意图,规则是不同的。@startuml 用户 -> 认证中心: 登录操作 认证中心 -> 缓存: 存放(key=token+ip,value=token)token 用户 <- 认证中心 : 认证成功返回token 用户 -> 认证中心: 下次访问头部携带token认证 认证中心 <- 缓存: key=token+ip获取token 其他服务 <- 认证中心: 存在且校验成功则跳转到用户请求的其他服务 其他服务 -> 用户: 信息 @enduml图例如下:生命线的激活和撤销@startuml participant User User -> A: DoWork activate A A -> B: << createRequest >> activate B B -> C: DoWork activate C C --> B: WorkDone destroy C B --> A: RequestCreated deactivate B A -> User: Done deactivate A @enduml图示如下:声明参与者使用 participant 关键字来声明一个参与者可以使你对参与者做出更多控制。关键字 participant 用于改变参与者的先后顺序。你也可以使用下面这些关键字来声明参与者,这会改变参与者的外观:actor(角色)boundary(边界)control(控制)entity(实体)database(数据库)collections(集合)queue(队列)@startuml participant participant as Foo actor actor as Foo1 boundary boundary as Foo2 control control as Foo3 entity entity as Foo4 database database as Foo5 collections collections as Foo6 queue queue as Foo7 Foo -> Foo1 : To actor Foo -> Foo2 : To boundary Foo -> Foo3 : To control Foo -> Foo4 : To entity Foo -> Foo5 : To database Foo -> Foo6 : To collections Foo -> Foo7 : To queue @enduml用例用例定义用例用圆括号括起来(两个圆括号看起来就像椭圆)。也可以用关键字 usecase 来定义用例。还可以用关键字 as 定义一个别名,这个别名可以在以后定义关系的时候使用。@startuml (First usecase) (Another usecase) as (UC2) usecase UC3 usecase (Last\nusecase) as UC4 @enduml角色定义角色用两个冒号包裹起来。也可以用 actor 关键字来定义角色。还可以用关键字 as 来定义一个别名,这个别名可以在以后定义关系的时候使用。在后面的例子中,我们会看到角色的定义是可选的@startuml :First Actor: :Another\nactor: as Man2 actor Woman3 actor :Last actor: as Person1 @enduml完整例子@startuml left to right direction skinparam packageStyle rectangle actor customer actor clerk rectangle checkout { customer -- (checkout) (checkout) .> (payment) : include (help) .> (checkout) : extends (checkout) -- clerk } @enduml类图元素声明@startuml abstract abstract abstract class "abstract class" annotation annotation circle circle () circle_short_form class class diamond diamond <> diamond_short_form entity entity enum enum interface interface @enduml类之间的关系类之间的关系通过下面的符号定义:使用.. 来代替 -- 可以得到点线. 在这些规则下,也可以绘制下列图形@startuml Class01 <|-- Class02 Class03 *-- Class04 Class05 o-- Class06 Class07 .. Class08 Class09 -- Class10 @enduml活动图(流程图)@startuml start if (condition A) then (yes) :Text 1; elseif (condition B) then (yes) :Text 2; stop elseif (condition C) then (yes) :Text 3; elseif (condition D) then (yes) :Text 4; else (nothing) :Text else; endif stop @enduml添加方法为了声明字段 (对象属性)或者方法,你可以使用后接字段名或方法名。系统检查是否有括号来判断是方法还是字段。@startuml Object <|-- ArrayList Object : equals() ArrayList : Object[] elementData ArrayList : size() @enduml
Go 基于令牌桶的限流器简介如果一般流量过大,下游系统反应不过来,这个时候就需要限流了,其实和上地铁是一样的,就是减慢上游访问下游的速度。限制访问服务的频次或者频率,防止服务过载,被刷爆等。Golang 官方扩展包 time(golang.org/x/time/rate) 中,提供了一个基于令牌桶等限流器实现。原理概述令牌:每次拿到令牌,才可访问桶 ,桶的最大容量是固定的,以固定的频率向桶内增加令牌,直至加满每个请求消耗一个令牌。限流器初始化的时候,令牌桶一般是满的。具体使用package limiter import ( "fmt" "testing" "time" "golang.org/x/time/rate" ) func TestLimter(t *testing.T) { limiter := rate.NewLimiter(rate.Every(time.Millisecond*31), 2) //time.Sleep(time.Second) for i := 0; i < 10; i++ { var ok bool if limiter.Allow() { ok = true } time.Sleep(time.Millisecond * 20) fmt.Println(ok, limiter.Burst()) } }执行结果:=== RUN TestLimter true 2 true 2 true 2 false 2 true 2 true 2 false 2 true 2 true 2 false 2 --- PASS: TestLimter (0.21s)通过执行结果可以看到, 令牌桶开始是2个满的,由于令牌的间隔比请求的间隔多了11ms(31-20), 所以每两个请求会失败一次。具体实现原理先看下限流器的创建方法:NewLimiterfunc NewLimiter(r Limit, b int) *Limiter { return &Limiter{ limit: r, burst: b, } }查看限流器数据结构 Limiter// The methods AllowN, ReserveN, and WaitN consume n tokens. type Limiter struct { mu sync.Mutex limit Limit burst int tokens float64 // last is the last time the limiter's tokens field was updated last time.Time // lastEvent is the latest time of a rate-limited event (past or future) lastEvent time.Time }burst 表示了桶的大小limit 表示放入桶的频率tokens 表示剩余令牌个数last 最近取走 token 的时间lastEvent 最近限流事件的时间当令牌桶发放后,会保留在 Reservation 对象中, 定义如下, Reservation 对象,描述了一个达到 timeToAct 时间后,可以获取到的令牌的数量 tokens 数。type Reservation struct { ok bool // 是否满足条件分配到了tokens lim *Limiter // 发送令牌的限流器 tokens int // tokens 的数量 timeToAct time.Time // 满足令牌发放的时间 limit Limit // 令牌发放速度 }限流器如何限流官方提供的限流器有阻塞等待, 也有直接判断方式的, 还有提供维护预留式等。如何实现限流的代码,在 reserveN 中。使用时,每次都调用了 Allow() 方法// Allow is shorthand for AllowN(time.Now(), 1). func (lim *Limiter) Allow() bool { return lim.AllowN(time.Now(), 1) } // AllowN reports whether n events may happen at time now. // Use this method if you intend to drop / skip events that exceed the rate limit. // Otherwise use Reserve or Wait. func (lim *Limiter) AllowN(now time.Time, n int) bool { return lim.reserveN(now, n, 0).ok }继续查看 reserverN 算法方法说明:三个参数:now, n, maxFutureReserve在 now 时间需要拿到 n 个令牌,最多等待的时间为 maxFutureReserve结果将返回一个预留令牌的对象 Reservation// maxFutureReserve specifies the maximum reservation wait duration allowed. // reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation { lim.mu.Lock() // 首先判断是否放入频次是否为无穷大,如果为无穷大,说明暂时不限流 if lim.limit == Inf { lim.mu.Unlock() return Reservation{ ok: true, lim: lim, tokens: n, timeToAct: now, } } // 拿到截止 now 时间时,可以获取的令牌 tokens 数量,上一次拿走令牌的时间是last now, last, tokens := lim.advance(now) // Calculate the remaining number of tokens resulting from the request. // 更新 tokens数量,把需要拿走的去掉 tokens -= float64(n) // Calculate the wait duration // 如果 tokens 数量为负数,说明需要等待,计算等待时间 WaitDuration var waitDuration time.Duration if tokens < 0 { waitDuration = lim.limit.durationFromTokens(-tokens) } // Decide result // 计算是否满足分配要求 // 1. 需要分配的大小不超过桶容量 // 2. 等待时间不超过设定的等待时长 ok := n <= lim.burst && waitDuration <= maxFutureReserve // Prepare reservation // 最后构造一个 Resvervation 对象 r := Reservation{ ok: ok, lim: lim, limit: lim.limit, } if ok { r.tokens = n r.timeToAct = now.Add(waitDuration) } // Update state // 需要更新当前 limit 的值 if ok { lim.last = now lim.tokens = tokens lim.lastEvent = r.timeToAct } else { lim.last = last } lim.mu.Unlock() return r }从实现上看, limiter 并不是每隔一段时间更新当前桶的数量,而是记录了上次访问时和当前桶中令牌的数量,当再次访问时,通过上次访问时间计算出当前令牌的数量,决定是否可以发放令牌。
下载测试代码go get 中可以获取测试程序, 注意加上 -d 避免下载后自动安装。Githubgo get -d github.com/wolfogre/go-pprof-practice cd $GOPATH/src/github.com/wolfogre/go-pprof-practice如果 go get 下载不了, 可以 git clone 下载gir clone https://github.com/wolfogre/go-pprof-practice对代码进行编译然后运行go mod init go mod tidy最后再运行go build ./go-pprof-practice运行 pprof 命令go tool pprof http://localhost:6060/debug/pprof/heap还是三板斧top, list 等命令list 命令可以看到这次出问题的地方在 github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal,函数内容如下:func (m *Mouse) Steal() { log.Println(m.Name(), "steal") max := constant.Gi for len(m.buffer) * constant.Mi < max { m.buffer = append(m.buffer, [constant.Mi]byte{}) } }可以看到,这里有个循环会一直向 m.buffer 里追加长度为 1 MiB 的数组,直到总容量到达 1 GiB 为止,且一直不释放这些内存,这就难怪会有这么高的内存占用了。使用 web 来查看图形化展示,可以再次确认问题确实出在这里:
内存模型Go 内存模型描述的是 “在一个 groutine 中对变量进行读操作能够侦测到在其他 gorountine 中对改变量的写操作” 的条件。happen-before定义To specify the requirements of reads and writes, we define happens before, a partial order on the execution of memory operations in a Go program. If event e1 happens before event e2, then we say that e2 happens after e1. Also, if e1 does not happen before e2 and does not happen after e2, then we say that e1 and e2 happen concurrently.这是 Happens Before 的定义,如果 e1 发生在 e2 之前,那么我们就说 e2 发生在 e1 之后,如果 e1 既不在 e2 前,也不在 e2 之后,那我们就说这俩是并发的.关于channel的happens-before在Go的内存模型中提到了三种情况:case1: 对一个channel的发送操作 happens-before 相应channel的接收操作完成case2: 关闭一个channel happens-before 从该Channel接收到最后的返回值0case3: 不带缓冲的channel的接收操作 happens-before 相应channel的发送操作之前case1:对一个channel的发送操作 happens-before 相应channel的接收操作完成测试代码:import "testing" var c = make(chan int, 10) var a string func f() { a = "hello, world" // (1) c <- 0 // (2) 写操作 发送操作 } func TestMemoryModel(t *testing.T) { go f() <-c // (3) //接收操作 print(a) // (4) }上面的代码,将保证会打印出 hello world 。有缓冲 channel 写操作发生在接收操作之前。不带缓冲的channel的接收操作 happens-before 相应channel的发送操作之前var c1 = make(chan int) var a1 string func f1() { a1 = "hello, world" // (1) <-c1 // (2) 接收操作 } func TestMemoryModel1(t *testing.T) { go f1() c1 <- 0 // (3) 发送操作 print(a1) // (4) }运行结果:=== RUN TestMemoryModel1 hello, world--- PASS: TestMemoryModel1 (0.00s) PASS上面的代码将保证会打印出 hello world 。因为根据上面的第三条规则(2) happens-before (3),最终可以保证(1) happens-before (4)。无缓冲 channel 接收操作发生在写操作之前。再看个例子var c2= make(chan int, 1) var a2 string func f2() { a2 = "hello, world" // (1) <-c2 // 接收操作 } // 不能保证 打印出 "hello, world" func TestMemoryModel2(t *testing.T) { go f2() c2 <- 0 // (3) print(a2) // (4) 写操作 //var day time.Time //print(day.Format("20060102")) }上面的代码不能保证打印出 hello world , 因为输出的channel 是有缓冲的,不能保证接收操作发生在写操作之前,但是能保证写操作发生在接收操作之前。
什么是火焰图火焰图(Flame Graph)是由 Linux 性能优化大师 Brendan Gregg 发明的,和所有其他的 profiling 方法不同的是,火焰图以一个全局的视野来看待时间分布,它从底部往顶部,列出所有可能导致性能瓶颈的调用栈。火焰图 svg 文件可以通过浏览器打开,它对于调用图的优点是:可以通过点击每个方块来分析它上面的内容。火焰图的调用顺序从下到上,每个方块代表一个函数,它上面一层表示这个函数会调用哪些函数,方块的大小代表了占用 CPU 使用的长短。火焰图的配色并没有特殊的意义,默认的红、黄配色是为了更像火焰而已。火焰图特征每一列代表一个调用栈,每一个格子代表一个函数纵轴展示了栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,正在占用 cpu 的函数。横轴的意义是指:火焰图将采集的多个调用栈信息,通过按字母横向排序的方式将众多信息聚合在一起。需要注意的是它并不代表时间。横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。其他的采样方式也可以使用火焰图, on-cpu 火焰图横轴是指 cpu 占用时间,off-cpu 火焰图横轴则代表阻塞时间。采样可以是单线程、多线程、多进程甚至是多 host火焰的每一层都会标注函数名,鼠标悬浮时会显示完整的函数名、抽样抽中的次数、占据总抽样次数的百分比。下面是一个例子。在某一层点击,火焰图会水平放大,该层会占据所有宽度,显示详细信息。按下 Ctrl + F 会显示一个搜索框,用户可以输入关键词或正则表达式,所有符合条件的函数名会高亮显示。总的来说颜色本身没有什么意义纵向表示调用栈的深度横向表示消耗的时间上述几个部分分别体现什么横向来看由于横向表示消耗的时间,所以一个格子的宽度越大越说明其可能是瓶颈纵向来看由于纵向表示调用栈的深度,所以火焰的火苗尖部就是CPU正在执行的操作综上主要看那些比较宽大的火苗特别是那些平头的火苗火焰图类型
下载测试代码go get 中可以获取测试程序, 注意加上 -d 避免下载后自动安装。Githubgo get -d github.com/wolfogre/go-pprof-practice cd $GOPATH/src/github.com/wolfogre/go-pprof-practice如果 go get 下载不了, 可以 git clone 下载gir clone https://github.com/wolfogre/go-pprof-practice对代码进行编译然后运行go mod init go mod tidy最后再运行go build ./go-pprof-practice保持程序运行,打开浏览器访问 http://localhost:6060/debug/pprof/,可以看到如下页面:内存泄漏问题排查golang 和 Java 有点类似,自带了内存回收, 所以一般不会发生内存泄漏。但是也不绝对, golang 中 协程本身是可能泄漏的,或者叫做协程协程失控,进而导致内存泄漏。启动程序为了能更加图形化的展示,可以安装。graphviz安装方式brew install graphviz # for macos apt install graphviz # for ubuntu yum install graphviz # for centos安装完成后,我们继续在上文的交互式终端里输入 web,注意,虽然这个命令的名字叫“web”,但它的实际行为是产生一个 .svg 文件,并调用你的系统里设置的默认打开 .svg 的程序打开它。如果你的系统里打开 .svg 的默认程序并不是浏览器(比如可能是你的代码编辑器),这时候你需要设置一下默认使用浏览器打开 .svg 文件浏览器访问 http://localhost:6060/debug/pprof/可以看到协程有 46 个, 使用 pprof 排查一下go tool pprof http://localhost:6060/debug/pprof/goroutine可以看到 cum 那一行 , 是 Drink 有40 个协程。查看 list.Drink输入 web, 浏览器可以看到 冒红的那部分。可能这次问题藏得比较隐晦,但仔细观察还是不难发现,问题在于 github.com/wolfogre/go-pprof-practice/animal/canidae/wolf.(*Wolf).Drink 在不停地创建没有实际作用的协程:func (w *Wolf) Drink() { log.Println(w.Name(), "drink") for i := 0; i < 10; i++ { go func() { time.Sleep(30 * time.Second) }() } }可以看到 Drink 函数 ,每次循环会有创建10个协程, 协程会 sleep 30s 才会退出,如果反复调用这个 Drink 函数, 那么会导致大量协程出现泄漏,协程数会增加。
内推美团:求一份 Java 简历,坐标:北京。右下角“与我联系”有我联系方式前言如果要在 golang 开发过程中进行性能调优,一般需要使用 pprof,本文介绍的是 pprof 工具使用方法。下载测试代码go get 中可以获取测试程序, 注意加上 -d 避免下载后自动安装Githubgo get -d github.com/wolfogre/go-pprof-practice cd $GOPATH/src/github.com/wolfogre/go-pprof-practice如果 go get 下载不了, 可以 git clone 下载gir clone https://github.com/wolfogre/go-pprof-practice对代码进行编译然后运行go mod init go mod tidy最后再运行go build ./go-pprof-practice保持程序运行,打开浏览器访问 http://localhost:6060/debug/pprof/,可以看到如下页面:参数说明类型描述备注allocs内存分配情况的采样信息可以用浏览器打开,但可读性不高blocks阻塞操作情况的采样信息可以用浏览器打开,但可读性不高cmdline显示程序启动命令及参数可以用浏览器打开,这里会显示 ./go-pprof-practicegoroutine当前所有协程的堆栈信息可以用浏览器打开,但可读性不高heap堆上内存使用情况的采样信息可以用浏览器打开,但可读性不高mutex锁争用情况的采样信息可以用浏览器打开,但可读性不高profileCPU 占用情况的采样信息浏览器打开会下载文件threadcreate系统线程创建情况的采样信息可以用浏览器打开,但可读性不高trace程序运行跟踪信息浏览器打开会下载文件,本文不涉及代码说明测试代码程序中 main 函数的说明import ( // 略 _ "net/http/pprof" // 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样报告 // 略 ) func main() { // 略 runtime.GOMAXPROCS(1) // 限制 CPU 使用数,避免过载 runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪 runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪 go func() { // 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了 if err := http.ListenAndServe(":6060", nil); err != nil { log.Fatal(err) } os.Exit(0) }() // 略 }排查 CPU 占用过高问题可以通过活动监视器查看下 practice 程序的占用。可以使用 go tool pprof http://localhost:6060/debug/pprof/profile 进行排查。输入 top 命令, 查看 CPU 占用较高的调用:可以看到的是其中一百亿次空循环占用了大量 CPU 时间,因此就定位到了问题。
defer 是什么?defer 修饰的函数是一个延迟函数,在包含它的函数返回时运行。defer 执行时机A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panickingdefer 触发时机是:函数执行到函数体末端函数执行return语句当前协程panicdefer 实现原理defer 数据结构源码在 src/runtime/runtime2.go type _defer struct { siz int32 // 由deferproc第一个参数传入,参数和结果的内存大小 started bool // 标识defer函数是否已经开始执行 heap bool // 堆分配、栈分配 openDefer bool //表示当前 defer 是否经过开放编码的优化 sp uintptr // 栈指针程序计数器,注册defer函数的函数栈指针 pc uintptr // 调用方程序计数器,deferproc函数返回后要继续执行的指令地址 fn *funcval // 由deferproc的第二个参数传入,也就是被注册的defer函数 _panic *_panic // 是触发defer函数执行的panic指针,正常流程执行defer时它就是nil link *_defer //结构体是延迟调用链表上的一个元素,所有的结构体都会通过 link 字段串联成链表。 }defer 执行机制在中间代码生成阶段, 有三种不同的机制处理 defer 关键字堆上分配(Go 版本1.1-1.12)默认兜底方案栈分配(Go版本 1.13 )相比堆分配能够减少 30%堆额外开销。开放编码(Go 版本 1.14) 额外开销可以忽略不计。堆上分配在defer 语句堆位置插入 runtime.deferproc, 在被执行时,延迟调用会被保存为一个 _defer 记录,并将被延迟调用的入口地址与参数复制保存,存入 Gorountine 的调用链表中。在函数返回之前的位置插入 runtime.deferreturn,当被执行时,会将延迟调用从 Goroutine 链表中取出并执行,多个延迟调用则以 jmpdefer 尾递归调用方式连续执行runtime.deferproc 负责注册, runtime.deferreturn 负责执行。derfer 关键字最重要的三个函数deferproc。在每遇到一个defer关键字时,实际上都会转换为deferproc函数,deferproc函数的作用是将defer函数存入链表中。(go关键字是使用newproc函数,它两的实现有着不少相似之处)deferreturn。在return指令前调用,从链表中取出defer函数并执行。deferprocStack。go1.13后对defer做的优化,通过利用栈空间提高效率。call 函数cmd/compile/internal/gc.state.call 会负责为所有函数和方法调用生成中间代码:获取需要执行的函数名,代码制作和函数调用的接收方需要获取栈地址并将函数或者方法的参数写入栈中使用 cmd/compile/internal/gc.state.newValue1A 函数生成函数调用的中间代码如果当前调用的函数是 defer, 那么会单独生成相关的结束代码块。获取函数的返回值,并结束当前调用。// Calls the function n using the specified call type. // Returns the address of the return value (or nil if none). func (s *state) call(n *Node, k callKind) *ssa.Value { var call *ssa.Value if k == callDeferStack { // 在栈上初始化 defer 结构体 call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferprocStack, s.mem()) ... } else { ... switch { case k == callDefer: call = s.newValue1A(ssa.OpStaticCall, types.TypeMem, deferproc, s.mem()) ... } call.AuxInt = stksize } s.vars[&memVar] = call ... }deferproc 函数runtime.deferproc 的功能时注册延迟函数,会为 defer 创建一个新的 runtime._defer的结构体、设置它的函数指针 fn、程序计数器 pc 和 栈制作 sp 并将相关的函数参数拷贝到相邻的内存空间。deferproc 函数有两个参数, 第一个是被注册的 defer 函数的参数返回值占多少字节。第二个参数是一个 runtime.funcval 结构体的指针runtime.newdefer通过 runtime.mallocgc 在堆上创建一个新的结构体,并添加到link字段上形成链表。最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn 的递归调用。func deferproc(siz int32, fn *funcval) { // 这里的g就是gorouutine,详看golang的调度模型 // 只有用户使用的goroutine可以应用defer if getg().m.curg != getg() { throw("defer on system stack") } sp := getcallersp() // 调用deferproc之前的rsp寄存器的值 argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() // deferproc函数的返回地址,也就是执行完defer后应当跳回哪段代码上 d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.fn = fn d.pc = callerpc d.sp = sp // 对defer函数参数进行处理 switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() }先通过 newdefer 获取一个 defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的,后调用的 defer 会优先执行。newdefer追加新等延迟调用deferreturn 函数runtime.deferreturn 是触发延迟函数链表的执行,会从 Goroutine 的 _defer 链表中取出最前面的 runtime._defer 并调用 runtime.jmpdefer 传入需要执行的函数和参数。runtime.jmpdefer 是一个用汇编语言实现的运行时函数,它的主要工作是跳转到 defer 所在的代码段并在执行结束之后跳转回 runtime.deferreturn。runtime.deferreturn 会多次判断当前 Goroutine 的 _defer 链表中是否有未执行的结构体,该函数只有在所有延迟函数都执行后才会返回最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn 的递归调用。func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { // 结束条件1,没有defer函数了,也就是所有defer函数都执行完成了 // 还记得defer的链式结构吗,其实就是一个递归函数不断调用 // 为nil的话就代表这条链遍历完成了 return } sp := getcallersp() if d.sp != sp { // 结束条件2,如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回 // 因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如: // a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数 return } switch d.siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) default: memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) } fn := d.fn d.fn = nil gp._defer = d.link freedefer(d) jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }包含如下几个步骤:判断执行条件参数拷贝释放_defer对象执行函数值得一提的是,defer具有即时传值的特点,defer也同样满足闭包和匿名函数的特性。以代码为例子,讲解 defer 注册和执行流程// 源程序 func A1(a int) { fmt.Println(a) } func A() { a, b := 1, 2 defer A1(a) a = a + b fmt.Println(a, b) } //函数A编译后的伪指令 func A() { a, b := 1, 2 runtime.deferproc(8, A1,1) // siz=8, A1=延迟函数入口, 1=A1函数入参 a = a + b fmt.Println(a, b)//3,2 runtime.deferreturn()//执行defer链表 return }函数A定义局部变量a=1,b=2,存储在A函数的栈中deferproc函数注册defer函数A1时,func deferproc(siz int32, fn *funcval)siz:A1没有返回值,64位下一个整型参数占用8字节。fn:A1函数入口地址,addr1deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。在堆上分配存储空间,并存放_defer结构体A1的参数加返回值共占8字节defer函数尚未执行,所以started=falsesp就是调用者A的栈指针pc就是deferproc函数的返回地址return addr被注册的function value为A1defer结构体后面的8字节用来保存传递给A1的参数。然后这个_defer结构体就被添加到defer链表头,deferproc注册结束。频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲_defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。deferreturn执行defer链表:从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输入参数值a=1。
为什么需要response.Body.Close()主要是为了避免内存泄漏的问题, 如果 response 不关闭,会导致内存泄漏。关闭 http 响应当你使用标准http库发起请求时,你得到一个http的响应变量。如果你不读取响应主体,你依旧需要关闭它。注意对于空的响应你也一定要这么做。对于新的Go开发者而言,这个很容易就会忘掉。先看段代码:package main import ( "fmt" "net/http" "io/ioutil" ) func main() { resp, err := http.Get("https://api.ipify.org?format=json") defer resp.Body.Close()//not ok if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) }上面的代码有没有问题呢?假设上面的代码请求失败了, resp 肯能会是 nil,这样执行就会出现一个 runtime panic, 因为 defer resp.Body.Close()//not 这个 resp 是个nil ,肯定就 panic 了。那怎么办?通过在http响应错误处理中添加一个关闭non-nil响应主体的的调用来修复这个问题。另一个方法是使用一个defer调用来关闭所有失败和成功的请求的响应主体。import ( "fmt" "net/http" "io/ioutil" ) func main() { resp, err := http.Get("https://api.ipify.org?format=json") if resp != nil { defer resp.Body.Close() } if err != nil { fmt.Println(err) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { fmt.Println(err) return } fmt.Println(string(body)) }
前言不知道是得罪了哪位企业主,或者哪位企业主想购买我的公众号,然后被我拒绝,举报说公众号名称“程序员开发者社区“ 与个人公众号定位不符合。企鹅也认为是名称有点误解, 于是我改个更高调的名字,和所有程序员开发者共勉,正式改名为“程序员财富自由之路”!!!!sync.Pool 使用场景保存和复用临时对象,减少内存分配,降低 GC 压力例子type Student struct { Name string Age int32 Remark [1024]byte } var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25}) func unmarsh() { stu := &Student{} json.Unmarshal(buf, stu) }json 反序列化在文本解析和网络通信过程中十分常见,当程序并发很高时,短时间内需要创建大量的临时变量,,这些对象分配在堆上,会给 GC 造成很大压力,严重影响程序性能。sync.Pool 是可伸缩的,同时也是并发安全的,大小受限于内存大小。sync.Pool 用于存储那些被分配了但是没有被使用,但是未来可能被使用的值。这样可以不用再次分配内存,提高效率。sync.Pool 是大小可伸缩的,高负载时会动态扩容,存放在池中对象不活跃会被自动清理。如何使用声明对象池只要实现 New 函数即可,对象池中没有对象,那么会调用 New 函数创建var studentPool = sync.Pool{ New: func() interface{} { return new(Student) }, }Get& Putstu := studentPool.Get().(*Student) json.Unmarshal(buf, stu) studentPool.Put(stu)Get() 用于从对象池中获取对象,因为返回值时 interface{} 因此需要值类型转换Put() 则是在对象使用完之后放回对象池struct 性能测试package sync import ( "encoding/json" "sync" "testing" ) type Student struct { Name string Age int32 Remark [1024]byte } var studentPool = sync.Pool{New: func() interface{} { return new(Student) }} var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25}) func BenchmarkUnmarshal(b *testing.B) { for n := 0; n < b.N; n++ { stu := &Student{} json.Unmarshal(buf, stu) } } func BenchmarkUnmarshalWithPool(b *testing.B) { for n := 0; n < b.N; n++ { stu := studentPool.Get().(*Student) json.Unmarshal(buf, stu) studentPool.Put(stu) } }执行命令go test -bench . -benchmem执行结果如下:goos: darwin goarch: amd64 pkg: code.byted.org/wangmingming.hit/GoProject/main/gobase/sync cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkUnmarshal-12 13280 94006 ns/op 1384 B/op 7 allocs/op BenchmarkUnmarshalWithPool-12 12662 95211 ns/op 232 B/op 6 allocs/op PASSBenchmarkUnmarshal 每个循环用了 94006 纳秒,结果项含义BenchmarkUnmarshal-12BenchmarkUnmarshal 是测试的函数名 -12 表示GOMAXPROCS(线程数)的值为1213280表示一共执行了13280次,即b.N的值94006.0 ns/op表示平均每次操作花费了94006.0纳秒1384/op表示每次操作申请了1384 Byte的内存申请7 allocs/op表示每次操作申请了7次内存可以看到 使用 sync.Pool 后,内存占用仅为未使用的 232/1384.
sync.Pool 使用场景保存和复用临时对象,减少内存分配,降低 GC 压力例子type Student struct { Name string Age int32 Remark [1024]byte } var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25}) func unmarsh() { stu := &Student{} json.Unmarshal(buf, stu) }json 反序列化在文本解析和网络通信过程中十分常见,当程序并发很高时,短时间内需要创建大量的临时变量,,这些对象分配在堆上,会给 GC 造成很大压力,严重影响程序性能。sync.Pool 是可伸缩的,同时也是并发安全的,大小受限于内存大小。sync.Pool 用于存储那些被分配了但是没有被使用,但是未来可能被使用的值。这样可以不用再次分配内存,提高效率。sync.Pool 是大小可伸缩的,高负载时会动态扩容,存放在池中对象不活跃会被自动清理。如何使用声明对象池只要实现 New 函数即可,对象池中没有对象,那么会调用 New 函数创建var studentPool = sync.Pool{ New: func() interface{} { return new(Student) }, }Get& Putstu := studentPool.Get().(*Student) json.Unmarshal(buf, stu) studentPool.Put(stu)Get() 用于从对象池中获取对象,因为返回值时 interface{} 因此需要值类型转换Put() 则是在对象使用完之后放回对象池struct 性能测试package sync import ( "encoding/json" "sync" "testing" ) type Student struct { Name string Age int32 Remark [1024]byte } var studentPool = sync.Pool{New: func() interface{} { return new(Student) }} var buf, _ = json.Marshal(Student{Name: "Geektutu", Age: 25}) func BenchmarkUnmarshal(b *testing.B) { for n := 0; n < b.N; n++ { stu := &Student{} json.Unmarshal(buf, stu) } } func BenchmarkUnmarshalWithPool(b *testing.B) { for n := 0; n < b.N; n++ { stu := studentPool.Get().(*Student) json.Unmarshal(buf, stu) studentPool.Put(stu) } }执行命令go test -bench . -benchmem执行结果如下:goos: darwin goarch: amd64 pkg: code.byted.org/wangmingming.hit/GoProject/main/gobase/sync cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkUnmarshal-12 13280 94006 ns/op 1384 B/op 7 allocs/op BenchmarkUnmarshalWithPool-12 12662 95211 ns/op 232 B/op 6 allocs/op PASSBenchmarkUnmarshal 每个循环用了 94006 纳秒,结果项含义BenchmarkUnmarshal-12BenchmarkUnmarshal 是测试的函数名 -12 表示GOMAXPROCS(线程数)的值为1213280表示一共执行了13280次,即b.N的值94006.0 ns/op表示平均每次操作花费了94006.0纳秒1384/op表示每次操作申请了1384 Byte的内存申请7 allocs/op表示每次操作申请了7次内存可以看到 使用 sync.Pool 后,内存占用仅为未使用的 232/1384.
设计模式观察者模式观察者模式,也被称为发布订阅模式(Publish-Subscribe Design Pattern)Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.翻译中文:在对象之间定义一个一对多的依赖,当一个对象状态改变的时候,所有依赖的对象都会自动收到通知。观察者模式的应用场景观察者模式需要三个条件:观察者 被观察,话题订阅实际场景可以是:公众号推送消息,独立的微信号关注多个公众号 ,每次总能收到公众号发布的更新内容,订阅号也会将公众号置顶标红提醒邮件订阅RSS Feeds代码package observer import "fmt" type Subject struct { observers []Observer context string } func NewSubject() *Subject { return &Subject{ observers: make([]Observer, 0), } } func (s *Subject) Attach(o Observer) { s.observers = append(s.observers, o) } func (s *Subject) notify() { for _, o := range s.observers { o.Update(s) } } func (s *Subject) UpdateContext(context string) { s.context = context s.notify() } type Observer interface { Update(*Subject) } type Reader struct { name string } func NewReader(name string) *Reader { return &Reader{ name: name, } } func (r *Reader) Update(s *Subject) { fmt.Printf("%s receive %s\n", r.name, s.context) }测试代码package observer func ExampleObserver() { subject := NewSubject() reader1 := NewReader("reader1") reader2 := NewReader("reader2") reader3 := NewReader("reader3") subject.Attach(reader1) subject.Attach(reader2) subject.Attach(reader3) subject.UpdateContext("observer mode") // Output: // reader1 receive observer mode // reader2 receive observer mode // reader3 receive observer mode }
设计模式策略模式策略模式定义一组算法类,将每个算法分别封装起来,让他们可以相互替换,策略模式可以使得算法独立于客户端,策略模式用来解耦策略的定义,创建,使用。一般来说策略模式也是包含 定义,创建,和使用 三个部分。策略模式和工厂模式有点类似,只是多了策略这一部分,运用场景策略模式最常用的场景是,利用它来避免冗长的if-else 或者 switch 分支判断它的作用不止如此,可以跟模板模式一样,提供框架的扩展点。策略模式的主要作用是解耦策略的定义,创建,使用,控制代码的复杂度代码package strategy import "fmt" type Payment struct { context *PaymentContext strategy PaymentStrategy } type PaymentContext struct { Name, CardID string Money int } func NewPayment(name, cardid string, money int, strategy PaymentStrategy) *Payment { return &Payment{ context: &PaymentContext{ Name: name, CardID: cardid, Money: money, }, strategy: strategy, } } func (p *Payment) Pay() { p.strategy.Pay(p.context) } type PaymentStrategy interface { Pay(*PaymentContext) } type Cash struct{} func (*Cash) Pay(ctx *PaymentContext) { fmt.Printf("Pay $%d to %s by cash", ctx.Money, ctx.Name) } type Bank struct{} func (*Bank) Pay(ctx *PaymentContext) { fmt.Printf("Pay $%d to %s by bank account %s", ctx.Money, ctx.Name, ctx.CardID) }测试package strategy func ExamplePayByCash() { payment := NewPayment("Ada", "", 123, &Cash{}) payment.Pay() // Output: // Pay $123 to Ada by cash } func ExamplePayByBank() { payment := NewPayment("Bob", "0002", 888, &Bank{}) payment.Pay() // Output: // Pay $888 to Bob by bank account 0002 }
设计模式代理模式代理模式是在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这个是跟装饰器模式最大的区别。一般情况下,我们让代理类和原始类实现相同的接口,但是如果原始类并没有定义接口,并且原始类代码并不是我们开发维护的,这种情况下,让代理类继承原始类方法来实现代理。简单的说,实现代理有两个方式:代理类和原始类实现相同的接口代理类继承原始类应用场景代理模式常用在业务系统开发一些非功能的需求,比如,监控,日志,统计,鉴权,限流,事务,幂等校验等。通过代理的方式,将附加功能和业务功能解耦,放到代理类统一处理,程序员只需要关注业务本身,除此之外,代理模式可以用在 RPC 和缓存等场景中。代码proxy.gopackage proxy type Subject interface { Do() string } type RealSubject struct{} func (RealSubject) Do() string { return "real" } type Proxy struct { real RealSubject } func (p Proxy) Do() string { var res string // 在调用真实对象之前的工作,检查缓存,判断权限,实例化真实对象等。。 res += "pre:" // 调用真实对象 res += p.real.Do() // 调用之后的操作,如缓存结果,对结果进行处理等。。 res += ":after" return res }proxy_test.gopackage proxy import "testing" func TestProxy(t *testing.T) { var sub Subject sub = &Proxy{} res := sub.Do() if res != "pre:real:after" { t.Fail() } }运行结果=== RUN TestProxy --- PASS: TestProxy (0.00s) PASS Process finished with exit code 0
前言Drools是java语言的规则引擎,本文是针对go语言的规则引擎框架先说场景:以一个电商运维场景为例,我们需要对用户注册年限p1、购买金额p2、地域p3等条件给用户进行发券,基于条件进行任意组合成不同规则。比如:规则1 :p1 > 2 && p2 > 10 000 & p3 in (‘beijng’,’shanghai’) 大于2年的老用户,并且购买金额大于10000的北京或上海用户。规则2:p1<1 小于1年的用户为了解决这个问题,引入规则引擎, 从 if... else ...中解放出来。。Go 规则引擎先说结论:比较了govaluate、goengine、gorule,最终使用govaluate。相比 gorule、goengine,govaluate除了支持in操作、还支持正则表达式,而且表达式也不需要转换成DRL。支持string类型的 ==操作支持in操作支持计算逻辑表达式和算数表达式支持正则Go 规则引擎对比框架功能基准测试govaluate https://github.com/Knetic/govaluate Star: 1.4 k1、直接使用表达式。不需要转IDL 2、支持算数表达式和逻辑表达式。3、支持string的判断。4、支持in 操作。1 in (1,2,3)字符串 in 。‘code1’ in (‘cod1′,’code2’) 5、支持正则测试3个逻辑表达式条件,如下:每次执行op需要15usgengie(B站开源) https://github.com/rencalo770/gengine Star: 193规则表达式类似于一个DRL。规则脚本具有if..else等语法,后续支持扩展灵活。grue https://github.com/hyperjumptech/grule-rule-engine/ star:525规则表达式需要生成生成一个 DRL。https://github.com/dop251/goja star:1.8kgo实现执行js脚本(类似于java 执行groovy ) 这里不关注表达式,只是通过这引擎可以执行js脚本代码。goja、gengine、grule都是基于脚本的,只是gojar是支持js,grule和gengine是自定义的语法脚本。govaluatefunc TestGoValueate() { // 支持多个逻辑表达式 expr, err := govaluate.NewEvaluableExpression("(10 > 0) && (2.1 == 2.1) && 'service is ok' == 'service is ok'" + " && 1 in (1,2) && 'code1' in ('code3','code2',1)") if err != nil { log.Fatal("syntax error:", err) } result, err := expr.Evaluate(nil) if err != nil { log.Fatal("evaluate error:", err) } fmt.Println(result) // 逻辑表达式包含变量 expression, err := govaluate.NewEvaluableExpression("http_response_body == 'service is ok'") parameters := make(map[string]interface{}, 8) parameters["http_response_body"] = "service is ok" res, _ := expression.Evaluate(parameters) fmt.Println(res) // 算数表达式包含变量 expression1, _ := govaluate.NewEvaluableExpression("requests_made * requests_succeeded / 100") parameters1 := make(map[string]interface{}, 8) parameters1["requests_made"] = 100 parameters1["requests_succeeded"] = 80 result1, _ := expression1.Evaluate(parameters1) fmt.Println(result1) }基准测试func BenchmarkNewEvaluableExpression(b *testing.B) { for i := 0; i < b.N; i++ { _, err := govaluate.NewEvaluableExpression("(10 > 0) && (100 > 20) && 'code1' in ('code3','code2',1)") if err != nil { log.Fatal("syntax error:", err) } } } func BenchmarkEvaluate(b *testing.B) { parameters1 := make(map[string]interface{}, 8) parameters1["gmv"] = 100 parameters1["customerId"] = "80" parameters1["stayLength"] = 20 for i := 0; i < b.N; i++ { _, err := govaluate.NewEvaluableExpression("(gmv > 0) && (stayLength > 20) && customerId in ('80','code2','code3')") if err != nil { log.Fatal("syntax error:", err) } } }在 测试 go 文件目录下执行go test -bench=. ./或者go test -bench=. -benchmem测试结果goos: darwin goarch: amd64 cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkNewEvaluableExpression-12 78542 14692 ns/op 8592 B/op 139 allocs/op BenchmarkEvaluate-12 77352 14884 ns/op
默认关闭事务GORM 默认的数据更新、创建都在事务中,如无必要,可以关闭默认的事务,获得更大的性能提升, 事务的全局性或者临时关闭,即使在关闭默认事务,仍然可以通过方法 Begin, Transactions 方法开启事务。事务模板// 开始事务 tx := db.Begin() // 在事务中做一些数据库操作(从这一点使用'tx',而不是'db') tx.Create(...) // ... // 发生错误时回滚事务 tx.Rollback() // 或提交事务 tx.Commit()具体例子func CreateAnimals(db *gorm.DB) err { tx := db.Begin() // 注意,一旦你在一个事务中,使用tx作为数据库句柄 if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil { tx.Rollback() return err } if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil { tx.Rollback() return err } tx.Commit() return nil }Prepared Statement 加速Prepared Statement 加速 可以大幅度提升所有的 SQL 执行性能, GORM 支持自动的 Prepared Statement 缓冲,启用后,由 Gorm 生成的 SQL 或者 RAW SQL 都会进行预处理并缓存,Prepare Statement 可与数据库事务协同工作。临时性开启// 临时性开启,后续该 tx 的 SQL 执行都会使用 Prepared Statement 模式 tx := db.Session(&Session{PrepareStmt: true}) tx.First(&user, 1) tx.Find(&users) tx.Model(&user).Update("Age", 18)具体例子// 会将 SELECT * FROM `users` 缓存,建立 Prepared Statement db.Find(&user) tx1 := dbProxy.Session(&Session{PreparedStmt: true}) // 会将 SELECT * FROM `users` WHERE id = ? 缓存,生成 Prepared Statement tx1.First(&user, 1) // 会使用前面已经缓存的 SELECT * FROM `users` tx1.Find(&users) // 会建立 UPDATE users SET age = ? 的 Prepared Statement tx1.Model(&user).Update("Age", 18)全局模式// 全局模式,所有的 DB 操作都会进行 Prepared Statement 缓存 dbProxy, err := gorm.POpenWithConfig("mysql", "XXXX_DSN", gorm.Config{ PrepareStmt: true, })具体例子db, err := gorm.Open(..., gorm.Config{ PrepareStmt: true, }) // 会将 SELECT * FROM `users` 缓存,建立 Prepared Statement db.Find(&user)嵌套事务问题GORM 提供了嵌套事务的支持,通过 save point, rollback saved point 实现,例如:DB.Transaction(func(tx *gorm.DB) error { tx.Create(&user1) tx.Transaction(func(tx2 *gorm.DB) error { tx.Create(&user2) return errors.New("rollback user2") // rollback user2 }) tx.Transaction(func(tx2 *gorm.DB) error { tx.Create(&user3) return nil }) return nil // commit user1 and user3 })如果最外侧的事务 rollback 后,所有事务将会被rollbackSELECT ... FOR UPDATEselect ...for update 支持DB.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users) // SELECT * FROM `users` FOR UPDATE多个字段 in 查询DB.Where("(body, subject) IN ?", [][]interface{}{ {"a", 1}, {"b", 2}, {"c", 3}, }).Find(&contents) 产生 SQL: select * from contents where (body,subject) in (('a', 1), ('b',2), ('c',3));字段多重权限问题 (只读/写/更新/创建/忽略)GORM v2 版本中,加入了对字段的支持, 用来避免对一些数据进行误操作,权限级别一共分为:忽略, 只读,只更新,只创建 等:type User struct { Name string `gorm:"<-:create"` // 允许读和创建 Name string `gorm:"<-:update"` // 允许读和更新 Name string `gorm:"<-"` // 允许读和写(创建和更新) Name string `gorm:"<-:false"` // 允许读,禁止写 Name string `gorm:"->"` // 只读(除非有自定义配置,否则禁止写) Name string `gorm:"->;<-:create"` // 允许读和写 Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读) Name string `gorm:"-"` // 读写操作均会忽略该字段 }Timeout 参数timeout Timeout for establishing connections, aka dia timeoutreadTime TCP/IO read timeoutwriteTime TCP IO write timeout如果要 SQL 执行超时关闭, 可以使用 Context.WithTimeOut查询到数据映射到 map[string]interface{}gorm v2 当查询数据到 map 时, 需要指定 Model 方法,或者Table 方法以指定查询的表, map 类型只支持map[string]interface{}, map 类型也支持 slice, 例如 []map[string]interface{}{}var results map[string]interface{}{} DB.Table("users").Find(&results) DB.Model(&User{}).Find(&results) var results []map[string]interface{}{} DB.Model("users").Find(&results)批量查询处理数据Gorm v2 可以使用 FIndInBatch 对大量数据进行批量查询批量处理, 但是要注意的是,查询不是一个事务,如果要做成食物,需要在外面写事务。// 设定批量数量为 100,每次查询 100 条数据,处理完毕后处理下 100 条数据 result := DB.Where("processed = ?", false).FindInBatches(&results, 100, func(tx *gorm.DB, batch int) error { for _, result := range results { // 批量处理数据 } // 批量更新数据,在使用 Save 处理批量数据时,会使用 Insert OnConflict DoNothing 模式 tx.Save(&results) // 本批次包含数据量,如果本批次只有50条数据返回,则为50 tx.RowsAffected batch // 这是第几批次的数据 // 如果返回 error ,后续查询处理操作将停止 return nil }, ) result.Error // 返回处理完所有批量数据时有无错误发生 result.RowsAffected // 返回所有批次被处理的数据总量更新多条记录// 根据 struct 更新 db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18}) // UPDATE users SET name='hello', age=18 WHERE role = 'admin; // 根据 map 更新 db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18}) // UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);更新选定字段如果您想要在更新时选定、忽略某些字段,您可以使用 Select、Omit// 使用 Map 进行 Select // User's ID is `111`: db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) // UPDATE users SET name='hello' WHERE id=111; db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false}) // UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111; // 使用 Struct 进行 Select(会 select 零值的字段) db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0}) // UPDATE users SET name='new_name', age=0 WHERE id=111; // Select 所有字段(查询包括零值字段的所有字段) db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0}) // Select 除 Role 外的所有字段(包括零值字段的所有字段) db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})更新 Hook对于更新操作,GORM 支持 BeforeSave、BeforeUpdate、AfterSave、AfterUpdate 钩子,这些方法将在更新记录时被调用,详情请参阅 钩子func (u *User) BeforeUpdate(tx *gorm.DB) (err error) { if u.Role == "admin" { return errors.New("admin user not allowed to update") } return }更新记录数获取受更新影响的行数// 通过 `RowsAffected` 得到更新的记录数 result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18}) // UPDATE users SET name='hello', age=18 WHERE role = 'admin; result.RowsAffected // 更新的记录数 result.Error // 更新的错误检查字段是否有变更GORM 提供了 Changed 方法,它可以被用在 Before Update Hook 里,它会返回字段是否有变更的布尔值 Changed 方法只能与 Update、Updates 方法一起使用,并且它只是检查 Model 对象字段的值与 Update、Updates 的值是否相等,如果值有变更,且字段没有被忽略,则返回 truefunc (u *User) BeforeUpdate(tx *gorm.DB) (err error) { // 如果 Role 字段有变更 if tx.Statement.Changed("Role") { return errors.New("role not allowed to change") } if tx.Statement.Changed("Name", "Admin") { // 如果 Name 或 Role 字段有变更 tx.Statement.SetColumn("Age", 18) } // 如果任意字段有变更 if tx.Statement.Changed() { tx.Statement.SetColumn("RefreshedAt", time.Now()) } return nil } db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"}) // Changed("Name") => true db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"}) // Changed("Name") => false, 因为 `Name` 没有变更 db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{ "name": "jinzhu2", "admin": false, }) // Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新 db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"}) // Changed("Name") => true db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"}) // Changed("Name") => false, 因为 `Name` 没有变更 db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"}) // Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新在更新时修改这个场景常用于数据加密,解密 若要在 Before 钩子中改变要更新的值,如果它是一个完整的更新,可以使用 Save;否则,应该使用 SetColumn ,例如:func (user *User) BeforeSave(tx *gorm.DB) (err error) { if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil { tx.Statement.SetColumn("EncryptedPassword", pw) } if tx.Statement.Changed("Code") { s.Age += 20 tx.Statement.SetColumn("Age", s.Age+20) } } db.Model(&user).Update("Name", "jinzhu")更新数据时多零值问题在更新数据时,如果使用了 struct 来更新数据,默认只会更新非零值字段,如果使用map更新数据,则会更新全部字段,在使用 struct 更新时,也可以使用 Select 方法来选择想要更新的字段,在这种情况下,零值/非零值字段都会更新,例如// UPDATE users SET name='new_name', age=0 WHERE id=111; DB.Model(&result).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0}) // UPDATE users SET name='hello', updated_at = '2013-11-17 21:34' WHERE id = 1 db.Model(&user).Updates(User{Name: "hello", Age: 0, Active: false})Smart Select 功能如果使用一个较小的 struct 查询时,将会自动添加较小 struct 的字段到查询的 Select 当中,来减少需查询的字段数量,因此对于 API 来说,可以定义一个较小对象来来减少不必要的字段查询,例如:type User struct { ID uint Name string Age int Gender string // hundreds of fields } type APIUser struct { ID uint Name string } // 在查询时自动选择 id, name 字段,并忽略其它的字段 db.Model(&User{}).Limit(10).Find(&APIUser{}) // SELECT `id`, `name` FROM `users` LIMIT 10JSON 特殊字段支持GORM对一些特殊字段进行了封装支持,可以参考data_typetype UserWithJSON struct { gorm.Model Name string Attributes datatypes.JSON } DB.Create(&User{ Name: "json-1", Attributes: datatypes.JSON([]byte(`{"name": "jinzhu", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)), } db.Find(&user, datatypes.JSONQuery("attributes").HasKey("role")) db.Find(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))
什么是设计模式设计模式是针对软件开发过程中遇到的一些设计问题,总结出来的一套解决方案或者设计思路。https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA)创建型模式简单工厂模式(Simple Factory)工厂方法模式(Factory Method)抽象工厂模式(Abstract Factory)创建者模式(Builder)原型模式(Prototype)单例模式(Singleton)结构型模式外观模式(Facade)适配器模式(Adapter)代理模式(Proxy)组合模式(Composite)享元模式(Flyweight)装饰模式(Decorator)桥模式(Bridge)行为型模式中介者模式(Mediator)观察者模式(Observer)命令模式(Command)迭代器模式(Iterator)模板方法模式(Template Method)策略模式(Strategy)状态模式(State)备忘录模式(Memento)解释器模式(Interpreter)职责链模式(Chain of Responsibility)访问者模式(Visitor)面向对象面向对象编程是一种编程方式或者编程风格, 以类或者对象作为组织代码的基本单元。包含封装,多态,继承,抽象等4个基本特性。面向对象语言面向对象语言是支持类和对象的语法机制,并有现在的语法机制,能够方便的实现面向对象的4大特征(封装、抽象、继承、多态)的编程语言。封装特性存在的意义,一方面是保护数据不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用性。封装封装也叫做数据隐藏和数据访问保护,通过暴露有限的访问接口,授权外部仅能通过类提供的方式来访问内部信息和数据。它需要编程语言提供权限访问控制语法来支持, 比如 Java 中的 private 、protected 、public等关键字。抽象封装说的是如何隐藏信息和保护数据,抽象说的是如何隐藏方法的具体实现,抽象可以通过接口或者抽象类来实现,但也并不需要特定的语法来支持。抽象存在的意义,一方面是提高代码的可扩展性和维护性, 修改方法时不需要改变定义,减少代码的改动范围,另外一方面也是处理复杂问题的有效手段, 能够有效过滤掉不需要关注的信息。继承继承用来表示 类之间 is-a 的关系,分为两种模式, 单继承和多继承,单继承表示一个子类只能继承一个父类,多继承表示一个子类可以继承多个父类,为了实现继承这个特性,编程语言需要提供特殊的语法机制来支持。继承主要是用来解决代码复用的问题。多态子类可以替代父类,实际代码运行过程中,调用子类的方法实现。多态这个特性需要编程语言的特殊语法机制来实现,比如继承、接口类,duck-typing, 多态可以提高代码的扩展性和复用性,有很多设计模式,设计原则,编程技巧的代码实现基础。面向对象和面向过程对于大规模复杂程序的开发,程序的处理并非单一的一条主线,是错综复杂的网状结构,面向对象比面向过程编程,更能够应对这种复杂类型的程序开发。面向对象编程相对于面向过程编程,具有丰富的特性(抽象,继承,封装,多态),利用这些特性写出的代码更加容易扩展,易复用,易维护。面向对象编程比面向过程编程更加人性化,更加智能。组合与继承继承是面向对象的四大特性之一,表示类之间的 is-a 的关系(Apple is Fruit ) 可以解决代码复用的问题,但是继承如果层次过深,过复杂,也会影响代码的可维护性。组合表示的是类的 has-a 的关系,比如 House has BathRoom 。类的继承层次如果很深,继承的关系会越来越复杂,而且层次很深很复杂的继承关系,会导致代码可读性变差,子类的实现依赖父类的实现,两者高度耦和,一旦父类代码修改,会影响子类的逻辑。GO 是面向对象语言么?Go 围绕 struct ,提供了私有属性、method、interface、struct 嵌套能力,可以用更轻量的方式实现面向对象(封装,继承,多态,抽象)Go 核心理论基于接口编程而非实现编程接口是一组协议或者约定,是功能提供者提供给使用者的一个功能列表,接口在不同的场景下有不同的解读,好的代码设计,不仅能够应对当下需求,而且在将来需求发生变化的时候,仍然可以在不破坏原有代码设计的情况下灵活应对。设计原则如何理解单一职责原则 (SRP)一个类或者一个模块只负责完成一个职责或者功能,不要设计大而全的类, 要设计粒度小,功能单一的类, 单一职责是为了实现代码高内聚,低耦合,提高代码的复用性,可读性,可维护性。如何判断类的职责是否足够单一不同应用的场景,不同阶段的需求背景,不同的业务层面,对通一个类的职责是否单一,可能会有不同的判定结果。如果出现以下情况,表示不满足单一职责原则:类中的代码行数,函数或者属性过多类依赖其他类过多, 或者依赖类的其他类过多私有方法过多比较难给类取一个合适的名字类中大量方法都集中在操作类的某些属性上代码中存在大量注释开闭原则如何理解 “对扩展开放,对修改关闭”添加一个新功能,应该是通过现有共扩展代码(新增模块,类,方法,属性等), 而非修改已有代码的方式来完成。第一点,开闭原则并不少说完全杜绝修改,而是以最小的修改代码的代价完成新功能的开发。第二点,同样的代码修改,在粗粒度下,可以被认为是修改,在细粒度下,被认为是“扩展”。我们对一个类添加新的方法。添加方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”如何做到修改关闭,扩展开放?时刻具备扩展意识,抽象意识,封装意识。编码时,要多花时间去思考,代码未来可能哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来在需求变更时,在不调整代码结构的基础上,做到最小代码的修改,将新代码灵活的放到扩展点上。最常用的提高代码扩展的方法有:多态,依赖注入, 基于接口而非实现编程,与大部分设计模式。里式替换里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。子类对象能给替换程序中父类出现的任何对方,并且保证程序的行为逻辑及正确性不被破坏。多态是面向对象编程等一大特性,也是面向对象编程语言的一种语法,它是一种代码实现的思路,里式转换原则设计中,是用来指导继承关系中子类如何设计,子类的设计保证在替换父类时,不改变原有逻辑和程序的正确性。接口隔离接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该被强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。理解接口隔离重要是理解其中接口两字。接口理解一组接口集合,可以是某个服务的接口,可以是某个类的接口,如果部分接口被有这使用。接口隔离原则和单一职责原则的区别单一职责是针对的是类,模块,接口的设计,接口隔离原则相对单一原则更侧重于接口的设计。另外思考角度也不一样。接口隔离原则提供了一种判断接口是否单一的标准:通过调用者如何使用接口来判断,如果调用者使用部分接口或接口的部分功能,那接口的设计就不够单一依赖倒置原则控制反转实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架。依赖注入依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用。依赖注入也是实现组合最简单的方式type A struct {} func NewA() A { return A{} } type B struct { a A } func NewB(a A) B { return B{a:a} } func main() { a := NewA() // 依赖注入 b := NewB(a) }依赖反转原则也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。高层模块不依赖低层模块,它们共同依赖同一个抽象。抽象不要依赖具体实现细节,具体实现细节依赖抽象。KISS 原则Keep It Simple and Stupid.Keep It Short and Simple.Keep It Simple and Straightforward.其实主要含义是尽量保持简单如何写出 KISS 原则的代码不要使用同事可能不懂的技术来实现代码不要重复造轮子,要善于使用已经有的工具类库不要过度优化DRY 原则DRY 原则(Don’t Repeat Yourself)几乎人尽皆知。你可能会觉得,这条原则非常简单、非常容易应用。只要两段代码长得一样,那就是违反 DRY 原则了。真的是这样吗?答案是否定的。这是很多人对这条原则存在的误解。实际上,重复的代码不一定违反 DRY 原则,而且有些看似不重复的代码也有可能违反 DRY 原则。通常存在三种典型的代码重复情况,它们分别是:实现逻辑重复、功能语义重复和代码执行重复。问题:如何提高代码复用性?减少代码耦合满足单一职责原则模块化业务与非业务逻辑分离通用代码下沉继承、多态、抽象、封装应用模板等设计模式LOD 原则迪米特法则的英文翻译是:Law of Demeter,缩写是 LOD。单从这个名字上来看,我们完全猜不出这个原则讲的是什么。不过,它还有另外一个更加达意的名字,叫作最小知识原则,英文翻译为:The Least Knowledge Principle。迪米特法则法则强调不该有直接依赖关系的类之间,不要有依赖;有依赖关系的类之间,尽量只依赖必要的接口。迪米特法则是希望减少类之间的耦合,让类越独立越好。每个类都应该少了解系统的其他部分。一旦发生变化,需要了解这一变化的类就会比较少。“单一职责原则”、“接口隔离原则”以及“最小知识原则”,都是实现高内聚低耦合的有效指导思想。“最小知识原则”更强调类与类之间的关系。
什么是 OKR ?OKR (Object & Key Results) 是实现目标管理,执行与合作工具。Objectives 是目标,回答的是”我和我的团队要干什么“Key Result 是一系列可衡量的关键结果, 回答的是”我是如何完成团队目标?“KR 是由 O 分解出来的,用于支撑 O 的实现,理想状态 KR 完成时, O就完成了。写 OKR 的过程也是对目标定义,讨论,对齐和理解的过程,会促进对业务的深入思考如何制定 OKR目标O的制定原则自驱动,是自己目标的 Owner, O 是自己提出, 主动评估,思考,明确自己中长期价值和短期目标聚焦高优,写进高优的应该是高优高价值的工作,O数量建议为 3-5个颗粒度适中,O 是某个时间段内的短期目标,不要过细,目标背后要有系统性思考,有挑战,希望每个人的 O 具有一定的挑战性,高目标可以激发一定的潜能,有助于取得优于普通水准的结果。长短期并重,OKR 能明确这项工作的价值,如果是长期工作,要合理拆解 OKR目标 KR 的制定原则相关性, O 是希望实现的目标,KR 是对目标是否实现的判断,问自己 KR 是否对 O 有直接的支撑作用可衡量,KR 是具体的结果和状态,不要模凌两可, 要明确产出而不是动作,用客观可观察的描述。聚焦高优,突出关键的 KR, 不是罗列堆砌,每个 O 对应的 KR 一般3个左右可实现,KR 是对自己的承诺,需要行动起来实现关键结果OKR 例子O:实现创纪录的收入,同时提高盈利能力 KR1:将Q3收入从80000增加到100000 KR2:正式启动方案A,将业务扩展到2个新国家 KR3:毛利率从54%增加到63%O:完成为我们的增长需求筹集的新资金 KR1:电子邮件和电话联系计划与风投和种子基金安排25次会议 KR2:与VC进行10秒钟的后续会议 KR3:结束至少一千万美元的融资前投资
Go 的优点Go like C++内存消耗少执行速度快启动快 Go not like C++程序编译时间短像动态语言一样灵活(runtime, interface, 闭包,反射)内存并发正常deferdefer 执行顺序是先进后出,栈的方式 FIFO ,参数的值在 defer 语句执行就已经确定了for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i) }执行结果:4 3 2 1 0append 不是线程安全的slice 中,如果 a[x] 和 b[y] 指向同一个内存区域,那么存在竞态关系package main import ( "fmt" ) func main() { a := []int{1, 2} b := a[1:] go func() { a[1] = 0 }() fmt.Println(b[0]) }slice.gopackage main import ( "fmt" ) func main() { a := []int{1, 2} b := a[1:] go func() { a[1] = 0 }() fmt.Println(b[0]) }运行命令go run -race slice.go执行结果2 ================== WARNING: DATA RACE Write at 0x00c0000bc018 by goroutine 7: main.main.func1() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:11 +0x47 Previous read at 0x00c0000bc018 by main goroutine: main.main() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:14 +0xb9 Goroutine 7 (running) created at: main.main() /Users/bytedance/go/src/code.byted.org/wangmingming.hit/GoProject/main/slice.go:10 +0xab ================== Found 1 data race(s) exit status 66零值零值和未初始后的值并不相同, 不同类型的零值是什么布尔类型是 false, 整型是0, 字符串是 “”指针,函数,interface 、slice 、channel 和 map 的零值都是 nil结构体的零值是递归生成的,每个成员都是对应的零值使用要注意如下几点:一个为nil的slice,除了不能索引外,其他的操作都是可以的nil的map,我们可以简单把它看成是一个只读的map// 一个为nil的slice,除了不能索引外,其他的操作都是可以的 // Note: 如果这个slice是个指针,不适用这里的规则 var a []int fmt.Printf("len(a):%d, cap(a):%d, a==nil:%v\n", len(a),cap(a), a == nil) //0 0 true for _, v := range a{// 不会panic fmt.Println(v) } aa := a[0:0] // 也不会panic,只要索引都是0 // nil的map,我们可以简单把它看成是一个只读的map var b map[string]string if val, ok := b["notexist"];ok{// 不会panic fmt.Println(val) } for k, v := range b{// 不会panic fmt.Println(k,v) } delete(b, "foo") // 也不会panic fmt.Printf("len(b):%d, b==nil:%v\n", len(b), b == nil) // 0 true值传递Go 语言中所有的传参都是值传递,或者说一个拷贝,传入的数据能不能在函数内被修改,取决于是指针或者含有指针的类型(指针被值传递复制后依然指向同一块地址),什么时候传入的参数会修改会生效,什么时候不会生效。slice类型在 值传递的时候len和cap不会变,所以函数内append没有用:type slice struct { array unsafe.Pointer len int cap int } // badcase func appendMe(s []int){ s = append(s, -1) }map 和 chan 是引用类型,是个指针, 在函数内修改会生效// map实际上是一个 *hmap func makemap(t *maptype, hint int64, h *hmap, bucket unsafe.Pointer) *hmap { //省略无关代码 } // chan实际上是个 *hchan func makechan(t *chantype, size int64) *hchan { //省略无关代码 }结构体传参// 这是一个典型的指针包裹类型 type Person struct { name string age *int } func modify(x Person){ x.name = "modified" *x.age = 66 }这个结构体中 age 是个指针类型,在函数内会被修改复制数据时,使用 copy 比 append 性能更好import ( "crypto/rand" "testing" ) var ( src = make([]byte, 512) dst = make([]byte, 512) ) func genSource() { rand.Read(src) } func BenchmarkCopy(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() genSource() b.StartTimer() copy(dst, src) } } func BenchmarkAppend(b *testing.B) { for n := 0; n < b.N; n++ { b.StopTimer() genSource() b.StartTimer() dst = append(dst, src...) } }dst 作为全局变量防止编译器优化 for-loopuptime;go version;go test -bench=. ./ 11:56:10 up 294 days, 14:58, 3 users, load average: 0.58, 0.52, 0.63 go version go1.14.1 linux/amd64 goos: linux goarch: amd64 pkg: copyvsappend BenchmarkCopy-40 9808320 116 ns/op BenchmarkAppend-40 479055 8740 ns/op PASSGo 语言中为啥没有继承go 没子类型的概念,只能把类型嵌入另外一个类型中,所以没有类型系统。使用伸缩性良好的组合,而不是继承数据和方法不绑定在一起,数据的集合使用 struct, 方法的集合使用 interface ,保持正交接收器是用指针还是值go 接收器可以用指针,也可以传值,传值的时候接收器不会改变。如果以下两种情况,请使用指针:mystruct 很大时,需要拷贝的成本太高方法需要修改 myStructNote:如果对象有可能并发执行方法,指针接收器中可能产生数据竞争,记得加锁func(s * MyStruct)pointerMethod(){ // 指针方法 s.Age = -1 // useful } func(s MyStruct)valueMethod(){ // 值方法 s.Age = -1 // no use }for 循环里的是副本for key, element = range aContainer {...}实际遍历的 aContainer 是原始值的一个副本element 是遍历到的元素原始值的一个副本key 和 Value 整个循环都是同一个变量,每次迭代都生成新变量aContainer和element的拷贝成本。aContainer 数组的时候的拷贝成本比较大,而切片和map的拷贝成本比较小。如果想要缩小拷贝成本,我们有几个建议:遍历大数组时,可以先创建大数组的切片再放在range后面element结构比较大的时候,直接用下标key遍历,舍弃elementmap 的值不可取址map 是哈希表的实现,所以值的地址在哈希表动态调整的时候会可能产生变化,因此,存在着 map 值的地址是没意义的,go 禁止了 map 取址操作,以下类型都不可取址map 元素string 的字节元素常量(有名变量和字面量都不可以)中间结果值(函数调用,显示值转换,各种操作)// 下面这几行编译不通过。 _ = &[3]int{2, 3, 5}[0] //字面量 _ = &map[int]bool{1: true}[1] //字面量 const pi = 3.14 _ = &pi //有名常量 m := map[int]bool{1: true} _ = &m[1] //map的value lt := [3]int{2, 3, 5} _ = &lt[1:1] //切片操作常用的仓库strings有 strings 库,不要重复造轮子,很多人试图再写一遍,没必要字符串前后处理var s = "abaay森z众xbbab" o := fmt.Println o(strings.TrimPrefix(s, "ab")) // aay森z众xbbab o(strings.TrimSuffix(s, "ab")) // abaay森z众xbb o(strings.TrimLeft(s, "ab")) // y森z众xbbab o(strings.TrimRight(s, "ab")) // abaay森z众x o(strings.Trim(s, "ab")) // y森z众x o(strings.TrimFunc(s, func(r rune) bool { return r < 128 // trim all ascii chars })) // 森z众字符串分割与合并// "1 2 3" -> ["1","2","3"] func Fields(s string) []string // 用空白字符分割字符串 // "1|2|3" -> ["1","2","3"] func Split(s, sep string) []string // 用sep分割字符串,sep会被去掉 // ["1","2","3"] -> "1,2,3" func Join(a []string, sep string) string // 将一系列字符串连接为一个字符串,之间用sep来分隔 // Note: // "1||3" -> ["1","","3"]错误处理可以把异常传递下去,并不丢失自己的类型可以保存堆栈信息for range如果每个元素比较大,循环时,使用range 取值的方式遍历,性能比较差package bench import "testing" var X [1 << 15]struct { val int _ [4096]byte } var Result int func BenchmarkRangeIndex(b *testing.B) { var r int for n := 0; n < b.N; n++ { for i := range X { x := &X[i] r += x.val } } Result = r } func BenchmarkRangeValue(b *testing.B) { var r int for n := 0; n < b.N; n++ { for _, x := range X { r += x.val } } Result = r } func BenchmarkFor(b *testing.B) { var r int for n := 0; n < b.N; n++ { for i := 0; i < len(X); i++ { x := &X[i] r += x.val } } Result = r }执行命令go test -bench=. bench_test.go
什么是异常?不按照我们期望执行的都可以称之为异常在Go语言中如何处理异常?一种是程序发生异常时, 将异常信息反馈给使用者一种是程序发生异常时, 立刻退出终止程序继续运行将异常信息反馈给使用者创建方式一: fmt.Errorf("提示的内容")创建方式二: errors.New("提示的内容")注意点: 本质上两个方法底层的实现原理都是一样的package builtin中定义了一个接口type error interface { Error() string }package errors中定义了一个结构体type errorString struct { s string }errorString结构体实现了builtin中定义的接口func (e *errorString) Error() string { return e.s }所以errorString结构体实现了error接口func New(text string) error { return &errorString{text} }异常信息提示func TestThrowError(t *testing.T) { if res, err := div(10, 0); err == nil { fmt.Println(res) } else { fmt.Println(err) } } func div(a int, b int) (res int, err error) { if b == 0 { // 创建异常的两种方式 err = fmt.Errorf("除数不能为0") err = errors.New("除数不能为0") } else { res = a / b } return res, err }运行结果:=== RUN TestThrowError 除数不能为0 --- PASS: TestThrowError (0.00s) PASS异常处理终止程序程序终止的方式:系统自动终止手动终止(企业级应用开发中不常用)格式 :panic("提示信息")func TestExceptionPanic(t *testing.T) { /* 一种是程序发生异常时, 立刻退出终止程序继续运行 终止程序也分为两种: 1.系统自动终止 2.我们手动终止 (企业开发不常用) 格式: panic("提示信息") */ // 系统自动终止 //arr := [3]int{1, 3, 5} //for i := 0; i < 20; i++ { // fmt.Println(arr[i]) //} res := div1(10, 0) fmt.Println(res) } // 除法运算 func div1(a int, b int) (res int) { if b == 0 { //手动终止程序 panic("除数不能为0") } else { res = a / b } return res }异常恢复程序不要随意被终止,只要不是程序不能运行,需要建立让程序保持运行如果程序出现 panic 异常,可以通过 defer 和 recover 实现 panic 异常的捕获,让程序正常运行。注意defer 和 recover 必须要在 panic 之前被定义panic 异常会随着函数的调用栈向外传递 A函数调用了B函数, B函数调用了C函数,如果在C函数中抛出了一个panic异常, 那么这个异常会一层一层的传递到B和A,也就是在B和A函数中也能捕获到这个异常func TestExceptionRecover(t *testing.T) { /* 1.程序不要随意被终止, 只要不是程序不能运行了, 就尽量让改程序继续保持运行 2.在Go语言中如果panic异常, 那么可以通过defer和recover来实现panic异常的捕获, 让程序继续执行 注意点: 1.defer和recover必须在panic抛出异常之前定义 2.panic异常会随着函数的调用栈向外传递 例如: A函数调用了B函数, B函数调用了C函数 如果在C函数中抛出了一个panic异常, 那么这个异常会一层一层的传递到B和A 也就是在B和A函数中也能捕获到这个异常 */ defer func() { if err := recover(); err != nil { fmt.Println("recover 捕获到了异常", err) } }() res := div2(10, 0) fmt.Println(res) } // 除法运算 func div2(a, b int) (res int) { // 在当前函数中捕获 //defer func() { // // defer无论所在的函数是正常结束,还是异常结束都会被执行 // // recover可以捕获panic异常 // if err := recover(); err != nil{ // fmt.Println("recover捕获到了", err) // } //}() if b == 0 { // 手动终止程序 panic("除数不能为0") } else { res = a / b } // 无效 //defer func() { // // defer无论所在的函数是正常结束,还是异常结束都会被执行 // // recover可以捕获panic异常 // if err := recover(); err != nil{ // fmt.Println(err) // } //}() return }运行结果:=== RUN TestExceptionRecover recover 捕获到了异常 除数不能为0 --- PASS: TestExceptionRecover (0.00s) PASS捕获异常注意点同一个函数中,多个 panic 异常,只要第一个会被捕获func TestPanics(t *testing.T) { /* 捕获异常注意点: 1.同一个函数中, 多个panic异常, 只有第一个会被捕获 */ /*defer func() { if err := recover(); err != nil { fmt.Println(err) //异常1 } }() panic("异常1") panic("异常2") panic("异常3") panic("异常4")*/ panicfuc() } func panicfuc() { // 如果有异常写在defer中, 但是defer后面还有其它异常, 那么捕获到的是其它的异常 // 如果其它异常是写在defer前面, 那么和同一个函数中, 多个panic异常, 只有第一个会被捕获 defer func() { if err := recover(); err != nil { fmt.Println(err) // 异常B } }() defer func() { panic("异常B") }() panic("异常C") }运行结果=== RUN TestPanics 异常B --- PASS: TestPanics (0.00s) PASS
GO111MODULE 是啥?GO111MODULE 是个环境变量,可以在使用 Go 或者更改 Go 导入包的方式时候设置。要注意的是,这个变量在不同 Go 版本有不同的语义没有包管理阶段一开始go发布的时候是没有包管理的go get命令会根据路径,把相应的模块获取并保存在$GOPATH/src也没有版本的概念,master 就代表稳定的版本首先,让我们谈谈 GOPATH。当 Go 在 2009 年首次推出时,它并没有随包管理器一起提供。取而代之的是 go get,通过使用它们的导入路径来获取所有源并将其存储在 $GOPATH/src 中。没有版本控制并且『master』分支表示该软件包的稳定版本。Go 1.11 引入了 Go 模块。Go Modules 不使用 GOPATH 存储每个软件包的单个 git checkout,而是存储带有 go.mod 标记版本的标记版本,并跟踪每个软件包的版本。什么时候用 GOPATH, 什么时候用 GOMODULE ?这是个问题,需要通过 GO111MODULE 来解决。Go 1.11 和 1.12 阶段即使项目在您的 GOPATH 中,GO111MODULE = on 仍将强制使用 Go 模块。仍然需要 go.mod 才能正常工作。GO111MODULE = off强制 Go 表现出 GOPATH 方式,即使你的项目不在 GOPATH 目录里。. GO111MODULE = auto 是默认模式。在这种模式下,Go 会表现:当项目路径在 GOPATH 目录外部时, 设置为 GO111MODULE = on 当项目路径位于 GOPATH 内部时,即使存在 go.mod, 设置为 GO111MODULE = off。Go 1.13 阶段在 Go 1.13 下, GO111MODULE 的默认行为 (auto) 语义变了。当存在 go.mod 文件时或处于 GOPATH 外, 其行为均会等同于 GO111MODULE=on。相当于 Go 1.13 下你可以将所有的代码仓库均不存储在 GOPATH 下。当项目目录处于 GOPATH 内,且没有 go.mod 文件存在时其行为会等同于 GO111MODULE=off。Go Modules 的使用说明使用 go get 同样会更新你的 go.modgo get 通常它是用于提供一个安装或下载包的功能。但如果使用了 Go modules,当你在一个有着 go.mod 文件存在的仓库下使用这个命令会将你所下载或安装的包静默记录于 go.mod 文件中。Go Modules 依赖项的存储在哪个目录?使用 Go Modules 时,在 go build 期间使用的包存储在 $GOPATH/pkg/mod 中。在尝试在开发工具中的import时,你可能最终使用的包是 GOPATH 中的版本,而不是编译期间使用的 pkg/mod。当让项目固定指向一个依赖项时,可以使用 vendor 目录解决方法 1: 使用 go mod vendor + go build -mod=vendor。这将强制 go 使用 vendor/files 而不是 $GOPATH/pkg/mod 中的一个。该选项还解决了 开发工具 不能打开包文件的正确版本的问题。解决方法 2: 在 go.mod 末尾添加 replace 行:use replace github.com/maelvls/beers => ../beers
go mod 和 govendor 都是 Go 包管理器,类似 Java 工程的 maven2012年3月 Go 1 发布,此时没有版本的概念2013年 Golang 团队在 FAQ 中提议开发者保证相同 import path 的兼容性,后来成为一纸空文2013年10月 Godep2014年7月 glide2014年 有人提出 external packages 的概念,在项目的目录下增加一个 vendor 目录来存放外部的包2015年8月 Go 1.5 实验性质加入 vendor 机制2015年 有人提出了采用语义化版本的草案2016年2月 Go 1.6 vendor 机制 默认开启2016年5月 Go 团队的 Peter Bourgon 建立委员会,讨论依赖管理工具,也就是后面的 dep2016年8月 Go 1.7: vendor 目录永远启用2017年1月 Go 团队发布 Dep,作为准官方试验2018年8月 Go 1.11发布 Modules 作为官方试验2019年2月 Go 1.12发布 Modules 默认为 auto2019年9月 Go 1.13 版本默认开启 Go Mod 模式有这么个故事故事:失宠的 Vendor 目录Vendor目录是Golang从1.5版本开始引入的,为项目开发提供了一种离线保存第三方依赖包的方法。但是到了Golang 1.11之后,由于引入了Module功能,在运行go build时,优先引用的是Module依赖包的逻辑,所以Vendor目录就被“无视”了,进而可能发生编译错误, moudle 说还是很想他,于是 提供了 go mod vendor 命令用来生成 vendor 目录。这样能避免一些编译问题,依赖可以先从 vendor 目录进行扫描。$ go mod help vendor usage: go mod vendor [-v] Vendor resets the main module's vendor directory to include all packages needed to build and test all the main module's packages. It does not include test code for vendored packages.这句话的意思是:把 go mod init 后下载的相关依 赖包(Gopath 的 pkg) 目录,拷贝到 vendor 目录。vendor 目录方式go vendor 是go 1.5 官方引入管理包依赖的方式基本思路: 将引用的外部包的源代码放在当前工程的vendor目录下面,go 1.6以后编译go代码会优先从vendor目录先寻找依赖包;找不到再从GOPATH 中寻找解决的问题将源码拷贝到当前目录下,这样导包当前工程代码到任意的机器的 ¥GOPATH/src 都可以编译通过,避免项目代码外部依赖过多未解决的问题无法精确的引用 外部包进行版本控制,不能指定引用某个特定版本的外部包,只是在开发时将其拷贝过来,但是一旦外部包升级,vendor 下面的包会跟着升级,而且 vendor 下面没有完整的引用包的版本信息, 对包升级带来了无法评估的风险。什么是 GOROOT 和 GOPATHGOROOT:golang的安装路径,当安装好go之后,默认会安装在/usr/local/go之下。GOROOT的主要作用是标识go的当前安装位置。GOPATH:存放SDK以外的第三方类库;收藏的可复用的代码,包含三个子目录:-- src : 存放项目源码文件 -- pkg : 编译后的包文件 -- bin :编译后生成的可执行文件要解决 vendor 目录 未解决的问题,使用 govendor可以平滑的将现有非 vendor 项目转换成 vendor 项目govendor add inport_out_packagename会生成一个元数据文件,记录工程依赖的外部包,及其版本信息vendor.json提高命令查看整个工程的依赖关系goverdor --list goverdor --list -vgovendorgovendor 是一个基于 vendor 机制实现的 Go 包依赖管理命令行工具。与原生 vendor 无侵入性融合,也支持从其他依赖管理工具迁移,可以很方便的实现同一个包在不同项目中不同版本、以及无相互侵入的开发和管理。在执行 go build 或 go run 命令时,会按照以下顺序去查找包:当前包下的 vendor 目录向上级目录查找,直到找到 src 下的 vendor 目录在 GOROOT 目录下查找在 GOPATH 下面查找依赖包常用命令安装go get -u -v github.com/kardianos/govendor初始化cd xxx govendor init初始化完成后,项目目录中会生成一个vendor文件夹,包含一个vendor.json文件,json文件中包含了项目所依赖的所有包信息{ "comment": "", "ignore": "test", "package": [], "rootPath": "govendor-example" }将已被引用且在 $GOPATH 下的所有包复制到 vendor 目录govendor add +external仅从 $GOPATH 中复制指定包govendor add gopkg.in/yaml.v2列出代码中所有被引用到的包及其状态govendor list运行结果e github.com/gin-contrib/sse e github.com/gin-gonic/gin e github.com/gin-gonic/gin/binding e github.com/gin-gonic/gin/internal/json e github.com/gin-gonic/gin/render e github.com/golang/protobuf/proto e github.com/mattn/go-isatty e github.com/ugorji/go/codec e gopkg.in/go-playground/validator.v8 e gopkg.in/yaml.v2 pl govendor-example m github.com/json-iterator/go m golang.org/x/sys/unix列出一个包被哪些包引用govendor list -v fmts fmt ├── e github.com/gin-contrib/sse ├── e github.com/gin-gonic/gin ├── e github.com/gin-gonic/gin/render ├── e github.com/golang/protobuf/proto ├── e github.com/ugorji/go/codec ├── e gopkg.in/go-playground/validator.v8 ├── e gopkg.in/yaml.v2 └── pl govendor-example使用建议使用govendor管理项目并进行项目协作时,我们每次不需要提交整个vendor目录,而只需要提交json文件,十分方便。一个配置文件全部搞定!vendor 目录解决了工程依赖打包的问题,可将依赖与工程一起打包,减少下载依赖的时间。同一个项目只创建一个govendor目录,且在代码库的一级目录。缺点依赖包全部都在vendor目录下,每个项目都有一份,所以每次拉取项目时都会拉一遍依赖。govendor不区分包版本,意味着开发期间拉的依赖的包很可能跟上线后的拉的依赖包版本不一致,很危险。govendor add +e会拉取全部外部包,即使是本项目没有用到的,这样会造成大量的冗余。但是只用govendor add +指定包又很麻烦。go modgo module是Go1.11版本之后官方推出的版本管理工具,并且从Go1.13版本开始,go module将是Go语言默认的依赖管理工具。包不再保存在GOPATH中,而是被下载到了$GOPATH/pkg/mod路径下.go mod vendor 会将依赖包放到 vendor 目录go.mod文件记录了项目所有的依赖信息,其结构大致如下:module github.com/Q1mi/studygo/blogger go 1.12 require ( github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586 github.com/gin-gonic/gin v1.4.0 github.com/go-sql-driver/mysql v1.4.1 github.com/jmoiron/sqlx v1.2.0 github.com/satori/go.uuid v1.2.0 google.golang.org/appengine v1.6.1 // indirect )go.sum是一个构建状态跟踪文件。它会记录当前module所有的顶层和间接依赖,以及这些依赖的校验和,来确保这些模块的将来下载内容与第一次下载的内容相同,但是第一次下载的模块也有可能是非法的(代理服务不可信、模块源被黑等),所以Go 1.13推出GOSUMDB(Go CheckSum Database)用来公证模块的Hash值,从而提供一个可以100%复现的构建过程并对构建对象提供安全性的保证,同时还会保留过去使用的包的版本信息,以便日后可能的版本回退。go modgo mod 与 go vendor 区别
加密算法有哪些非对称加密算法:RSA,DSA/DSS 对称加密算法:AES,RC4,3DES HASH算法:MD5,SHA1,SHA256数据加密的长度变化加密流出字符串转换成 bytes -> padding -> base64 编码将字符串转换成 byte根据字符串所占长度不同,长度会扩充到不同倍数ascii 码的字符,如数字字母等,每个字符只占一个字节,长度不扩充正常情况下,汉字等 unicode 编码,一个字符占 3 个字节,长度扩充3倍如果是 mysql 中的 utf8mb4 编码,一个字符最大可以占用4个字节,长度扩充4倍padding将bytes 字符填充到16的整数倍,长度最大增加16加密长度增加28(消息校验体16, 随机数12)base64 编码长度填充到3整数倍, 之后4/3大致上最大长度为 4/3* n *length + 64Go 版本实现getCount 计算:func getCount(text string) int { l := countBytes(text) l = countPadding(l) l = countEncrypt(l, true) l = countBase64(l) return l } func countBytes(text string) int { orig := []byte(text) return len(orig) } func countPadding(l int) int { pl := 16 - (l % 16) return l + pl } func countEncrypt(l int, flag bool) int { if flag == false { return l + 16 } return l + 16 + 12 } func countBase64(l int) int { if l%3 == 0 { return l / 3 * 4 } else { return (l/3 + 1) * 4 } }内推链接
什么是索引根据索引类型,可以分成主键索引和二级索引(非主键索引)主键索引:主键索引是叶子结点保存主键对应行的全部数据, 在 InnoDB 中,主键索引,也被叫做聚簇索引。二级索引(非主键索引):二级索引的叶子结点保存的是索引值和主键值,当二级索引进行查询时,需要进行回表操作。select * from t_user where id=1 即主键查询方式,则只需要搜索id这棵B+树select * from t_user where name=‘张三’ 即普通索引查询方式,则需要先搜索name索引树,得到id的值为3,再到id索引树搜索一次。这个过程称为回表可以看到,基于二级索引的查询需要多扫描一颗索引数,因此,尽量使用主键查询。什么是覆盖索引场景:10W条数据,我要从其中查出100条不连续的数据,给你id,来查name和password进行展示,如何才能高性能的去使用?当 SQL 语句所求查询字段 (select 列)和查询条件字段(where) 全都包含在一个索引中(联合索引), 可以直接使用索引而不需要回表,这个就是覆盖索引。联合索引在某一列上加索引以提升相关语句查询效率,联合索引就是在多个列上加索引。相对单列来说,就是在多个列上加索引create table t_user ( id bigint(20) not null auto_increment , name varchar(255) not null, password varchar(255) , index(name) primary key (id) )engine=innodb default character set=utf8 collate=utf8_general_ci看个例子:select id from user_table where name= '张三'name 是二级索引,索引树的叶子结点存储的保存有 name 和 id 值,所以通过 name 索引树查找到 id 之后,可以直接提供查询结果,不需要回表。这个查询里 索引 name 覆盖了我们的查询需求,我们称为是覆盖索引。select password from user_table where name= '张三'name 索引树上找到叶子结点, name = "张三“ 对应的主键 id, 通过 id 在主键树上找到满足条件的数据。主键和索引有什么区别主键索引主键是一种约束,唯一索引是一种索引,两者在本质上是不同的。主键创建后一定包含一个唯一性索引,唯一性索引并不一定就是主键。唯一索引唯一性索引列允许空值,而主键列不允许为空值。主键列在创建时,已经默认为非空值 + 唯一索引了。主键可以被其他表引用为外键,而唯一索引不能。一个表最多只能创建一个主键,但可以创建多个唯一索引。主键更适合那些不容易更改的唯一标识,如自动递增列、身份证号等。索引下堆SET optimizer_switch = 'index_condition_pushdown=on';在MySQL 5.6中 引入的索引下推优化(index condition pushdown), 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数不使用索引下堆:根据(username,is_del)联合索引查询所有满足名称以“张”开头的索引,然后回表查询出相应的全行数据,然后再筛选出未删除的用户数据。每一个虚线表示一次回表操作。使用索引下堆InnoDB在(name,is_del)索引内部就判断了数据是否逻辑删除,对于逻辑删除的记录,直接判断并跳过虚线表示回表,使用索引下堆,回表次数减少为 2 次。
去找公司运维问出口ip,得到答复说:XX.XX.XX.128/25 这个网段一百多个ip都是我们的ip内心:???100+的ip?这个25代表什么?应该是误把255打成25了吧!恩,对的,应该是想说XX.XX.XX.128到XX.XX.XX.255这差不多有100多个的!!是问了问这个25是什么,运维大哥回答说这是子网掩码。?????IP 地址分类IP地址是一种在Internet上的给主机编址的方式,也称为网际协议地址。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址之间的差异A 类IP地址:一个 A 类地址由1 字节的网络地址和3字节主机地址形成 B 类 IP地址:一个B类地址由2字节的网络地址和2字节的主机地址形成 C 类 IP 地址:一个C 类地址由3字节的网络地址和1字节的主机地址形成A类保留给政府机构(0.0.0.0到127.255.255.255)B类分配给中等规模的公司(128.0.0.0到191.255.255.255)C类分配给任何需要的人(192.0.0.0到223.255.255.255)D类用于组播(224.0.0.0---239.255.255.255)E类用于实验(240.0.0.0---247.255.255.255) A、B、C三类中IP地址=网络地址+主机地址,而D、E两类不区分网络地址和主机地址 特殊说明:(1)A类中的 10.X.X.X是私有地址;127.X.X.X是保留地址 (2)B类中的 172.16.0.0~172.31.255.255是私有地址 (3)C类中的 192.168.X.X是私有地址主机地址和网络地址网络地址: 可以简单理解我们平时常说的网段 主机地址: 是在这个网段中不同设备的地址子网掩码子网掩码只有一个作用:将 IP 地址划分成网络地址和主机地址两部分。子网掩码是一个网络掩码,地址掩码,是用来指明 IP 地址的哪些标识是主机所在的子网,以及哪些标识是主机的位掩码,子网掩码不能单独存在,必须和 IP 结合使用。A类的默认子网掩码 255.0.0.0 B类的默认子网掩码 255.255.0.0 C类的默认子网掩码 255.255.255.0子网掩码的计算方式子网掩码的位数决定可能的子网数目和子网的主机数目。根据子网数利用子网数来计算在求子网掩码之前必须先搞清楚要划分的子网数目,以及每个子网内的所需主机数目。1.将子网数目转化为二进制来表示 2.取得该二进制的位数,为 N 3. 取得该IP地址的类子网掩码,将其主机地址部分的前N位置1 即得出该IP地址划分子网的子网掩码。如欲将B类IP地址168.195.0.0划分成27个子网:1)27=11011 2)该二进制为五位数,N = 5 3)将B类地址的子网掩码255.255.0.0的主机地址前5位置1(B类地址的主机位包括后两个字节,所以这里要把第三个字节的前5位置1),得到 255.255.248.0根据主机数利用主机数来计算1)将主机数目转化为二进制来表示 2)如果主机数小于或等于254(注意去掉保留的两个IP地址),则取得该主机的二进制位数,为 N,这里肯定N<8。如果大于254,则 N>8,这就是说主机地址将占据不止8位。 3)使用255.255.255.255来将该类IP地址的主机地址位数全部置1,然后从后向前的将N位全部置为 0,即为子网掩码值。如欲将B类IP地址168.195.0.0划分成若干子网,每个子网内有主机700台:1) 700=1010111100 2)该二进制为十位数,N = 10 3)将该B类地址的子网掩码255.255.0.0的主机地址全部置1,得到255.255.255.255然后再从后向前将后10位置0,即为:11111111.11111111.11111100.00000000即255.255.252.0。这就是划分成主机为700台的B类IP地址168.195.0.0的子网掩码。如何根据子网掩码计算网络地址和主机标识子网掩码与IP地址一样是32位地址,然后将IP地址与子网掩码进行与运算即可得到网络地址举个例子:IP地址为192.168.10.2,子网掩码为255.255.255.240。 先将十进制转换成二进制: IP地址: 11000000 10101000 00001010 00000010 子网掩码:11111111 11111111 11111111 11110000 进行与运算:-------------------------- 11000000 10101000 00001010 00000000 则可得其网络标识为192.168.10.0,主机标识为2。XX.XX.XX.128/25回到开头的,已经说过了子网掩码也是32位的地址,那么开头的25怎么转化呢?25的意思是网络号为25,就代表连续的25个1,然后剩下的用0补齐即11111111 11111111 11111111 10000000(1)主机号:主机号+网络号=32,32-25=7 (2)网络地址:当7位主机号全为0,也就是XX.XX.XX.128 (3)广播地址:当7位主机号全为1,也就是XX.XX.XX.255 (4)可用地址数量:7位主机号有2 ^ 7 种结果,但是要去掉网络地址和广播地址,即:2^7-2=126(这个也就是运维所说的一百多个ip)
在项目中,对报文进行压缩、加密后,最后一步一般是 base64 编码。因为 base64 编码的字符串更适合不同平台,不同语言的传输。base64 编码的优点:算法是编码,不是压缩,编码后只会增加字节数(一般是比之前的多1/3,比如之前是3, 编码后是4)算法简单,基本不影响效率算法可逆,解码很方便,不用于私密传输。毕竟编码了,肉眼不能直接读出原始内容。加密后的字符串只有【0-9a-zA-Z+/=】 不可打印字符(转译字符)也可以传输为啥要编码计算机中任何数据都是 ASCII 码存储的, ascii 是 128-255 之间的不可见字符。在网络上进行数据交换,从 A 到 B, 往往要经过多个路由器,不同设备之间对字符的处理方式有一些不同,不可见字符有可能被错误处理,是不利于传输的,因此要先做一个 base64 编码,变成可见字符,这样出错的可能性比较大。看看英文版怎么说:When you have some binary data that you want to ship across a network, you generally don't do it by just streaming the bits and bytes over the wire in a raw format. Why? because some media are made for streaming text. You never know -- some protocols may interpret your binary data as control characters (like a modem), or your binary data could be screwed up because the underlying protocol might think that you've entered a special character combination (like how FTP translates line endings). So to get around this, people encode the binary data into characters. Base64 is one of these types of encodings. Why 64? Because you can generally rely on the same 64 characters being present in many character sets, and you can be reasonably confident that your data's going to end up on the other side of the wire uncorrupted.使用场景对于证书来说,尤其是根证书,一般是 base64 编码的,在网上被很多人下载电子邮件的附件一般是 base64 编码,因为附件往往有不可见字符比如 http 协议中 key , value 字段的值,必须进行 URLEncode,因为有写特殊符号,有特殊含义,那么需要把这些字符传输完再解析回来。xml 中如果像嵌入另外一个 xml 文件,直接嵌入,往往 xml 标签就乱套了, 不容易解析,因此,需要把 xml 编译成字节数组的字符串,编译成可见字符。网页中的一些小图片,可以直接以 base64 编码的方式嵌入,不用再链接请求消耗网络资源。较老的纯文本协议 SMTP ,这些文本偶尔传输一个文件时,需要用 base64base64 编码步骤将待编码的字符串转换成二进制表示出来3个字节为一组,也就是24位二进制为一组将这个24位分成4组,每 6个为一组,每组签名补 00 将6为二进制转换成8个二进制,从原来的3字节转换为4字节计算这4个字节对应的十进制,然后跟 ASCII 表对应,拼接字符串形成最后的 base64 编码。base64 源码base64.h#include "Base64.h" const std::string CBase64::base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; int CBase64::find_utf_8_bit_head(const unsigned char *src_content,int src_size ) { int i_ret = -1; int byteNum = 0; //字符数 if( src_content ) { for(int i = src_size-1; i >= 0; i--) { if( 0 == (src_content[i] >> 7 ) ) { byteNum = 1; i_ret = src_size - i; break; } if( 0x06 == (src_content[i]>> 5) ) { byteNum = 2; i_ret = src_size - i; break; } if( 0x0E == (src_content[i] >> 4) ) { byteNum = 3; i_ret = src_size - i; break; } if( 0x1E == (src_content[i] >> 3) ) { byteNum = 4; i_ret = src_size - i; break; } if( 0x3E == (src_content[i] >> 2) ) { byteNum = 5; i_ret = src_size - i; break; } if( 0x7E == (src_content[i] >> 1) ) { byteNum = 6; i_ret = src_size - i; break; } } if( i_ret == byteNum ) i_ret = -1; } return i_ret; } std::string CBase64::base64_encode(const unsigned char* bytes_to_encode, unsigned int in_len) { std::string ret; int i = 0; int j = 0; unsigned char char_array_3[3]; unsigned char char_array_4[4]; while (in_len--) { char_array_3[i++] = *(bytes_to_encode++); if (i == 3) { char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for(i = 0; (i <4) ; i++) ret += base64_chars[char_array_4[i]]; i = 0; } } if (i) { for(j = i; j < 3; j++) char_array_3[j] = '\0'; char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; while((i++ < 3)) ret += '='; } return ret; } //CString CBase64::Base64Encode(LPCTSTR lpszSrc) //{ // char* strSrc = new char[_tcslen(lpszSrc)+1]; // ZeroMemory(strSrc, _tcslen(lpszSrc)+1); // strcpy(strSrc, (char*)(_bstr_t(lpszSrc))); // std::string str = base64_encode((unsigned char*)strSrc, (int)strlen(strSrc)); // CString strDst = str.c_str(); // return strDst; //} std::string CBase64::Base64Encode(char* lpszSrc) { int str_len = strlen(lpszSrc); int find_index = find_utf_8_bit_head((unsigned char*)lpszSrc, str_len); if(find_index > -1) { memset(lpszSrc+(str_len-find_index), 0, find_index); } str_len = strlen(lpszSrc); return base64_encode((unsigned char*)lpszSrc, str_len); } std::string CBase64::base64_decode(const unsigned char* bytes_to_encode, unsigned int in_len) { int i = 0; int j = 0; int in_ = 0; unsigned char char_array_4[4], char_array_3[3]; std::string ret; while (in_len-- && ( bytes_to_encode[in_] != '=') /*&& is_base64(bytes_to_encode[in_])*/) { char_array_4[i++] = bytes_to_encode[in_]; in_++; if (i ==4) { for (i = 0; i <4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j <4; j++) char_array_4[j] = 0; for (j = 0; j <4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } //CString CBase64::Base64Decode(LPCTSTR lpszSrc) //{ // char* strSrc = new char[_tcslen(lpszSrc)+1]; // ZeroMemory(strSrc, _tcslen(lpszSrc)+1); // strcpy(strSrc, (char*)(_bstr_t(lpszSrc))); // std::string str = base64_decode((unsigned char*)strSrc, (int)strlen(strSrc)); // CString strDst = str.c_str(); // return strDst; //} std::string CBase64::Base64Decode(char* lpszSrc) { return base64_decode((unsigned char*)lpszSrc, (int)strlen(lpszSrc)); }base64.cpp#include "Base64.h" const std::string CBase64::base64_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz" "0123456789+/"; int CBase64::find_utf_8_bit_head(const unsigned char *src_content,int src_size ) { int i_ret = -1; int byteNum = 0; //字符数 if( src_content ) { for(int i = src_size-1; i >= 0; i--) { if( 0 == (src_content[i] >> 7 ) ) { byteNum = 1; i_ret = src_size - i; break; } if( 0x06 == (src_content[i]>> 5) ) { byteNum = 2; i_ret = src_size - i; break; } if( 0x0E == (src_content[i] >> 4) ) { byteNum = 3; i_ret = src_size - i; break; } if( 0x1E == (src_content[i] >> 3) ) { byteNum = 4; i_ret = src_size - i; break; } if( 0x3E == (src_content[i] >> 2) ) { byteNum = 5; i_ret = src_size - i; break; } if( 0x7E == (src_content[i] >> 1) ) { byteNum = 6; i_ret = src_size - i; break; } } if( i_ret == byteNum ) i_ret = -1; } return i_ret; } std::string CBase64::base64_encode(const unsigned char* bytes_to_encode, unsigned int in_len) { std::string ret; int i = 0; int j = 0; unsigned char char_array_3[3]; unsigned char char_array_4[4]; while (in_len--) { char_array_3[i++] = *(bytes_to_encode++); if (i == 3) { char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for(i = 0; (i <4) ; i++) ret += base64_chars[char_array_4[i]]; i = 0; } } if (i) { for(j = i; j < 3; j++) char_array_3[j] = '\0'; char_array_4[0] = (char_array_3[0] & 0xfc) >> 2; char_array_4[1] = ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4); char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6); char_array_4[3] = char_array_3[2] & 0x3f; for (j = 0; (j < i + 1); j++) ret += base64_chars[char_array_4[j]]; while((i++ < 3)) ret += '='; } return ret; } //CString CBase64::Base64Encode(LPCTSTR lpszSrc) //{ // char* strSrc = new char[_tcslen(lpszSrc)+1]; // ZeroMemory(strSrc, _tcslen(lpszSrc)+1); // strcpy(strSrc, (char*)(_bstr_t(lpszSrc))); // std::string str = base64_encode((unsigned char*)strSrc, (int)strlen(strSrc)); // CString strDst = str.c_str(); // return strDst; //} std::string CBase64::Base64Encode(char* lpszSrc) { int str_len = strlen(lpszSrc); int find_index = find_utf_8_bit_head((unsigned char*)lpszSrc, str_len); if(find_index > -1) { memset(lpszSrc+(str_len-find_index), 0, find_index); } str_len = strlen(lpszSrc); return base64_encode((unsigned char*)lpszSrc, str_len); } std::string CBase64::base64_decode(const unsigned char* bytes_to_encode, unsigned int in_len) { int i = 0; int j = 0; int in_ = 0; unsigned char char_array_4[4], char_array_3[3]; std::string ret; while (in_len-- && ( bytes_to_encode[in_] != '=') /*&& is_base64(bytes_to_encode[in_])*/) { char_array_4[i++] = bytes_to_encode[in_]; in_++; if (i ==4) { for (i = 0; i <4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j <4; j++) char_array_4[j] = 0; for (j = 0; j <4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } //CString CBase64::Base64Decode(LPCTSTR lpszSrc) //{ // char* strSrc = new char[_tcslen(lpszSrc)+1]; // ZeroMemory(strSrc, _tcslen(lpszSrc)+1); // strcpy(strSrc, (char*)(_bstr_t(lpszSrc))); // std::string str = base64_decode((unsigned char*)strSrc, (int)strlen(strSrc)); // CString strDst = str.c_str(); // return strDst; //} std::string CBase64::Base64Decode(char* lpszSrc) { return base64_decode((unsigned char*)lpszSrc, (int)strlen(lpszSrc)); }
什么是限流限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统 的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。比如场景:某天小明突然发现自己的接口请求突然之间涨到了原来的10倍,接口几乎不能使用,产生了一系列连锁反应,导致了整个系统崩溃。这就好比,老电闸中都安装了保险丝,一旦使用大功率设备,保险丝就会熔断,保证各个电器不被强电流烧坏,系统也同样安装保险丝,防止非预期请求过大,引起系统瘫痪。限流方法常用的限流算法有:计数法,滑动窗口计数法,漏桶算法和令牌桶算法。漏桶算法思路水(请求)进入到漏桶里,漏桶以一定的速度流出,当水流的速度过大会直接溢出, 漏桶是强行限制了数据的传输速率。令牌桶算法除了要能够限制数据的平均传输速率外,还需要允许某种程度的突发请求,令牌桶更为合适。令牌桶的思路是以一个恒定的速率往桶里放令牌,如果请求需要被处理,则需要从桶里取出一个令牌,如果没有令牌可取,那么就拒绝服务。Google开源工具包Guava提供了限流工具类RateLimiter是基于令牌桶算法来实现的。public double acquire() { return acquire(1); } public double acquire(int permits) { checkPermits(permits); //检查参数是否合法(是否大于0) long microsToWait; synchronized (mutex) { //应对并发情况需要同步 microsToWait = reserveNextTicket(permits, readSafeMicros()); //获得需要等待的时间 } ticker.sleepMicrosUninterruptibly(microsToWait); //等待,当未达到限制时,microsToWait为0 return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L); } private long reserveNextTicket(double requiredPermits, long nowMicros) { resync(nowMicros); //补充令牌 long microsToNextFreeTicket = nextFreeTicketMicros - nowMicros; double storedPermitsToSpend = Math.min(requiredPermits, this.storedPermits); //获取这次请求消耗的令牌数目 double freshPermits = requiredPermits - storedPermitsToSpend; long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros); this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros; this.storedPermits -= storedPermitsToSpend; // 减去消耗的令牌 return microsToNextFreeTicket; } private void resync(long nowMicros) { // if nextFreeTicket is in the past, resync to now if (nowMicros > nextFreeTicketMicros) { storedPermits = Math.min(maxPermits, storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros); nextFreeTicketMicros = nowMicros; } }计数器控制单位时间内的请求数量import java.util.concurrent.atomic.AtomicInteger; public class Counter { /** * 最大访问数量 */ private final int limit = 10; /** * 访问时间差 */ private final long timeout = 1000; /** * 请求时间 */ private long time; /** * 当前计数器 */ private AtomicInteger reqCount = new AtomicInteger(0); public boolean limit() { long now = System.currentTimeMillis(); if (now < time + timeout) { // 单位时间内 reqCount.getAndAdd(1); return reqCount.get() <= limit; } else { // 超出单位时间 time = now; reqCount = new AtomicInteger(0); return true; } } public static void main(String[] args) { } }计数方式有没有问题?假设每分钟请求数量为 60 个,每秒钟系统可以处理1个请求,用户在00:59 发送了60 个请求,然后在 1:00 发生了60个请求,此时 2 秒内有120个请求(每秒60)个请求,这样的方式并没有实现限制流量,因为每分钟可以处理60个,但是实际上这60个是一秒钟发过来的。滑动窗口计数滑动窗口是对计数方式对改进,增加一个时间粒度的度量单位。把一分钟分成了若干等份,比如分成6份, 每份10s, 在一份独立计数器上,在 00:00-00:09 之间计数器累加1, 当等份数量越大,限流统计越详细。package ratelimit; import java.util.Iterator; import java.util.Random; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.IntStream; public class TimeWindow { private ConcurrentLinkedQueue<Long> queue = new ConcurrentLinkedQueue<Long>(); /**a * 间隔秒数 */ private int seconds; /** * 最大限流 */ private int max; public TimeWindow(int max, int seconds) { this.seconds = seconds; this.max = max; new Thread(() -> { while (true) { try { Thread.sleep((seconds - 1) * 1000); } catch (InterruptedException e) { e.printStackTrace(); } clean(); } }).start(); } public static void main(String[] args) { final TimeWindow timeWindow = new TimeWindow(10, 1); IntStream.range(0, 3).forEach((i) -> { new Thread(() -> { try { while (true) { Thread.sleep(new Random().nextInt(20) * 100); } } catch (InterruptedException e) { e.printStackTrace(); } timeWindow.take(); }).start(); }); } public void take() { long start = System.currentTimeMillis(); int size = sizeOfValid(); if (size > max) { System.out.println("超限"); } synchronized (queue) { if (sizeOfValid() > max) { System.err.println("超限"); System.err.println("queue 中有:" + queue.size() + "最大数量:" + max); } this.queue.offer(System.currentTimeMillis()); } System.err.println("queue 中有:" + queue.size() + "最大数量:" + max); } private int sizeOfValid() { Iterator<Long> it = queue.iterator(); Long ms = System.currentTimeMillis() - seconds * 1000; int count = 0; while (it.hasNext()) { long t = it.next(); if (t > ms) { //在时间窗口范围内 count++; } } return count; } public void clean() { Long c = System.currentTimeMillis() - seconds * 1000; Long t1 = null; while ((t1 = queue.peek()) != null && t1 < c) { System.out.println("数据清理"); queue.poll(); } } }令牌桶问题令牌桶规定固定容量的桶,令牌 token 以固定速度往桶内填充,当桶填满时 token 不会继续放入,每过来一个请求把 token 从桶中移除,当没有 token 可以获取时,拒绝请求。令牌桶算法当网络设备衡量流量是否超过额定带宽时,需要查看令牌桶,而令牌桶中会放置一定数量的令牌,一个令牌允许接口发送或接收1bit数据(有时是1 Byte数据),当接口通过1bit数据后,同时也要从桶中移除一个令牌。当桶里没有令牌的时候,任何流量都被视为超过额定带宽,只有当桶中有令牌时,数据才可以通过接口。令牌桶中的令牌不仅仅可以被移除,同样也可以往里添加,所以为了保证接口随时有数据通过,就必须不停地往桶里加令牌,由此可见,往桶里加令牌的速度,就决定了数据通过接口的速度。 因此,我们通过控制往令牌桶里加令牌的速度从而控制用户流量的带宽。而设置的这个用户传输数据的速率被称为承诺信息速率(CIR),通常以秒为单位。比如我们设置用户的带宽为1000 bit每秒,只要保证每秒钟往桶里添加1000个令牌即可。令牌桶可以用来保护自己,主要用来对调用者频率进行限流,为的是不让自己的系统垮掉。令牌桶算法代码package com.netease.datastream.util.flowcontrol; import java.io.BufferedWriter; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.util.Random; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; /** * <pre> * Created by inter12 on 15-3-18. * </pre> */ public class TokenBucket { // 默认桶大小个数 即最大瞬间流量是64M private static final int DEFAULT_BUCKET_SIZE = 1024 * 1024 * 64; // 一个桶的单位是1字节 private int everyTokenSize = 1; // 瞬间最大流量 private int maxFlowRate; // 平均流量 private int avgFlowRate; // 队列来缓存桶数量:最大的流量峰值就是 = everyTokenSize*DEFAULT_BUCKET_SIZE 64M = 1 * 1024 * // 1024 * 64 private ArrayBlockingQueue<Byte> tokenQueue = new ArrayBlockingQueue<Byte>( DEFAULT_BUCKET_SIZE); private ScheduledExecutorService scheduledExecutorService = Executors .newSingleThreadScheduledExecutor(); private volatile boolean isStart = false; private ReentrantLock lock = new ReentrantLock(true); private static final byte A_CHAR = 'a'; public TokenBucket() { } public TokenBucket(int maxFlowRate, int avgFlowRate) { this.maxFlowRate = maxFlowRate; this.avgFlowRate = avgFlowRate; } public TokenBucket(int everyTokenSize, int maxFlowRate, int avgFlowRate) { this.everyTokenSize = everyTokenSize; this.maxFlowRate = maxFlowRate; this.avgFlowRate = avgFlowRate; } public void addTokens(Integer tokenNum) { // 若是桶已经满了,就不再家如新的令牌 for (int i = 0; i < tokenNum; i++) { tokenQueue.offer(Byte.valueOf(A_CHAR)); } } public TokenBucket build() { start(); return this; } /** * 获取足够的令牌个数 * * @return */ public boolean getTokens(byte[] dataSize) { // Preconditions.checkNotNull(dataSize); // Preconditions.checkArgument(isStart, // "please invoke start method first !"); int needTokenNum = dataSize.length / everyTokenSize + 1;// 传输内容大小对应的桶个数 final ReentrantLock lock = this.lock; lock.lock(); try { boolean result = needTokenNum <= tokenQueue.size(); // 是否存在足够的桶数量 if (!result) { return false; } int tokenCount = 0; for (int i = 0; i < needTokenNum; i++) { Byte poll = tokenQueue.poll(); if (poll != null) { tokenCount++; } } return tokenCount == needTokenNum; } finally { lock.unlock(); } } public void start() { // 初始化桶队列大小 if (maxFlowRate != 0) { tokenQueue = new ArrayBlockingQueue<Byte>(maxFlowRate); } // 初始化令牌生产者 TokenProducer tokenProducer = new TokenProducer(avgFlowRate, this); scheduledExecutorService.scheduleAtFixedRate(tokenProducer, 0, 1, TimeUnit.SECONDS); isStart = true; } public void stop() { isStart = false; scheduledExecutorService.shutdown(); } public boolean isStarted() { return isStart; } class TokenProducer implements Runnable { private int avgFlowRate; private TokenBucket tokenBucket; public TokenProducer(int avgFlowRate, TokenBucket tokenBucket) { this.avgFlowRate = avgFlowRate; this.tokenBucket = tokenBucket; } @Override public void run() { tokenBucket.addTokens(avgFlowRate); } } public static TokenBucket newBuilder() { return new TokenBucket(); } public TokenBucket everyTokenSize(int everyTokenSize) { this.everyTokenSize = everyTokenSize; return this; } public TokenBucket maxFlowRate(int maxFlowRate) { this.maxFlowRate = maxFlowRate; return this; } public TokenBucket avgFlowRate(int avgFlowRate) { this.avgFlowRate = avgFlowRate; return this; } private String stringCopy(String data, int copyNum) { StringBuilder sbuilder = new StringBuilder(data.length() * copyNum); for (int i = 0; i < copyNum; i++) { sbuilder.append(data); } return sbuilder.toString(); } public static void main(String[] args) throws IOException, InterruptedException { tokenTest(); } private static void arrayTest() { ArrayBlockingQueue<Integer> tokenQueue = new ArrayBlockingQueue<Integer>( 10); tokenQueue.offer(1); tokenQueue.offer(1); tokenQueue.offer(1); System.out.println(tokenQueue.size()); System.out.println(tokenQueue.remainingCapacity()); } private static void tokenTest() throws InterruptedException, IOException { TokenBucket tokenBucket = TokenBucket.newBuilder().avgFlowRate(512) .maxFlowRate(1024).build(); BufferedWriter bufferedWriter = new BufferedWriter( new OutputStreamWriter(new FileOutputStream("D:/ds_test"))); String data = "xxxx";// 四个字节 for (int i = 1; i <= 1000; i++) { Random random = new Random(); int i1 = random.nextInt(100); boolean tokens = tokenBucket.getTokens(tokenBucket.stringCopy(data, i1).getBytes()); TimeUnit.MILLISECONDS.sleep(100); if (tokens) { bufferedWriter.write("token pass --- index:" + i1); System.out.println("token pass --- index:" + i1); } else { bufferedWriter.write("token rejuect --- index" + i1); System.out.println("token rejuect --- index" + i1); } bufferedWriter.newLine(); bufferedWriter.flush(); } bufferedWriter.close(); } }令牌桶和漏桶的选择问题
RPC 原理RPC( Remote Procedure Call ) 远程调用过程。1. 定义了一个接口文件,描述了对象,对象成员,接口方法等一系列信息。 2.通过RPC 框架提供的编译器,将接口说明文件编译成对应的语言文件。 2. 在客户端和服务端分别引用 RPC 编译器生成的文件,即可像调用本地方法一样远程调用。RPC 通信过程如下:1.客户端以正常方式调用客户桩(client stub) 2. 客户桩生成一个消息,然后调用本地操作系统。 3. 客户端操作系统将消息发送给原程操作系统。 4. 远程操作系统将消息交给服务器桩 5. 服务器桩将参数提取出来,然后调用服务过程 6. 服务器执行要求的操作,操作完成后将结果返回给服务器桩, 7. 服务器桩将结果打包成一个消息, 然后调用本地操作系统。 8. 服务器操作系统将含有结果的消息发送给客户端操作系统 9. 客户端操作系统将消息交给客户桩 10. 客户桩将结果提取出来,返回给他的调用方 资源粒度, RPC 调用类似于本地调用,RESTful API 每一次添加接口都可能需要额外开发接口的数据,这相当于应用视图中再写一次方法调用。流量消耗,RestFull API 在应用层和使用 HTTP 协议, 即使是传输高效的 JSON 也会消耗较大流量, RPC 可以使用 TCP,也可以使用 UDP , 而且可以编码,降低数据大小和减少流量消耗。Thrift 架构Thrift 作用于各个服务之间的 RPC 通信,支持跨语言,thrift 是一个典型的 CS 框架,客户端服务端可以使用不同的语言开发, thrift 通过 IDL (Interface Description Language) 来关联客户端和服务器。Thrift 整体架构your code 是业务逻辑代码FooService.Client /Foo.Processor() + Foo.Write() Foo.Read是 thrift 根据 IDL 生成的客户端和服务端代码,对应的是 RPC 中的 Client Stub 和 Server StubTProtocol 是用来对数据进行序列化和反序列化。TTransport 提供传输数据功能,使用 Apache Thrift 可以方便的定义一个服务并选择不同的传输协议。Thrift 网络栈架构TTransport 层TSocket :阻塞 SocketTFrametransport :以 frame 为单位进行传输, 非阻塞式服务中使用TFileTransport : 以文件形式进行传输TProtocol 层代表 thrift 客户端和服务端之间的传输数据的协议,指的是客户端和服务端传输数据的格式,比如 Json, thrift 中有如下格式:TBinaryProtocol:二进制格式TCompactProtocol:压缩格式TJSONProtocol : Json 格式TSimpleJsonProtocol:提供只写的 JSON 协议Thrift 支持的 Server 模型TSimpleServer :用于简单的单线程模型,常用于测试TThreadPoolServer :多线程模型,使用标准的阻塞 IOTNoBlockingServer: 多线程服务模型,使用非阻塞 IO,需要使用TFramedTransport 数据传输方式。THsHaServer : THsHa 引入了线程池去处理,其模型读写任务放到线程池去处理,Half-sync/Half-async处理模式,Half-async是在处理IO事件上(accept/read/write io),Half-sync用于handler对rpc的同步处理;Thrift 支持的基本数据类型byte: 有符号字节i16: 16 位有符号整数i32 : 32 位有符号整数i64: 64 位有符号整数double : 64 位浮点数string : 字符串Thrift 支持的容器类型list:一系列由 T 类型的数据组成的有序列表, 元素可以重复set : 一系列由 T 类型组成的无序集合,元素不可以重复map: 一个字典结构,Key 为 K 类型, Value 为 V 类型,和 Java 中的 HashMap 类似thrift 支持 struct 类型,可以将一些数据类型聚合到一块。struct People { 1:string name; 2:i32 age; 3:string gender; }thrift 支持枚举类型enum Gender { MALE, FEMALE }thrift 支持异常类型exception RequestException { 1:i32 code; 2:string reason; }thrift 定义 Service. 格式如下:service HelloWorldService { // service中可以定义若干个服务,相当于Java Interface中定义的方法 string doAction(1:string name, 2:i32 age); }thrift 支持给类型定义别名typedef i32 int typedef i64 longthrift. 支持常量的定义const i32 MAX_RETRIES_TIME = 10; const string MY_WEBSITE = "http://facebook.com";thrift 支持命名空间,相当于 Java 中的package.namespace java com.test.thrift.demo#、//、/**/都可以作为thrift文件中的注释。thrift提供两个关键字required和optional,分别用于表示对应的字段是必填的还是可选的(推荐尽量使用optional),如下struct People { 1:required string name; 2:optional i32 age; }thrift也支持文件包含,相当于CPP中的include,Java中的import,使用关键字include:include "global.thrift"thrift IDL 例子// data.thrift namespace java thrift.generated namespace py py.thrift.generated typedef i16 short typedef i32 int typedef i64 long typedef bool boolean typedef string String // struct关键字用于定义结构体,相当于面向对象编程语言中的类 struct Person { // 相当于定义类中的成员,并生成相应的get和set方法,optional表示username这个成员可以没有 1: optional String username, 2: optional int age, 3: optional boolean married } // 定义一个异常类型,用于接口中可能抛出的异常 exception DataException { 1: optional String message, 2: optional String callStack, 3: optional String date } // 定义服务接口 service PersonService { Person getPersonByUsername(1: required String username) throws (1: DataException data), void savePerson(1: required Person person) }执行 thrift --gen java src/thrift/data.thrift 生成代码.thrift 如何安装,可参考 https://wangxiaoming.blog.csdn.net/article/details/114317905客户端可以像调用本地的方法一样调用服务端的方法生成代码结构如下:
Raft 协议开源代码:https://github.com/wenweihu86/raft-javaRaft 协议是工程上使用比较广泛的一致性, 去中心化的,高可用的分布式协议。raft 是一个共识算法, 所谓共识算法,是对某个事件达成一致的看法。Raft 论文http://thesecretlivesofdata.com/raft/Raft 算法简介问题分解 复制集中点一致性这个问题,分解为各个可以被独立解释的子问题。包括 leader election, log replication, safety , membership changes。状态简化 对算法做出一些限制, 减少需要考虑的状态数,使得算法更加清晰。leader electionleader 所有请求的处理者,接收客户端发起的操作请求,写入本地日志后同步到其他集群其他节点follower 请求的被动更新者,从leader 接收更新请求,写入到本地日志。如果客户端的操作请求发送到了发送给 follower, 会先从 follwer 发送到 leadercandidate 如果 follower 在一定的时间没有接收到 follower 的心跳, 此时进入leader election, 本节点切换为 candidate 直到选主结束。状态转移图:所有节点启动时都是 follower 状态, 在 一段时间如果没有收到来自 leader 的心跳,follower 切换为 candidate , 并发起选举。如果收到了 majority 的选举票(包含自己的一票),那么切换为 leader 状态。如果发现其他节点比自己更新,则主动切换为 follower系统中只会存在一个 leader, 如果一段时间内没有 leader, 那么大家通过选举的方式选出 leader. leader 不停的向 follower 发出心跳,表面 leader 的存活状态, 如果l leader 故障,follower 会切换成 candidate 选举出新 leader。term每个 leader 有一个新的履职期, 这个履职期称为一届任期, 叫做 term。任期(term) 是从选举开始算,然后有一段或长或短的工作期(normal operation), 任期充当了一个逻辑时钟的概念, term3 的意思是还没有选举出 leader就已经结束了, 然后发起了一个新的选举 term4。选举过程follower 在 timeout 时间内,没有收到来自 leader 的心跳,则会发起选举:增加节点本地的 current term, 切换为 candidate 状态投自己一票并行的发送给其他节点 RequestVotes RPCs等待其他节点的回复收到了 大多数选票(majority ),那么赢得选举,切换状态为 leader被告知别人已经当选,则切换为 follower一段时间还是没有收到 majority 的投票结果,保持 candidate 状态,重新发出选举。每个任期,一个节点只能投票一次候选人知道的信息不能比自己少fisrtcome- first-serverd 先到先得log replication当系统有了leader, 系统应用进入了工作区, 客户端的一切请求会发送到 leader, leader 来调度这些并发请求到顺序,并且保证 leader 和 follower 状态的一致性。raft 的做法就是,将这些请求的顺序告诉 follower, leader 和 follwer 以相同的顺序来执行这些请求, 保持状态一致。所谓强一致性, 并不是指集群中任意时刻状态都一致。而是指一个目标, 让一个分布式系统看起来只有一个数据副本, 并且读写都是原子的, 这样应用层可以忽略系统多个数据间的同步问题。共识算法,就是来保证一致性的。共识算法保证在小部分节点故障的 <=(N-1)/2 节点故障的情况下,系统仍然可以对外提供服务。共识算法,是通过复制状态机来实现。所有节点从同一个状态 state 出发,经过相同的 log, 最终达到一致的状态 state.相同的初始状态+相同的输入 = 相同的结束状态。Raft 负责保证集群所有节点 log 一致性。leader 具有较强的领导力, 所有 log 都必须交给 leader 节点处理,并由 leader 复制给其他节点。这个过程叫做日志复制。日志复制机的流程:leader 选举出来后,就承担了领导整个集群的责任,开始接受客户端请求,并将操作包装成日志,发送给其他节点。leader 为客户端提供服务, 客户端的每个请求都包含一条被状态复制机执行的指令。leader 把该指令作为一个新的日志附加到自身的日志集合。然后向其他节点发起附加请求条目(AppendEntries RPC)。来要求其他节点将日志附加到自己的本地日志集合中。当这条日志确保被安全复制,即 (N/2 +1) 节点有复制后,leader 将该日志 apply 到他本地的状态机中,然后把操作成功的结果返回给客户端。每个 logEntry 包含了 command, 还有 leader 的 term ,五个节点的日志并不完全一致, Raft 保证了高可用,但是并不强一致性,而是最终一致性。leader 会不断尝试给 follower 发送 log entries,直到所有节点都相同。raft日志复制: https://raft.github.io/安全性对选举的限制每个 candidate 必须在 RequestVote RPC 中携带自己的最新日志,如果 follower 发现candidate 日志还没有自己的新, 那么就拒绝投给该 candidate也就是说 candidate 想要赢得选举,必须得到大多数节点的投票,那么它的日志一定不能落后大多数节点。又因为一条日志只有被复制到大多数节点才能被 COMMIT, 也就是说 赢的选举的 candidate 一定拥有所有 commited 节点的日志。对提交的限制除了对选举限制外,还需要对 commit 增加一些限制。当 leader 得知某条日志被集群过半的节点复制成功时,就可以进行 commit,committed 日志一定最终会被状态机 apply。leader 并不能随时随地 commit 旧任期的留下的日志,即便旧任期已经复制到大部分节点,上图从左到右按时间顺序模拟了问题场景。阶段a:S1 是 leader,收到请求后将 (term2, index2) 只复制给了 S2,尚未复制给 S3 ~ S5。阶段b:S1 宕机,S5 当选 term3 的 leader(S3、S4、S5 三票),收到请求后保存了 (term3, index2),尚未复制给任何节点。阶段c:S5 宕机,S1 恢复,S1 重新当选 term4 的 leader,继续将 (term2, index2) 复制给了 S3,已经满足大多数节点,我们将其 commit。阶段d:S1 又宕机,S5 恢复,S5 重新当选 leader(S2、S3、S4 三票),将 (term3, inde2) 复制给了所有节点并 commit。注意,此时发生了致命错误,已经 committed 的 (term2, index2) 被 (term3, index2) 覆盖了。为了避免这种错误,我们需要添加一个额外的限制:Leader 只允许 commit 包含当前 term 的日志。针对上述场景,问题发生在阶段c,即使作为 term4 leader 的 S1 将 (term2, index2) 复制给了大多数节点,它也不能直接将其 commit,而是必须等待 term4 的日志到来并成功复制后,一并进行 commit。阶段e:在添加了这个限制后,要么 (term2, index2) 始终没有被 commit,这样 S5 在阶段d将其覆盖就是安全的;要么 (term2, index2) 同 (term4, index3) 一起被 commit,这样 S5 根本就无法当选 leader,因为大多数节点的日志都比它新,也就不存在前边的问题了。以上便是对算法增加的两个小限制,它们对确保状态机的安全性起到了至关重要的作用。至此我们对 Raft 算法的核心部分,已经介绍完
什么是领域驱动领域模型是通过识别领域对象与行为来连接现实主体与操作之间的映射关系。对象行为的组织原则更体现面向对象对象设计思想,通过聚合,解耦抽象等方式达到系统的可复用,可维护,可扩展能力。MVCMVC 三层架构中 M 表示 model ,V 表示的是 View, C 表示的是 Controller, 也就是分成了三层:数据层, 表示层,逻辑层。模型:负责存储系统的中心数据视图:将数据显示给用户控制器:处理用户输入的信息,负责读取视图数据,控制用户输入,并向模型写入数据。后端项目一般是分成三层: Repository 层,Service 层,Controller 层,其中,Repository 负责数据访问,Service 负责处理业务逻辑, Controller 负责暴露接口。什么是贫血模型贫血模型领域对象很简单,只有所有属性的 get/set 方式,与少量的属性值转换,不包含任何业务逻辑, 不关心数据对象的持久化,只用来做数据对象的承载和传递介质。领域对象@Entity @Data @ToString @AllArgsConstructor @NoArgsConstructor public class User { @Id private String userId; private String userName; private String password; private boolean isLock; }真正的业务逻辑在领域服务中负责实现,此服务中引入持久化仓库, 在业务逻辑之后持久化到仓库中,并可发布领域事件。Service 层, 领域服务, 业务处理,使用领域对象进行数据承载。public interface UserService { void create(User user); void edit(User user); void changePassword(String userId, String newPassword); void lock(String userId); void unlock(String userId); } @Service public class UserServiceImpl implements UserService { @Autowired private UserRepository repo; @Override public void edit(User user) { User dbUser = repo.findById(user.getUserId()).get(); dbUser.setUserName(user.getUserName()); repo.save(dbUser); // 发布领域事件 ... } @Override public void lock(String userId) { User dbUser = repo.findById(userId).get(); dbUser.setLock(true); repo.save(dbUser); // 发布领域事件 ... } // ... 省略完整代码 }再看个贫血模型的具体例子:////////// Controller+VO(View Object) ////////// public class UserController { private UserService userService; //通过构造函数或者IOC框架注入 public UserVo getUserById(Long userId) { UserBo userBo = userService.getUserById(userId); UserVo userVo = [...convert userBo to userVo...]; return userVo; } } public class UserVo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Service+BO(Business Object) ////////// public class UserService { private UserRepository userRepository; //通过构造函数或者IOC框架注入 public UserBo getUserById(Long userId) { UserEntity userEntity = userRepository.getUserById(userId); UserBo userBo = [...convert userEntity to userBo...]; return userBo; } } public class UserBo {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; } ////////// Repository+Entity ////////// public class UserRepository { public UserEntity getUserById(Long userId) { //... } } public class UserEntity {//省略其他属性、get/set/construct方法 private Long id; private String name; private String cellphone; }UserEntity 和 UserRepository 负责数据访问, UserBo 和 UserService 负责处理业务逻辑, UserVo 和 UserController 属于接口层。这样 UserBo 中只包含属性数据,不包含业务逻辑的累,叫做贫血模式传统的贫血模型其实很受欢迎, 贫血模型是面向过程的编程风格。开发系统业务比较简单,只要实现 CRUD 操作即可, 不需要费力设计充血模型, 其次贫血模型足够应对这种简单的业务操作。充血模型的设计比贫血模型更加有难度,充血模型是面向对象的编程风格,一开始就要想好,对数据要暴露哪些操作,定义哪些业务逻辑, 而不是和贫血模型一样,只要设计好模型,后续有什么功能需求,Service 就定义什么操作。思维固化,转型有成本,基于贫血模型开发多年,一下要转换成充血模型,领域驱动,有一定的学习成本。充血模型充血模型包含领域对象作用此对象的相关行为, 领域对象不仅包含属性,还包含业务处理逻辑,同时也包含对象的持久化操作。@Entity @Data @Builder @AllArgsConstructor public class User implements UserService { @Id private String userId; private String userName; private String password; private boolean isLock; // 持久化仓库 @Transient private UserRepository repo; // 是否是持久化对象 @Transient private boolean isRepository; @PostLoad public void per() { isRepository = true; } public User() { } public User(UserRepository repo) { this.repo = repo; } @Override public void create(User user) { repo.save(user); } @Override public void edit(User user) { if (!isRepository) { throw new RuntimeException("用户不存在"); } userName = user.userName; repo.save(this); // 发布领域事件 ... } @Override public void lock() { if (!isRepository) { throw new RuntimeException("用户不存在"); } isLock = true; repo.save(this); // 发布领域事件 ... } }领域模型的优点对象自治度很高,表达能力很强,非常适合复杂的业务逻辑的实现,可靠复用程度比较高。更符合面向对象的思想。当前充血模型不够纯粹,将业务逻辑和持久化操作放在一块,有一定的耦合度。在领域对象行为比较复杂的情况下, 需要多个行为共享对象状态时,充血模型表现更强。充血模型2上面的充血模型,不仅包含业务逻辑,还包含数据持久化,为解决业务逻辑不纯粹的问题,可以将持久化操作移出去。@Entity @Data @Builder @AllArgsConstructor public class User implements UserService { @Id private String userId; private String userName; private String password; private boolean isLock; // 是否是持久化对象 @Transient private boolean isRepository; @Override public void create(User user) { user.userId = UUID.randomUUID().toString(); } @Override public void edit(User user) { userName = user.userName; } @Override public void lock() { isLock = true; } } @Service public class UserManager { @Autowired private UserRepository repo; public User findOne(String userId){ return repo.findById(userId).get(); } public void edit(User u) { User user = findOne(u.getUserId()); user.edit(u); repo.save(user); // 发布领域事件 ... } public void lock(String userId) { User user = findOne(userId); user.lock(); repo.save(user); // 发布领域事件 ... } }去掉了持久化的入侵,业务更加纯粹什么场景选择充血模型基于充血模型的 DDD 开发模式,更适合复杂的系统开发,比如包含各种利息计算模型,还款模型的复杂业务的金融系统。领域驱动设计主要是用来指导如何解耦业务系统,划分业务模块, 定义业务领域模型及其交互。他的发展主要是由于微服务,微服务不仅仅要关注服务治理,监控,调用链追踪, API 网关等问题,还有个重要的问题,就是对公司业务进行拆分,形成微服务。对领域驱动设计关键在于对业务的熟悉理解程度,领域驱动设计的思想是:轻 Service ,重 Domain。我们平时开发时,大部分是 SQL 驱动,接到一个后端开发需求时,首先是去看接口如何对应到数据库中,要访问哪些表,哪些库,然后思考 SQL 如何写。这样会看到,类似的SQL 到处都是,通常为一个 需求写一个 SQL。对于领域驱动,我们首先需要理清所有业务,包括定义模型包含的属性和方法,领域模型相当于可复用业务的中间层,新的需求需要基于之前定义好的业务来完成。
Consul 架构简介Consul 是一款不错的服务注册与发现工具。Consul 架构图:图片上 datacenter 分成上下两个部分, 但是这两个部分又不是完全隔离的。他们之间通过 WAN GOSSIP 进行报文交互。单个 datacenter 中, 节点被划分成两种颜色, 红色的 server, 紫色的 client, 他们之间通过 GRPC 进行通信(业务数据), 除此之外, Client 和 Server 之间通过还有一条 LAN Gosssip 进行通信,比如,当 Server 节点增加,或者 down 机后,Client 可以获取对应的 Server列表,去除或者增加 Server 列表。同一个 Consul agent 程序,启动的时候,通过制定不同的参数来运行 Server 和 Client 模式。也就是说 client 和 server 本质上都是 Client AgentServer:参与共识仲裁(raft)存储机器状态(日志存储)处理查询维护周边(LAN/WAN) 节点之间的关系Client :负责通过该节点注册到 Consul 微服务的健康检查将客户端的注册请求和查询转换为 server 的 RPC 请求维护周边各节点(LAN/WAN) 的关系Client-Server 模式架构图:目前 Consul 的架构全部升级为 client-Server 模式,服务注册不再向 consul-Server进行注册,而是向服务所在机器的 consul-client 进行注册,通过 Consul-Client 将注册信息同步到 Consul-Server 机器中。纯 Server 模式Zookeeper 就是这种模,client server 本质上都是 consul 的 agent, 只是角色不同。纯 server 模式的架构图:纯 server 模式架构的问题高可用 服务实例注册时配置的 consul-host 是负载均衡地址,服务注册到集群后,由集群中的一个节点负责对该实例进行健康检查。假如有这样的情况,服务A,服务B,都注册到 Service1 ,也就是由 Service1 对 服务A,服务B 进行健康检查,当 Service1 宕机时,这样会导致 服务A,服务B 在注册列表中消失,导致无法无法访问到,但是其实服务本身没有问题。服务注销 当服务从注册中心注销时,由于是负载均衡的,可能会导致服务注销失败,因为要在Service1 上注销,有可能负载均衡到 Service2 上进行注销,导致注销失败,解决的办法是遍历集群中所有节点进行注销。Client-Server 架构呢?高可用 服务实例向本级别 consul-client 进行注册,如果 consul-client 不可用,只影响 consul-client 所在机器的服务实例,不影响另外一台机器上的实例。服务注销 服务注销值需要在 consul-client 上进行注销,不需要经过负载均衡。基于 Client-Server架构服务注册时序图服务端口实现原理核心原理在于亮点:集群信息之间的高效同步机制,保障拓扑变动与控制信号的及时同步server 集群内日志存储的强一致性。他们主要基于两个协议来实现Gossip 协议,在集群内消息传递使用 raft 协议保障日志的一致性。
Slice 也是值传递么?看个例子吧:func TestSliceReference(t *testing.T) { var args = []int64{1,2,3} fmt.Printf("切片args的地址: %p\n",args) modifiedNumber3(args) fmt.Println(args) } func modifiedNumber3(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) args[0] = 10 }运行结果:=== RUN TestSliceReference 切片args的地址: 0xc000016120 形参切片的地址 0xc000016120 [10 2 3] --- PASS: TestSliceReference (0.00s) PASS发现没有,切片的地址和形参的地址为啥一样, 难道也问题?不是值传递?上面可以看到,我们并没有用取址符 & 来进行地址转换,就把 slice 打印出来来,再测试一下:func TestSliceReference2(t *testing.T) { var args = []int64{1,2,3} fmt.Printf("切片args的地址: %p \n",args) fmt.Printf("切片args第一个元素的地址: %p \n",&args[0]) fmt.Printf("直接对切片args取地址%v \n",&args) modifiedNumber4(args) fmt.Println(args) } func modifiedNumber4(args []int64) { fmt.Printf("形参切片的地址 %p \n",args) fmt.Printf("形参切片args第一个元素的地址: %p \n",&args[0]) fmt.Printf("直接对形参切片args取地址%v \n",&args) args[0] = 10 }运行结果=== RUN TestSliceReference2 切片args的地址: 0xc0000ee030 切片args第一个元素的地址: 0xc0000ee030 直接对切片args取地址&[1 2 3] 形参切片的地址 0xc0000ee030 形参切片args第一个元素的地址: 0xc0000ee030 直接对形参切片args取地址&[1 2 3] [10 2 3] --- PASS: TestSliceReference2 (0.00s) PASS可以看到的是 使用 & 地址符进行取址是无效的,而且使用 %p 取出的地址是和第一个元素地址是一样的。为啥这样?看 fmt.Printf 源码fmt包,print.go中的printValue这个方法,截取重点部分,因为`slice`也是引用类型,所以会进入这个`case`: case reflect.Ptr: // pointer to array or slice or struct? ok at top level // but not embedded (avoid loops) if depth == 0 && f.Pointer() != 0 { switch a := f.Elem(); a.Kind() { case reflect.Array, reflect.Slice, reflect.Struct, reflect.Map: p.buf.writeByte('&') p.printValue(a, verb, depth+1) return } } fallthrough case reflect.Chan, reflect.Func, reflect.UnsafePointer: p.fmtPointer(f, verb)可以看到 p.buf.writeByte('&') 这个代码打印地址输出结果带有 & , 这个就是为啥fmt.Printf("切片args的地址: %p \n",args)打印出来的是一个地址 0xc0000ee030。为啥打印出来的切片中还会包含 "[]" 呢?看下 printValue 源码:case reflect.Array, reflect.Slice: //省略部分代码 } else { p.buf.writeByte('[') for i := 0; i < f.Len(); i++ { if i > 0 { p.buf.writeByte(' ') } p.printValue(f.Index(i), verb, depth+1) } p.buf.writeByte(']') }因为递归调用了 p.printValue(a, verb, depth+1) 进行打印, 这个就是为啥fmt.Printf("直接对切片args取地址%v \n",&args)输出直接对切片args取地址&[1 2 3]还有个问题, 为啥 %p 输出的内存地址 和 slice 结构里面第一个元素的地址是一样的呢?func (p *pp) fmtPointer(value reflect.Value, verb rune) { var u uintptr switch value.Kind() { case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer: u = value.Pointer() default: p.badVerb(verb) return } ...... 省略部分代码 // If v's Kind is Slice, the returned pointer is to the first // element of the slice. If the slice is nil the returned value // is 0. If the slice is empty but non-nil the return value is non-zero. func (v Value) Pointer() uintptr { // TODO: deprecate k := v.kind() switch k { case Chan, Map, Ptr, UnsafePointer: return uintptr(v.pointer()) case Func: if v.flag&flagMethod != 0 { ....... 省略部分代码上面有一句话,If v’s Kind is Slice, the returned pointer is to the first。意思是对于 slice 类型,返回的是元素第一个元素的地址,这里正好解释上面为什么 fmt.Printf("切片args的地址:%p \n",args)和fmt.Printf("形参切片的地址 %p \n",args)打印出来的地址是一样的,因为args是引用类型,所以他们都返回slice这个结构里的第一个元素的地址。Slice 的结构//runtime/slice.go type slice struct { array unsafe.Pointer len int cap int }slice 是一个结构体, 第一个元素是指针类型,这个指针指向的是底层数组的第一个指针。所以当 slice 类型的时候, fmt.Printlnf() d打印的是第一个元素的地址。其实也是指针处理,只不过指针的存放的内容是第一个元素的内存地址,但是这个指针的在传递过程中是会拷贝的。slice 其实也是值传递,为啥引用类型传递可以修改原内容的数据, 因为底层默认传递的指向第一个元素地址的指针。容易混淆的是 fmt.printf() 打印的是第一个这个传递指针对应的内容,而不是存储指针的地址,会给人一种错觉,以为是引用传递。map 是值传递么?map 没有明显的指针func TestMapReference(t *testing.T) { persons:=make(map[string]int) persons["asong"]=8 addr:=&persons fmt.Printf("原始map的内存地址是:%p\n",addr) modifiedAge(persons) fmt.Println("map值被修改了,新值为:",persons) } func modifiedAge(person map[string]int) { fmt.Printf("函数里接收到map的内存地址是:%p\n",&person) person["asong"]=9 }运行结果:=== RUN TestMapReference 原始map的内存地址是:0xc00000e038 函数里接收到map的内存地址是:0xc00000e040 map值被修改了,新值为: map[asong:9] --- PASS: TestMapReference (0.00s) PASS接着看一下 map 源码//src/runtime/map.go // makemap implements Go map creation for make(map[k]v, hint). // If the compiler has determined that the map or the first bucket // can be created on the stack, h and/or bucket may be non-nil. // If h != nil, the map can be created directly in h. // If h.buckets != nil, bucket pointed to can be used as the first bucket. func makemap(t *maptype, hint int, h *hmap) *hmap { mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) if overflow || mem > maxAlloc { hint = 0 } // initialize Hmap if h == nil { h = new(hmap) } h.hash0 = fastrand()map 使用 make 函数返回的是一个 hmap 类型的指针,func modifiedAge(persons map[string]int) 其实和 func modifiedAge(person *hmap) 意思是一样的,这样实际上传递也是使用了指针的副本进行传递, 属于值传递, map 也是引用类型,但是传递的类型不是引用,也是值传递,传递的是指针的拷贝。chan 是值传递么func TestChanReferencr(t *testing.T) { p := make(chan bool) fmt.Printf("原始chan的内存地址是:%p\n", &p) go func(p chan bool) { fmt.Printf("函数里接收到chan的内存地址是:%p\n", &p) //模拟耗时 time.Sleep(2 * time.Second) p <- true }(p) select { case l := <-p: fmt.Println(l) } }运行结果=== RUN TestChanReferencr 原始chan的内存地址是:0xc000120028 函数里接收到chan的内存地址是:0xc000120030 true --- PASS: TestChanReferencr (2.00s) PASS看到实参和形参地址返回的是不一样的,chan 的底层实现// src/runtime/chan.go func makechan(t *chantype, size int) *hchan { elem := t.elem // compiler checks this but be safe. if elem.size >= 1<<16 { throw("makechan: invalid channel element type") } if hchanSize%maxAlign != 0 || elem.align > maxAlign { throw("makechan: bad alignment") } mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range")) }其实可以看到 和 map 有点类型,通过 mak 函数,返回的也是一个 hchan 类型的指针,实际上在操作中,传递的是指针的副本。属于值传递。struct 是值传递么?看个例子type Persons struct { Name string Age int64 } func TestStructRerence(t *testing.T) { per := Persons{ Name: "asong", Age: int64(8), } fmt.Printf("原始struct地址是:%p\n", &per) modifiedAge2(per) fmt.Println(per) } func modifiedAge2(per Persons) { fmt.Printf("函数里接收到struct的内存地址是:%p\n", &per) per.Age = 10 }运行结果:=== RUN TestStructRerence 原始struct地址是:0xc00000c048 函数里接收到struct的内存地址是:0xc00000c060 {asong 8} --- PASS: TestStructRerence (0.00s) PASS可以看到 struct 就是值传递, 没有指针发现没?当你修改指为10 的时候,发现没有修改成功,原来 struct 中 Age 值,还是 8总结Go 语言中的参数传递都是值传递,虽然 Go 语言中都是值传递,但我们还是可以修改原参数中的内容的,因此传递的参数是引用类型。
实参与形参数func printNumber(args ...int)形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。func main() { var args int64= 1 printNumber(args) // args就是实际参数 } func printNumber(args ...int64) { //这里定义的args就是形式参数 for _,arg := range args{ fmt.Println(arg) } }什么是值传递什么是值传递,传递的是值,也就是说,函数传递的原来数据的拷贝,一个副本,比如当传递一个 int 类型的参数,传递的其实是这个参数的一个副本。传递一个指针类型的参数,其实传递的就是这个指针类型的拷贝,而不是这个指针执行的值。什么是引用传递引用传递是指在函数调用过程中,是将实际参数的地址传递到函数中, 然后在函数中对参数进行修改,会影响到实际值。golang是值传递package gobase import ( "fmt" "testing" ) func TestReference(t *testing.T) { var args int64 = 1 modifiedNumber(args) // args就是实际参数 fmt.Printf("实际参数的地址 %p\n", &args) fmt.Printf("改动后的值是 %d\n", args) } func modifiedNumber(args int64) { //这里定义的args就是形式参数 fmt.Printf("形参地址 %p \n", &args) args = 10 }运行结果:=== RUN TestReference 形参地址 0xc0000a01c8 实际参数的地址 0xc0000a01c0 改动后的值是 1 --- PASS: TestReference (0.00s) PASS上面运行结果可以看出,go 是值传递, 但是不能确定 Go 是否只有值传递,也没有引用传递?传递一个引用参数类型测试一下:func TestReference2(t *testing.T) { var args int64= 1 addr := &args fmt.Printf("原始指针的内存地址是 %p\n", addr) fmt.Printf("指针变量addr存放的地址 %p\n", &addr) modifiedNumber2(addr) // args就是实际参数 fmt.Printf("改动后的值是 %d\n",args) } func modifiedNumber2(addr *int64) { //这里定义的args就是形式参数 fmt.Printf("形参地址 %p \n",&addr) *addr = 10 }运行结果=== RUN TestReference2 原始指针的内存地址是 0xc0001221d0 指针变量addr存放的地址 0xc00012c028 形参地址 0xc00012c030 改动后的值是 10 --- PASS: TestReference2 (0.00s) PASS通过输出可以看到,这个是指针拷贝, 存放两个指针到地址是不一样的, 虽然指针的内容相同,但是两个是不一样的指针。我们声明了一个变量 args , 其值为1, 内存存放地址为 0xc000b4008 , 通过这个地址可以找到变量 args, 这个地址是 args 的指针 addr, 指针adrr其实也是一个指针类型的变量。需要内存来存放它, 对应的内存地址是 0xc000ae18, 指针存放的内容是 0xc000b4008, 在我们传递指针变量 addr 给 modifedNumber 的时候,该指针变量是指针的一个拷贝, 新拷贝的指针变量存放地址是 0xc0000ae028 ,对应存放的内容还是 0xc000b4008, 这个指向了 args 变量的, 于是可以修改 0xc000b4008 指针对应的值,也就是修改了 args 对应的值。