site logo

Keep doing what you want, never give up.

推荐文章
文章配图

大厂面试 - 拼多多:Go字典Map

1、Go语言中的Map是有序的还是无序的,为什么? 在Go语言中,字典Map是无序的。 因此,如果使用 for … range 循环遍历的话每次迭代获得的 键 - 元素 对的顺序都可能是不同的。 在Go的内部实现中是先通过一个叫做fastrand()的函数生成一个随机数,然后使用这个随机数作为一个桶位置当作起始点开始进行遍历迭代的,这就导致每次遍历的起始位置不是固定的。 2、Map字典的键类型不能是哪些类型,应该优先考虑哪些类型作为字典的键类型呢? Go 语言Map字典的键类型不可以是函数类型、字典类型和切片类型。 因为 Go 语言的字典类型其实是一个哈希表(hash table)的特定实现,因此其中重要的一个步骤就是需要把键值转换为哈希值,所以,从性能的角度来看的话,“把键值转换为哈希值”以及“把要查找的键值与哈希桶中的键值做对比”, 都是比较耗时的操作。 因此,可以说,求哈希和判等操作的速度越快,对应的类型就越适合作为键类型。 3、在值为nil的字典上执行读操作会成功吗? 由于字典是引用类型,所以当我们仅声明而不初始化这个字典类型的变量时,这个变量的值就会是nil。 我们在这样一个值为nil的字典上执行读操作是没有问题的,但是如果是添加键 - 元素对的话,那Go 语言的运行时系统就会立即抛出一个 panic 致命性错误。
文章配图

大厂面试 - 腾讯:Go线程实现模型中的GMP

1、解释下G、M、P分别代表什么? G:是goroutine的缩写,一个G代表一个Go代码片段; M:是machine的缩写,一个M代表一个内核线程,也称工作线程; P:是processor的缩写,一个P代表执行一个Go代码片段所必须的资源; 2、使用go关键字生成的goroutine是放置在P中,还是M中? 当一个G被创建并初始化完成后会立即被存储到本地P的runnext字段中,因为G必须先被添加入到P的可以运行G队列中才能在M中运行。
文章配图

Go语言编程性能优化小技巧

虽然 Go 语言是一个高性能的编程语言,但有些情况下我们还是需要关心它的性能优化。 下面便是一些常见的Go性能优化小提示: 字符串处理相关: 使用StringBuffer 或是StringBuild 来拼接字符串,性能会比使用 + 或 +=高三到四个数量级。 如果需要把数字转换成字符串,使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。 尽可能避免把String转成[]Byte ,这个转换会导致性能下降。 内存分配相关: 如果在 for-loop 里对某个 Slice 使用 append(),请先把 Slice 的容量扩充到位,这样可以避免内存重新分配以及系统自动按 2 的 N 次方幂进行扩展但又用不到的情况,从而避免浪费内存。 GC相关: 避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能使用 sync.Pool 来重用对象。 锁相关: 使用 lock-free 的操作,避免使用 mutex,尽可能使用 sync/Atomic包。 IO相关: 使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用 bufio.NewWrite() 和 bufio.NewReader() 可以带来更高的性能。 循环相关: 对于在 for-loop 里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会提升两个数量级。 协议相关: 如果你需要更高性能的协议,可以考虑使用 protobuf 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。 字典相关: 你在使用 Map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。
文章配图

大厂面试 - 贝壳:JWT是什么?

JWT(JSON Web Token)是一种用于在网络环境中进行安全信息传递的开放标准 完整的JWT由三部分组成,其结构可以表示为xxxxxx.yyyyyy.zzzzzz格式。 头部(Header): 通常包含令牌的类型和加密算法; 载荷(Papload): 包含自定义的数据如用户信息,权限等; 签名(Signature): 作用是用来验证消息的完整性和真实性,防止消息被篡改; JWT是无状态的,服务器不需要为其保存会话信息,从而减轻了服务器的存储压力,但同时也意味着一旦泄露任何请求都会被允许,而常用的解决方法是给JWT设置不固定的过期时间,强制客户端过期重新获取。 JWT另一个特点是,一旦生成后在其过期前都不可再修改其有效期。 它的主要应用场景包括用户身份验证和授权,用户在登录成功后获取JWT后,后续的所有请求都需要携带该JWT向服务器请求资源,服务器则可以根据这个JWT来验证用户的有效性。
文章配图

大厂面试 - 贝壳:MySQL索引的最左前缀原则

1、MySQL索引的最左前缀原则是什么意思?如果有查询条件为 a > 1 and b = 1 and c = 1,请问这个查询能命中MySQL索引吗? 索引的最左前缀原则是指查询语句要使用索引的条件需要满足从关联索引的最左侧开始且需要连续匹配。 比如,如果创建联合索引的顺序是(a, b, c)则上面的查询条件就可以命中索引,但如果创建y索引的顺序是(b,a, c)或者其他顺序,则上面的查询语句则无法使用索引。 2、使用explain工具分析慢查询的时候需要关注哪些信息? 首先关注“type”项,如改项显示为“ALL”则意味着查询使用了全标扫描,这是最差的情况,必须进行优化。如果改项的值为“index”或“range”等则相对较好。 还要注意“row”这项,它代表预计扫描的行数,如果改值较大,即使查询使用到了索引也需要进行优化。 此外,“possible_keys”和“key”这两项也非常重要,前者表示可能使用到的索引,后者表示查询实际使用的索引。如果改项值为空则意味着未使用索引,需要立即优化。
文章配图

大厂面试 - 腾讯:Go结构体struct及interface接口

1、什么情况下会使用空struct,它会分配内存吗? 空结构体 struct{} 是一个特殊的结构体,它没有字段属性,一些常见的使用场景包括:集合、实现接口、信号传递。 Go语言没有内置的集合类型,但是我们可以使用map[T]struct{}来模拟集合,因为struct{}不占用空间,因此只需要存储键,能节省内存。 空结构体还可以用来实现没有方法的接口,这中接口通常用于占位符或标记的用途。 type NoMethodInterface interface { } type EmptyStruct struct{} type AnotherEmptyStruct struct{} func process(i NoMethodInterface) { // 什么也不干,占位,将来可以扩展 } // 因为NoMethodInterface接口没有任何方法,所以任何类型都被认为是这个接口的实现,包括空结构体EmptyStruct func main() { var empty EmptyStruct var another AnotherEmptyStruct // 同一接口的两种不同的实现方式 process(empty) process(another) } 在Go的并发编程中,使用 chan struct{} 来传递信号,由于 struct{} 不占用内存,因此可以高效的用于信号传递而无需携带任何额外的数据。 最后,空结构体 struct{} 在实例化的时候并不会分配内存,每个空结构体的实例在内存中占用0个字节,即使在一个集合中存储了多个struct{}实例,它们也不会占用额外的内存空间而只会占用集合本身存储键所需要的内存空间。 2、Go中接口有什么用,如何实现? 接口的精髓在于抽象和多态度,接口允许我们定义一个行为的集合,而不关心实现这些行为的具体类型,这样我们就可以编写更加抽象的代码,当我们需要改变具体实现时,不需要修改使用接口的代码,对调用者做到了透明。接口使得不同类型可以实现相同的方法,从而通过接口类型的变量来引用不同的具体实现,这种特性大大增加了代码的灵活性和可扩展性。此外,接口还有利于代码解耦,它定义的是一组行为而不是具体实现,使得不同实现可以通过接口进行通信,从而减少了模块之间的耦合。 Go接口的实现非常简单,只需要实现接口中定义的所有方法即可。 Go源码中最通俗易懂的实现之一就是标准库中的 io.Writer 接口: package io type Writer interface { Write(p []byte) (n int, err error) } io.Writer接口非常简洁,只有一个Write方法,任何实现了Write方法的类型都可以被视为io.Writer,例如标准库中的os.File类型就实现了Writer接口:
文章配图

大厂面试 - 腾讯:如何避免Go内存逃逸

什么是内存逃逸? 内存逃逸是一种程序错误,它指程序在运行过程中分配了内存,但是之后没有正确的释放这些被分配的内存,从而导致这些内存一直被占用,这就有可能会引起系统内存耗尽,从而导致程序崩溃。 我们都知道程序运行的时候需要将其拷贝到内存中才能执行,而内存空间划分成了不同的区域,其中最重要的两个区域是:堆区 - Heap 和 栈区 - Stack 为什么要分成堆区和栈区呢? 主要是为了满足不同的编程需求: 1、效率:栈区自动管理,适合快速的内存分配和释放,而堆区则适合存储需要长时间存在的大量数据。 2、灵活性:堆区基本可以随心所欲动态的分配内存,适合需要灵活使用大块内存的情况,而栈区刚好相反适合局部短时间使用的情况。 3、内存管理:有助于更好的管理和优化内存使用,提高程序的性能和稳定性。 栈区:用来存放函数调用时的局部变量和函数调用信息的;一般是由系统自动管理的,当函数调用开始时分配内存,函数调用结束时自动释放内存;栈区的分配和释放速度都非常快;栈区一般是有大小限制的,所以如果函数嵌套调用过多,或者局部变量太多也可能会导致栈溢出。 堆区:用于存储动态分配的内存(比如使用new或者malloc等函数分配的内存);特别是C语言中是需要一由程序员手动管理分配和释放的;因为是手动管理分配和释放,所以速度一般比较慢;堆区的大小一般来说比栈区大,可以存储更多的数据。 分配快慢的问题: 栈分配内存只需要用到两个CPU指令,即PUSH分配和RELEASE释放; 堆分配内存首先需要找到一块大小合适的内存块,之后还要通过垃圾回收才能释放。 Go语言为了解决内存逃逸实现了一套内存逃逸分析,所谓逃逸分析就是指程序在编译阶段对代码中哪些变量需要在栈中分配,哪些变量需要在堆上分配进行静态分析的方法。逃逸分析的目的是做到更好内存释放分配,提高程序的运行效率。 Go逃逸分析的源码位于src/cmd/compile/internal/gc/escape.go,感兴趣的可以自行研究,其逃逸分析原理在源的注释中已有说明: pointers to stack objects cannot be stored in the heap - 指向栈对象的指针不能存储在堆中 pointers to a stack object cannot outlive that object - 指向栈对象的指针不能超过该对象的生命周期 知道了以上知识点,我们再看如何避免Go内存逃逸? 1、避免返回函数内部的局部指针变量,比如下面这段代码就会产生内存逃逸: func Sum(a int, b int) *int { total := a + b return &total } func main() { Sum(200, 50) } 2、避免使用interface类型作为函数参数,interface在Go中代表任意,也就代表了不确定性。 3、避免闭包产生逃逸,因为闭包函数也是一个指针。
文章配图

面试官:说说HTTPS的Man-in-the-Middle 中间人攻击

顾名思义,中间人攻击就是指通信双方的对话内容被第三个人窃取了。 回想抗日谍战剧里潜伏在敌人内部的情报员是如何将获得的重要情报传递给后方根据地的? 情报员获得情报后通过某种加密手段将情报内容加密后再传递给根据地,根据地同志在获得了情报后再用事先商量好的密钥解密情报以获取情报的真正内容。 该过程的优势是即使敌人获取到了被加密的情报也无法获取情报里真正的内容,但是,如果敌人知道了我们的加解密方法的话那就很容易知道情报的内容了。 中间人攻击就类似上图一样,左右两边的用户聊得正嗨,却不知道他们的聊天内容正在被头顶的人偷窥呢。 现在回过头来说HTTPS中间人攻击,既然HTTPS是加密通信,那中间人是如何窃取到对话信息的呢? 这就需要了解HTTPS加密的过程了,我们假设小明和小花之间的对话采用了某种加密方法,在他们之间传递的都是密文,他们拿到密文后再各自用事先约定好的密钥来解密,当然这个密钥也是通过网络传递的,不是小明事先亲手偷偷的交给小花的,正常情况下的通信过程是: 1、小花把自己的公钥public key 1发送给小明; 2、小明用这个公钥 public key 1加密他和小花通信的真正密钥S,加密后叫S1; 3、小明把S1发送给小花; 4、小花用自己的私钥private key 1解密S1,,现在小花得到了S; 5、后续小明和小花就用这个密钥S进行通信了。 现在假设有一个中间人,也不知道他用什么方法拿到了小花的公钥,现在小明和小花的通信过程变成了下面这样: 1、小花在把自己的公钥public key 1传递给小明的时候被中间人调包了; 2、中间人把自己的公钥public key 2发给了小明; 3、小明现在有中间人给的公钥public key 2; 4、小明用这个中间人公钥 public key 2加密他和小花通信的真正密钥S,加密后叫S1; 5、小明把S1发送给小花,实际发送给了中间人; 6、中间人用自己的私钥private key 2解密了S1获得了S; 7、中间人拿到S后再用小花的公钥public key 1重新加密S得到S2; 8、中间人把S2发给了小花; 9、小花用自己的私钥private key 1解密S2,,现在小花得到了S; 10、后续小明和小花就用这个密钥S进行通信了; 但是因为此时中间人已经拥有了小明和小花的真正密钥S,所以他们之间的加密通信内容也能被中间人一览无余了。
文章配图

Hugo目录中的index.md与_index.md有什么区别?

要解释index.md与_index.md的区别,需要先弄清楚一个叫Page Bundle的名词。按Hugo官方的解释,Page Bundle就是一个“页面包裹”,把一个页面的内容和相关资源打包在一起,通俗的理解就是用一个目录来管理文章和图片等资源,把一篇文章和文章中的配图一起放在某个目录下,这个目录就是一个Page Bundle。 Page Bundle又分成了两大类,类比一棵树,一种叫叶子Page Budle,另外一种叫分支或树干Page Bundle。那怎么来区分一个Page Bundle到底是树叶,还是树干呢? 这个时候就轮到index.md和_index.md出场了,如果一个目录下包含_index.md文件的就是树干,反之只包含了index.md的目录就是树叶。 content/ ├── leaf/ │ ├── index.md # 表明leaf目录是leaf bundle │ └── leaf.jpg └── branch/ ├── _index.md # 表明branch目录是branch bundle └── branch.jpg 我们都知道树叶是一颗树的最末端,即树叶上不可能再长树干了,同理,包含index.md的目录也不能包含子目录了;而树干是可以继续长出树干和叶子的,所以类比到包含_index.md的目录,它是可以继续嵌套其它目录和子目录的。 Hugo页面有个方法叫 .Resources,这个方法可以提取出页面相关的所有资源,包括图片、音视频等,它提取的资源就是从对应的Page Bundle中提取出来的。 以上就是index.md和_index.md的主要区别与作用。
文章配图

苹果iOS应用内购买(IAP)服务端通知解析:全网最详细教程

虽然通过苹果的StoreKit2框架客户端可以直接处理整个支付流程,不再需要验证票据了,但是绝大多数业务场景都需要再服务器端再次验证支付逻辑,这时我们就需要解析苹果服务器到服务器到支付通知了,这里只说明通知数据结构的解析,具体可以分为如下三步来实现: 第1步:苹果服务器POST过来的JSON数据: { "signedPayload": "eyJhbGciOiJF这里是被加密的内容eyJhbGciOiJF" } 第2步:按照苹果官方提供的方法提取出signedPayload的内容如下: { "notificationType" : "DID_RENEW", "subtype" : "子类型", "notificationUUID" : "f2d65c0c-4980-4211-9d02-d104959a468e", "data" { "appAppleId" : 706*****38, "bundleId" : "App的Bundle Id标志符", "bundleVersion" : "20230506165910", "environment" : "Sandbox", "signedTransactionInfo" : "eyJhbGc加密的交易信息zV1Q", "signedRenewalInfo":"eyJhbGciO加密的续费信息giG29hdGA", "status" : 1 }, "version" : "2.0", "signedDate" : 1717485836523 } 其中notificationType字段表示通知的具体类型,共有18种可能的情况 : 1、CONSUMPTION_REQUEST - 消费者发起了一个【退款/自动续期订阅】请求 2、DID_CHANGE_RENEWAL_PREF - 结合subtype字段【UPGRADE/DOWNGRADE】决定消费者修改了订阅计划,如果subtype字段为空表示该回至当前状态 3、DID_CHANGE_RENEWAL_STATUS - 结合subtype字段【AUTO_RENEW_ENABLED/AUTO_RENEW_DISABLED】一起决定订阅续期状态变化,如果用户发起了退款请求则App Store也会禁用自动续期 4、DID_FAIL_TO_RENEW - 结合subtype字段【GRACE_PERIOD/空】一起决定服务端是否可以停服务了,如还处在GRACE_PERIOD宽限期内不要停服务,否则可以收回服务了 5、DID_RENEW - 结合subtype字段【BILLING_RECOVERY/空】一起决定服务端是否可以提供服务了,subtype值为BILLING_RECOVERY表示上次过期的订阅自动续期成了,为空表示一个新的自动续期订阅成功了 6、EXPIRED - 结合subtype字段【VOLUNTARY/BILLING_RETRY/PRICE_INCREASE/PRODUCT_NOT_FOR_SALE/空】一起决定订阅过期原因,分别表示用户自主取消/上一个账单自动续期失败/用户不同意涨价/商品不再处于销售状态/其它原因 7、EXTERNAL_PURCHASE_TOKEN - 结合subtype字段【EXTERNAL_PURCHASE_TOKEN】仅启用了外部支付才有此值 8、GRACE_PERIOD_EXPIRED - 宽限期已结束还是无法扣款成功,服务端可以取消服务了 9、OFFER_REDEEMED - 结合subtype字段【INITIAL_BUY/RESUBSCRIBE/UPGRADE/DOWNGRADE/空】一起决定消费者使用了促销优惠或优惠码,分别表示第一次使用优惠码/非首次使用优惠码/用优惠码升级/用优惠码降级/用优惠码再次购买当前已买且有效的服务 10、PRICE_INCREASE - 结合subtype字段【PENDING/ACCEPTED】一起决定消费者是否同意服务涨价,分别表示待处理/接受涨价 11、REFUND - 表明App Store已经成功处理了一笔退款 12、REFUND_DECLINED - 表明App Store拒绝了开发发起的退款请求 13、REFUND_REVERSED - 表明App Store撤销了之前因消费者争议而产生的退款请求,需要继续提供服务 14、RENEWAL_EXTENDED - 表明App Store扩展了某个订阅的续订日期 15、RENEWAL_EXTENSION - 结合subtype字段【SUMMARY/FAILURE】一起决定App Store正尝试调用为所有活动订阅者延长订阅日期 16、REVOKE - 表明家庭共享服务关闭,不可再共享了 17、SUBSCRIBED - 结合subtype字段【INITIAL_BUY/RESUBSCRIBE】一起决定消费者订阅了一个产品,分别表示第一次订阅或通过家庭共享访问/非第一次 18、TEST - 表明App Store发送了一个测试通知,你请求了Test Notification API