site logo

Keep doing what you want, never give up.

推荐文章
文章配图

大厂面试 - 腾讯: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接口: package os func (f *Fule) Write(p []byte) (n int, err error) { // 具体实现细节 } 这样我们就可以用下面的代码来使用 io.Writer 接口: func main() { var writer io.Writer f, err := os.Create("example.txt") if err != nil { return } defer f.Close() // 将打开的文件赋值给writer变量 writer = f // 使用writer向文件中写入数据 _, err := writer.Write([]byte("Hello World!")) if err != nil { return } }
文章配图

面试官:说说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,所以他们之间的加密通信内容也能被中间人一览无余了。
文章配图

苹果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
文章配图

Google SEO 通过 HTTP 410 永久移除某个URL索引

我们可以通过Nginx配置来实现Google SEO,永久删除那些已经被Google收录但却是错误的url网址,比如网址被谷歌收录后又做了变更改变了原来的url地址。 又或者移除Google之前收录的非HTTPS协议网址等。 按如下方式在Nginx的server段内返回HTTP 410状态码即可: location /path/to/deleted/article { return 410; } HTTP状态码说明: HTTP 410 Gone 说明请求的目标资源在源服务器上已经不存在了,并且是【永久性】的丢失。 如果不清楚是【永久丢失】还是【临时丢失】,则应该使用404状态码。
文章配图

运行Github上Go包中example示例的正确姿势

日常开发中我们经常会使用到第三方Go包,而很多Go包中默认都包含了一些example示例代码来帮助用户直观的感受产品运行效果,那我们应该怎么运行这些代码呢? 以前都是一股脑儿的git clone整个仓库然后切换到example目录运行相关代码来看效果,除了这种方法外,我们还可以使用Go为我们提供的go list工具链命令来实现同样的效果。 这里以github.com/gorilla/websocket这个Web Socket包为例,官方示例代码如下: 1 go get github.com/gorilla/websocket 2 cd `go list -f '{{.Dir}}' github.com/gorilla/websocket/examples/chat` 3 go run *.go 这里主要解释下第二行代码的含义: # cd反引号(``)表示命令替换,它会先执行反引号中的命令,然后将命令代码的返回结果替换到命令行中 cd `命令代码` # go list命令的作用是列出包的相关信息 go list github.com/gorilla/websocket/examples/chat 给go list加上 -f ‘{{ .Dir }}‘参数是指定了go list的输出格式,这里使用了Go模版语法,’{{ .Dir }}‘表示输出包的目录路径 # go list命令的作用是列出包的相关信息 go list -f '{{ .Dir }}' github.com/gorilla/websocket/examples/chat 所以,这段代码整体的作用就是获取github.com/gorilla/websocket/examples/chat这个Go包的目录里路径,并切换到这个目录下,然后可以直接在改目录下运行相关go代码了。
文章配图

我用Tailwind CSS写了一个程序员简历模版

对于一个后端程序员来说,Tailwind CSS无疑是构建前端UI页面的大杀器,其简洁的语法和所见即所得的实时预览效果极大的提高了前端页面的开发效率,让你用写HTML的方式书写CSS的核心思想简直如行云流水般丝滑。 如下便是用Tailwind CSS构建的一个简历模版,最终效果如下图: 更多手写Tailwind CSS模版可见 此处 ,后续会持续更新。
文章配图

MongoDB聚合查询aggregate之group分组

MongoDB中并没有类似MySQL关系型数据库中Left Join这种连表查询,但是它提供了一个称为aggregate的聚合管道查询语法来让我们实现类似MySQL那样的关联查询。其大致思路就是将多个查询语句按顺序逐一执行,然后汇总结果。 假设我们有两个集合,一个用户集合用来存放用户信息名字叫user,另外一个集合名叫photo用来存放用户的照片信息。 用户集合user ------------------------- | uid | name | |-----------|-----------| | 100000 | A | | 100001 | B | | 100002 | C | ------------------------- 照片集合photo ----------------------------- | uid | photo | |-----------|---------------| | 100000 | A的照片一 | | 100001 | B的照片一 | | 100000 | A的照片二 | | 100002 | C的照片一 | ----------------------------- 现在的需求是查出user表中所有用户的照片集合,相同用户的照片需要合并在一个数组中: 查询结果 ------------------------------------------------- | uid | name | photo | |-----------|---------------|-------------------| | 100000 | A | A的照片一,A的照片二 | | 100001 | B | B的照片一 | | 100002 | C | C的照片一 | ------------------------------------------------- 这时我们就可以联合aggregate语句的$lookup、$unwind、 $project以及$group语法得出想要的结果,查询语句如下:
文章配图

如何解决Nginx无法按天记录访问日志的问题?

Nginx的map指令用于将一个变量的值映射给另外一个变量。它就像是一个“查字典”的工具,根据输入的值来查找和返回对应的输出值,我们可以用它来配置一些复杂的规则以简化Nginx配置的条件判断。 一个典型的map应用案例便是用它按日期(通常是按天)来切割Nginx的访问日志,相关的配置网络上已经有很多说明了,也都一样像下面这样配置即可: # 指定Nginx工作进程的用户或组 user nobody bobody; user nobody; # 定义日志记录的内容格式 log_format main '$remote_addr [$time_iso8601] $request ' '$status $body_bytes_sent $http_referer ' '$http_user_agent $http_x_forwarded_for $request_time'; # 将Nginx内置时间变量 $time_iso8601 的值映射到新定义的 $logday 变量 map $time_iso8601 $logday { '~^(?<ymd>\d{4}-\d{2}-\d{2})' $ymd; default 'date-not-found'; } # 在日志文件名中加入 $logday 日期 access_log logs/access-$logday.log main; 但不一样的是,可能即使你按照上面的配置修改并重启了nginx,但发现nginx并没有像我们希望的那样按日期生成日志,如果你删除之前logs目录中已存在的日志文件并成功执行nginx -s reload后甚至会出现压根就没有生成任何日志文件。 而之所以出现这种现象的原因其实是因为生成日志的nginx进程权限不够,缺少对logs目录的写权限。 知道了原因,那解决方法也非常简单,给logs目录赋予相应的写权限即可: sudo chown -R nobody:nobody sudo chmod -R 755 logs 有了正确的权限Nginx便能像我们期望的那样按天记录日志了。
文章配图

Linux免密Clone不同的Github仓库代码

个人和小微企业由于资金紧张,所以大部分都只有一台服务器来跑所有的服务,如果不同的服务是存储在不同的github仓库的话,那就需要将不同的github仓库代码clone到同一台服务器的不同目录。 要实现这样的需求其实我们只需要实现将服务器ssh公钥添加到github仓库的Deploy Key中即可,但是一个ssh key只能被添加一次,如果你将同一个key添加到不同仓库的Deploy Key中github会提示“Key is already in use” 解决办法就时我们生成多个ssh key密钥对,然后让git克隆不同仓库的代码时使用不同的公钥即可,操作步骤如下: 1、生成多个ssh密钥对(为简单起见我们生成两个,一路回车即可): ssh-keygen -t rsa -f ~/.ssh/id_rsa_仓库1 ssh-keygen -t rsa -f ~/.ssh/id_rsa_仓库2 2、添加不同公钥到对应仓库的Deploy Key中: cat ~/.ssh/id_rsa_仓库1.pub cat ~/.ssh/id_rsa_仓库2.pub 3、告诉git工具不同的仓库要使用不同的公钥,这需要通过修改 ~/.ssh/confg文件来实习: Host github-仓库HOST别名1 HostName github.com User git IdentityFile ~/.ssh/id_rsa_仓库1的私钥 Host github-仓库HOST别名2 HostName github.com User git IdentityFile ~/.ssh/id_rsa_仓库2的私钥 最后就可以使用git进行免密操作了(注意git@后面的主机名其实是上面在~/.ssh/confg中添加的别名): git clone git@github-仓库HOST别名1:username/仓库1.git git clone git@github-仓库HOST别名2:username/仓库2.git
文章配图

面试官:Go module目录名中为什么有叹号!特殊字符

如下图,为什么有的Go module目录名包含叹号!这个特殊字符? 在Go官方文档中有提到说module路径是采用的操作系统OS的方式来识别的,但是我们不能依靠文件系统将 rsc.io/QUOTE 和 rsc.io/quote 分开。因为Windows 和 macOS 不这样做。 换句话说就是操作系统不区分目录大小写,于是Go团队就想出了一个使用!叹号替换大写字母的解决方案,具体替换方案是将大写字母替换为一个!叹号和这个大写字母对应的小写字母,比如如下截图中的Abirdcfly目录就被替换成了 !abirdcfly