golang北京小厂面试复盘

下面是一家小厂的golang研发岗面试题,薪酬范围15k-20k,面试时间是2022-3-12,希望能帮到大家


 

1.自报家门部分

面试官:做一下自我介绍,做过golang的项目的?简单聊聊项目

参考:无

 


 

2.golang基础部分

面试官:golang的数据类型有哪些?

参考:go分基础数据类型和派生数据类型。基础数据类型有数字型(int,uint,float,complex,rune(类似int32,常用作处理汉字),byte(类似uint8),uintptr(存放一个指针),字符型,布尔型,字符串型,派生数据类型有指针,数组,结构体,管道,函数(居然也是数据类型),接口,切片,map。

补充:uintptr和unsafe.Pointer的区别

  • unsafe.Pointer类型可以和任意指针类型互转
  • uintptr可以做指针运算,这一点有时候很重要,但是依赖平台,同一类型变量在不同的平台占用的存储空间大小不一样,在用uintptr做指针运算的时候,偏移量也会相应的不一样(后面有例子说明)。
type MyStruct struct {
    i int
    j int
}

func myFunction(ms *MyStruct) {
    ptr := unsafe.Pointer(ms)
    for i := 0; i < 2; i++ {
        c := (*int)(unsafe.Pointer((uintptr(ptr) + uintptr(8*i))))
        *c += i + 1
        fmt.Printf("[%p] %d\n", c, *c)
    }
}

func main() {
    a := &MyStruct{i: 40, j: 50}
    myFunction(a)
    fmt.Printf("[%p] %v\n", a, a)
}

  运行结果:

 

面试官:切片和数组的区别是什么?

参考:1.数组是定长的,切片可以扩从长度

2.数组是值类型,切片是引用类型,切片的底层是数组,切片底层的数据结构有三个字段,分别是数组指针,len,cap。

虽然go只有值传递,但是切片由于存放了数组指针,所以传切片时,底层数组的改变会带动切片本身的改变

3.数字声明完就可以使用,而切片需要内置函数make()后才会分配内存

面试官:结构体可以比较吗?

参考:分情况,如果结构体中不含有不可比较的数据类型,那么结构体就可以比较,反之亦然。其中,

不可比较的数据类型有:SliceMapFunction

可比较的数据类型有:IntegerFloating-pointStringBooleanComplex(复数型)PointerChannel(小心这个)InterfaceArray

面试官:那不可比较的结构体有没有什么方法比较呢?

参考:可以使用 reflect.DeepEqual 进行比较,DeepEqual函数用来判断两个值是否深度一致,不同类型的值永远不会深度相等。

参考链接:https://www.cnblogs.com/dashu-saycode/p/14286228.html

面试官:map为什么无序?什么方法可以变得有序?

参考:map无序有两点原因:1.map在遍历时,并不是从0号桶开始遍历的,每次遍历会从随机一个桶的随机一个cell开始遍历

2.map遍历时,按序遍历bucket,同时按序遍历buckct中和其overflow bucket中的cell,但是map扩容后,会发生key的搬迁,这造成原来在某个bucket中的key可能去了别的bucket,因此,mao遍历无序

什么方法变得有序:利用slice对map的key排序,而后进行遍历

面试官:map是线程安全的吗?

参考:不是,map和slice都不是线程安全的,而且多个线程同时访问时,map会报错(slice不会),如果想要实现线程安全,可以使用读写锁,

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

counter.RLock()
n := counter.m["煎鱼"]
counter.RUnlock()
fmt.Println("煎鱼:", n)

  也可以使用sync.Map,它采取了 “空间换时间” 的机制,冗余了两个数据结构,分别是:read 和 dirty,减少加锁对性能的影响。他适合读多写少的场景,若出现写多/并发多的场景,会冲突变多,性能急剧下降。

面试官:了解defer吗?

参考:defer常用作数据库连接,文件句柄关闭,他会逆序执行(类似于栈),并在函数退出时释放资源

面试官:了解select吗?select和switch有什么区别?

参考:

  1. select语句只能用于信道的读写操作,
  2. 每个 case 都必须是一个通信
  3. select中的case条件(非阻塞)是并发执行的,如果多个case同时满足,公平的选择一个,其他被忽略。如果一个也没有满足,进入default,没有default,select 将阻塞。
  4. 对于空的select{},会引起死锁,对于for中的select{}, 也有可能会引起cpu占用过高的问题,对于case条件语句中,如果存在信道值为nil的读写操作,则该分支将被忽略,可以理解为从select语句中删除了这个case语句。

         select和switch只是从结构上看着相似,其实用法大不相同,switch只会选择一个满足且仅满足的case条件执行,而 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作

面试官:讲一讲协程

参考:Go 协程是与其他函数或方法一起并发运行的函数或方法。Go 协程可以看作是轻量级线程。与线程相比,创建一个 Go 协程的成本很小。因此在 Go 应用中,常常会看到有数以千计的 Go 协程并发地运行。

面试官:那协程比线程有哪些优势?

参考:

  • 相比线程而言,Go 协程的成本极低。堆栈大小只有若干 kb,并且可以根据应用的需求进行增减。而线程必须指定堆栈的大小,其堆栈是固定不变的。
  • Go 协程会复用(Multiplex)数量更少的 OS 线程。即使程序有数以千计的 Go 协程,也可能只有一个线程。如果该线程中的某一 Go 协程发生了阻塞(比如说等待用户输入),那么系统会再创建一个 OS 线程,并把其余 Go 协程都移动到这个新的 OS 线程。所有这一切都在运行时进行,作为程序员,我们没有直接面临这些复杂的细节,而是有一个简洁的 API 来处理并发。
  • Go 协程使用信道(Channel)来进行通信。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是 Go 协程之间通信的管道。

 3.mysql基础部分

面试官:mysql连接查询你知道多少?

参考:交叉连接(笛卡尔连接),内连接,外连接(左连接和右连接),多表连接查询

笛卡尔连接:交叉连接,

select*from line,vehicle

内连接:默认的连接类型,只有满足连接条件的记录才能出现在查询结果中

SElECT fieldlist FROM table1 【INNER】JOIN table2 ON 
table1.column1=table2.column2 【where condition】

左连接:结果集包括左表的所有记录和右表中满足连接条件的记录

left join

右连接:结果集包括右表的所有记录和左表中满足连换条件的记录

right join

面试官:左连接和右连接的区别是什么?

左连接   (left join)
    
select *  from table1 left join tbale2 on table1.id=table2.id
这条sql语句返回结果   table1表中的数据全部返回   table2表中的数据只返回满足where条件的(左表数据全部返回)
 
右链接   (right join)
select * from table1 right join table2 on table1.id=table2.id
这条sql语句返回结果   table2表中的数据全部返回    table1表中的数据只返回满足where条件的(右表数据全部返回)

面试官:mysql的锁有哪些?什么区别?

参考:按照对数据操作的锁粒度来分,分为行级锁,间隙锁,临键锁,页级锁,表级锁。

行级锁:表示只针对当前操作的行进行加锁

间隙锁:锁定一个范围,但不包括记录本身.间隙锁的目的是为了让其他事务无法在间隙中新增数据,防止同一事务的两次当前读

临键锁:它是记录锁和间隙锁的结合,锁定一个范围,并且锁定记录本身

页级锁:采取了折中的页级锁,一次锁定相邻的一组记录

表级锁:mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分mysql引擎支持

按照共享策略:可以分为共享锁,排它锁,意向共享锁,意向排它锁

读锁(共享锁):Shared Locks(S锁),针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁):Exclusive Locks(X锁),当前写操作没有完成前,它会阻断其他写锁和读锁
IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
IX锁:意向排他锁、Intention Exclusive Lock。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。

加锁策略上分:乐观锁和悲观锁

悲观锁认为对于同一个数据的并发操作,一定是会发生修改的(或者增删改多,查少),哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。

乐观锁:则认为对于同一个数据的并发操作,是不会发生修改的(或者增删改少,查多)。在更新数据的时候,会采用不断尝试更新的方式来修改数据。也就是先不管资源有没有被别的线程占用,直接取申请操作,如果没有产生冲突,那就操作成功,如果产生冲突,有其他线程已经在使用了,那么就不断地轮询。乐观锁就是不加锁。好处就是减少上下文切换,坏处是浪费CPU时间

其他锁:自增锁

自增锁:主要用于事务中插入自增字段,也就是我们最常用的自增主键id。通过innodb_autoinc_lock_mode参数可以设置自增主键的生成策略。防止并发插入数据的时候自增id出现异常。

面试官:sql注入是什么?怎么预防sql注入?

 参考:SQL注入简单来说就是通过在表单中填写包含SQL关键字的数据来使数据库执行非常规代码的过程。SQL数据库的操作是通过SQL语句来执行的,这就导致如果我们在代码中加入了某些SQL语句关键字(比如说DELETE、DROP等),这些关键字就很可能在数据库写入或读取数据时得到执行。

如何防止SQL注入

1、检查变量数据类型和格式

如果SQL语句是类似where id={$id}这种形式,数据库里所有的id都是数字,那么就应该在SQL被执行前,检查确保变量id是int类型;如果是其他的类型比如日期、时间等也是一个道理。只要是有固定格式的变量,在SQL语句执行前,应该严格按照固定格式去检查,确保变量是我们预想的格式,这样很大程度上可以避免SQL注入攻击。
2、过滤特殊符号
对于无法确定固定格式的变量,一定要进行特殊符号过滤或转义处理。

3、绑定变量,使用预编译语句

实际上,绑定变量使用预编译语句是预防SQL注入的最佳方式,使用预编译的SQL语句语义不会发生改变,在SQL语句中,变量用问号?表示,黑客即使本事再大,也无法改变SQL语句的结构。

补充:为什么SQL预编译能有效防御SQL注入?

1、预编译语句是什么?

一条sql在db接收到最终执行完毕返回可以分为下面三个过程:

  1. 词法和语义解析;
  2. 优化sql语句,制定执行计划;
  3. 执行并返回结果。

很多情况,同一类型的sql语句可能会反复执行,如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,不但影响执行效率也不安全。

所谓预编译语句就是将这类语句中的值用占位符替代,可以视为将sql语句模板化或者说参数化,一般称这类语句叫Prepared Statements

预编译语句的优势在于归纳为:一次编译、多次运行,省去了解析优化等过程;此外预编译语句能防止sql注入。

2、为什么Statement会被sql注入?

Statement之所以会被sql注入是因为SQL语句结构发生了变化。
比如:

"select * from tablename where username='"+uesrname+  "'and password='"+password+"'"

在用户输入’or true or’之后sql语句结构改变。

  • select * from tablename where username=”or true or” and password=”

这样本来是判断用户名和密码都匹配时才会计数,但是经过改变后变成了或的逻辑关系,不管用户名和密码是否匹配该式的返回值永远为true;

3、为什么Preparement可以防止SQL注入?

原理是采用了预编译的方法,先将SQL语句中可被客户端控制的参数集进行编译,生成对应的临时变量集,再使用对应的设置方法,为临时变量集里面的元素进行赋值,赋值函数setString(),会对传入的参数进行强制类型检查和安全检查,所以就避免了SQL注入的产生。

Preparement样式为:

select * from tablename where username=? and password=?

该SQL语句会在得到用户的输入之前先用数据库进行预编译,这样的话不管用户输入什么用户名和密码的判断始终都是并的逻辑关系,防止了SQL注入。

面试官:知道视图吗?什么时候会用到视图?

参考:视图从代码上看,视图是一个select语句,从逻辑上看,被当做一个虚拟表看待。避免了代码的冗余;避免了大量重复的sql语句,增加数据的保密性

第一点:使用视图,可以定制用户数据,聚焦特定的数据。例如为销售人员定制特定的视图

第二点:使用视图,可以简化数据操作。

第三点:使用视图,基表中的数据就有了一定的安全性因为视图是虚拟的,物理上是不存在的,只是存储了数据的集合,我们可以将基表中重要的字段信息,可以不通过视图给用户,视图是动态的数据的集合,数据是随着基表的更新而更新。

第四点:可以合并分离的数据,创建分区视图。多个分公司的数据合并到一个总视图中

面试官:说说你设计表时的一些思路?

参考:在实际开发中最为常见的设计范式有三个:第一范式是最基本的范式。如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式;第二范式在第一范式的基础之上更进一层。第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以把多种数据保存在同一张数据库表中;第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。总结一下,就是:第一范式(确保每列保持原子性);第二范式(确保表中的每列都和主键相关);第三范式(确保每列都和主键列直接相关,而不是间接相关)。

另外,在设计表时的规则还包括但不限于:

//规则1:表必须要有主键。
//规则2:一个字段只表示一个含义。
//规则3:总是包含两个日期字段:gmt_create(创建日期),gmt_modified(修改日期),且这两个字段不应该包含有额外的业务逻辑。
//规则4:MySQL中,gmt_create、gmt_modified使用DATETIME类型。
//规则5:禁止使用复杂数据类型(数组,自定义类型等)。
//规则6: MySQL中,附属表拆分后,附属表id与主表id保持一致。不允许在附属表新增主键字段。
//规则7: MySQL中,存在过期概念的表,在其设计之初就必须有过期机制,且有明确的过期时间。过期数据必须迁移至历史表中。
//规则8: MySQL中,不再使用的表,必须通知DBA予以更名归档。
//规则9: MySQL中,线上表中若有不再使用的字段,为保证数据完整,禁止删除。
//规则10: MySQL中,禁止使用OCI驱动,全部使用THI驱动。

面试官:char和varchar有什么区别?

参考:VARCHAR 是可变长度的,CHAR 是固定长度


4.redis基础部分

面试官:reids包括哪些数据类型?

参考:字符串(String)、链表(lists)、哈希表(hash)、集合(set)、有序集合(Zset)4

面试官:Redis为什么这么快
参考:

1.完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);

2.数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;

3.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

4.使用多路I/O复用模型,非阻塞IO;

多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。

这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。 采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗)

5.使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;

面试官:redis是单线程还是多线程?为什么?

参考:原来是单线程的,在redis6实现了多线程,使用单线程原因如下:

1. CPU不是瓶颈:Redis的所有操作都是基于内存的,而CPU不是Redis的瓶颈。在大多数情况下,Redis的瓶颈很可能是机器内存或网络带宽的大小。如果我们想要更高的性能,可以使用单线程Redis,我们可以使用集群(多个进程)解决方案。
2. 并发性:并行性不是支持多个客户端的唯一策略。Redis使用epoll和事件循环来实现并发策略并节省大量时间而无需上下文切换。
3. 易于实现:编写多线程程序可能会更加困难。我们需要为线程添加锁和同步机制。
4. 易于部署:单线程应用程序可以部署在至少具有单个CPU内核的任何计算机上。

面试官:并发与并行?
参考:并发性和并行性之间的区别
1. 并发就是一次处理很多事情。并行是关于一次做很多事情。
2. 并发是关于结构;并行是关于执行的。
3. 并发提供了一种构造解决方案的方法,以解决可能(但不一定)可并行化的问题。

我们可以使用餐厅服务员的类比:
什么是并发
服务员可以为多个客户提供服务,而一次只能为一个客户准备菜。
由于厨房提供的菜肴之间会有一定的间隔,因此当顾客人数少于5人时,一位侍者通常可以处理。
什么是并行
假设厨房一次可以为20位顾客提供餐具。如果一位服务员的顾客数量太大,我们需要更多的服务员。在这种情况下,多个服务员同时工作。我们称其为并行性。

面试官:redis再内存的淘汰算法是什么?

参考:为了保证读取的效率,redis把数据对象都存储在内存当中,它可以支持周期性的把更新的数据写入磁盘文件中。而且它还提供了交集和并集,以及一些不同方式排序的操作,他的淘汰算法包括:

大体分为四种

lru:挑选最近最少使用的数据淘汰

lfu:挑选频率最低的数据淘汰

random:随机淘汰

ttl:挑选将要过期的数据淘汰

 

posted @ 2022-03-14 18:26  coder-yif  阅读(286)  评论(0)    收藏  举报