本站提供最佳博乐达源码防伪服务,欢迎转载和分享。

【iapp源码音乐】【源码交易怎么用】【永洪产品源码】go mutex 源码

2024-12-29 09:23:50 来源:libwebsocket源码下载 分类:综合

1.手摸手Go 深入理解sync.Cond
2.「2023必刷版」Golang高频面试题
3.Golang面试题从浅入深高频必刷「2023版」
4.Go并发编程 — sync.Once
5.Go并发编程:goroutine,channel和sync详解
6.一文完全掌握 Go math/rand

go mutex 源码

手摸手Go 深入理解sync.Cond

       sync.Cond 是一个用于等待一个或一组goroutines满足条件后唤醒的条件变量实现。它关联一个锁,通常是 *Mutex 或 RWMutex,具体根据需求初始化。

       sync.Cond的基本用法以实现FIFO队列为例。我们定义了队列的iapp源码音乐Offer和Pop操作。一个goroutine不断向队列投放数据,同时有两个goroutines不断取拿数据。Cond帮助我们管理队列的访问。

       在使用sync.Mutex时,需要理解其核心功能。具体地,Cond.wait()的使用遵循特定模板,其逻辑将在后续分析中详细说明。

       sync.Cond的数据结构包含三个部分:noCopy、Locker和copyChecker。noCopy确保实例不可复制,Locker用于互斥访问,copyChecker检查Cond实例是否被复制。当实例被复制时,会检查并确保内存地址的正确性。

       sync.Cond的notifyList包含三类字段,用于管理等待的goroutines。整体结构简单明了。

       sync.Cond的源码交易怎么用核心操作Wait()分为五个步骤,主要完成两个任务:等待和加入等待链表。Signal操作唤醒一个等待时间最长的goroutine,无需持有锁。Broadcast()与Signal()区别在于它可以唤醒全部等待的goroutine,并直接更新等待链表。

       总结sync.Cond的使用,它提供了一种高效管理goroutines等待和唤醒的机制,简化了多线程编程中的同步问题。通过源码分析,我们深入了解了Cond的内部实现,为实际应用提供了基础。

「必刷版」Golang高频面试题

       在面试Go语言时,候选人应深入了解其特性,如切片与数组的差异,Goroutine与GMP模型,以及锁机制如Mutex和RWMutex的使用。Go语言以其优雅简洁和高效性能备受青睐,尤其在云时代项目中占据重要地位。面试中,常考知识点包括切片扩容机制的更新、互斥锁的并发控制、死锁的概念与解决、sync.Cond的通信机制、Channel的永洪产品源码通信原理和SingleFlight的并发限制。掌握这些内容不仅需要理论理解,还需结合源码阅读,例如Go源码的详细注释。此外,低代码工具如JNPF快速开发平台在企业内部工具开发中的应用,也是技术发展中的一个值得关注的趋势,可以提高开发效率。以下是对高频面试题的概述:

       1. **Go语言特性**:Go语言以其高效与简洁著称,结合了C++的性能和Java的库管理。项目如Docker和Kubernetes展示了其在云时代的优势。

       2. **面试重点**:深入了解Go语言,包括切片、通道、异常处理、Goroutine、GMP模型、字符串拼接、指针、反射、接口、sync和测试工具等。

       3. **实例解析**:

       - **切片与数组**:数组用于固定长度,切片则更灵活,尤其适合动态数据。批量标注的源码

       - **切片扩容机制**:Go 1.后,切片扩容策略更为平滑。

       - **Mutex**:Go的互斥锁,用于并发资源保护,有正常模式和饥饿模式。

       - **RWMutex**:适用于读多写少的场景,处理读写锁。

       - **死锁**:理解死锁产生的条件和解决策略。

       - **sync.Cond**:用于等待条件的goroutine通信。

       - **Channel**:Go的并发通信机制,用于goroutine间通信。

       - **SingleFlight**:并发控制原语,避免重复调用。

       4. **技术前沿**:低代码工具如JNPF快开发平台,提高开发效率,减少重复工作。

Golang面试题从浅入深高频必刷「版」

       大家好,我是阳哥,专注于Go语言的学习分享和就业指导。Go语言以其优雅简洁的特点脱颖而出,兼顾了C++的高性能和Java/Python的便利性,拥有接口、垃圾回收和goroutine等独特设计,尤其在云时代,春秋黑马源码指标Go的应用项目如Docker、Kubernetes等众多优秀案例证明了其价值。

       在面试中,深入理解Go语言特性是关键,例如切片与数组的差异、通道(Goroutine的通信机制)、异常处理、GMP模型、字符串高效拼接等知识点。掌握Go源码,尤其是其详细注释,能有效提升技能。同时,熟悉go test和相关工具链也是必修课。

       1. Go语言基础

切片与数组:Go中的数组适用于固定长度,而切片更灵活,常用于动态长度数据。理解切片的初始化、长度、容量操作以及扩容机制,包括不同Go版本的差异。

Mutex与RWMutex:互斥锁在并发控制中至关重要,理解Mutex的基本操作和饥饿模式,以及RWMutex在读写场景中的应用。

死锁与同步工具:了解死锁的概念及解决策略,以及sync.Cond在条件等待中的应用和易错点。

Channel:作为并发通信的核心,理解Channel的用法、底层结构和常见应用场景。

       进阶主题

SingleFlight:Go中的并发控制扩展,与sync.Once的区别,以及在优化系统性能中的作用和使用方法。

Go并发编程 — sync.Once

       ç®€ä»‹

       Once 可以用来执行某个函数,但是这个函数仅仅只会执行一次,常常用于单例对象的初始化场景。说到这,就不得不说一下单例模式了。

单例模式

       å•ä¾‹æ¨¡å¼æœ‰æ‡’汉式和饿汉式两种,上代码。

饿汉式

       é¥¿æ±‰å¼é¡¾åæ€ä¹‰å°±æ˜¯æ¯”较饥饿,所以就是上来就初始化。

var?instance?=?&Singleton{ }type?Singleton?struct?{ }func?GetInstance()?*Singleton?{ return?instance}懒汉式

       æ‡’汉式顾名思义就是偷懒,在获取实例的时候在进行初始化,但是懒汉式会有并发问题。并发问题主要发生在 instance == nil 这个判断条件上,有可能多个 goruntine 同时获取 instance 对象都是 nil ,然后都开始创建了 Singleton 实例,就不满足单例模式了。

var?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ if?instance?==?nil?{ ?instance?=?&Singleton{ }}return?instance}加锁

       æˆ‘们都知道并发问题出现后,可以通过加锁来进行解决,可以使用 sync.Metux 来对整个方法进行加锁,就例如下面这样。这种方式是解决了并发的问题,但是锁的粒度比较高,每次调用 GetInstance 方法的时候都需要获得锁才能获得 instance 实例,如果在调用频率比较高的场景下性能就不会很好。那有什么方式可以解决嘛?让我们接着往下看吧

var?mutex?sync.Mutexvar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ mutex.Lock()defer?mutex.Unlock()if?instance?==?nil?{ ?instance?=?&Singleton{ }}return?instance}Double Check

       ä¸ºäº†è§£å†³é”çš„粒度问题,我们可以使用 Double Check 的方式来进行解决,例如下面的代码,第一次判断 instance == nil 之后需要进行加锁操作,然后再第二次判断 instance == nil 之后才能创建实例。这种方式对比上面的案例来说,锁的粒度更低,因为如果 instance != nil 的情况下是不需要加锁的。但是这种方式实现起来是不是比较麻烦,有没有什么方式可以解决呢?

var?mutex?sync.Mutexvar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ if?instance?==?nil?{ ?mutex.Lock()?defer?mutex.Unlock()?if?instance?==?nil?{ ?instance?=?&Singleton{ }?}}return?instance}使用 sync.Once

       å¯ä»¥ä½¿ç”¨ sync.Once 来实现单例的初始化逻辑,因为这个逻辑至多只会跑一次。推荐使用这种方式来进行单例的初始化,当然也可以使用饿汉式。

var?once?sync.Oncevar?instance?*Singletontype?Singleton?struct?{ }func?GetInstance()?*Singleton?{ once.Do(func()?{ ?instance?=?&Singleton{ }})return?instance}源码分析

       ä¸‹é¢å°±æ˜¯ sync.Once 包的源码,我删除了注释,代码不多,Once 数据结构主要由 done 和 m 组成,其中 done 是存储 f 函数是否已执行,m 是一个锁实例。

type?Once?struct?{ done?uint?//?f函数是否已执行mMutex?//?锁}func?(o?*Once)?Do(f?func())?{ if?atomic.LoadUint(&o.done)?==?0?{ ?o.doSlow(f)}}func?(o?*Once)?doSlow(f?func())?{ o.m.Lock()defer?o.m.Unlock()if?o.done?==?0?{ ?defer?atomic.StoreUint(&o.done,?1)?f()}}

       Do 方法

       ä¼ å…¥ä¸€ä¸ª function,然后 sync.Once 来保证只执行一次,在 Do 方法中使用 atomic 来读取 done 变量,如果是 0 ,就代码 f 函数没有被执行过,然后就调用 doSlow方法,传入 f 函数

       doShow 方法

       doShow 的第一个步骤就是先加锁,这里加锁的目的是保证同一时刻是能由一个 goruntine 来执行 doSlow 方法,然后再次判断 done 是否是 0 ,这个判断就相当于我们上面说的 DoubleCheck ,因为 doSlow 可能存在并发问题。然后执行 f 方法,然后执行使用 atomic 将 done 保存成 1。使用 DoubleCheck 保证了 f 方法只会被执行一次。

       æŽ¥ç€çœ‹ï¼Œé‚£å¯ä»¥è¿™æ ·å®žçŽ° sync.Once 嘛?

       è¿™æ ·ä¸æ˜¯æ›´ç®€å•ä¸€ç‚¹å˜›ï¼Œä½¿ç”¨åŽŸå­çš„ CAS 操作就可以解决并发问题呀,并发只执行一次 f 方法的问题是可以解决,但是 Do 方法可能并发,第一个调用者将 done 设置成了 1 然后调用 f 方法,如果 f 方法特别耗时间,那么第二个调用者获取到 done 为 1 就直接返回了,此时 f方法是没有执行过第二次,但是此时第二个调用者可以继续执行后面的代码,如果后面的代码中有用到 f 方法创建的实例,但是由于 f 方法还在执行中,所以可能会出现报错问题。所以官方采用的是Lock + DoubleCheck 的方式。

if?atomic.CompareAndSwapUint(&o.done,?0,?1)?{ f()}拓展

       æ‰§è¡Œå¼‚常后可继续执行的Once

       çœ‹æ‡‚了源码之后,我们就可以扩展 sync.Once 包了。例如 f 方法在执行的时候报错了,例如连接初始化失败,怎么办?我们可以实现一个高级版本的 Once 包,具体的 slowDo 代码可以参考下面的实现

func?(o?*Once)?slowDo(f?func()?error)?error?{ o.m.Lock()defer?o.m.Unlock()var?err?errorif?o.done?==?0?{ ?//?Double?Checkerr?=?f()if?err?==?nil?{ ?//?没有异常的时候记录done值atomic.StoreUint(&o.done,?1)}}return?err}

       å¸¦æ‰§è¡Œç»“果的 Once

       ç”±äºŽ Once 是不带执行结果的,我们不知道 Once 什么时候会执行结束,如果存在并发,需要知道是否执行成功的话,可以看下下面的案例,我这里是以 redis 连接的问题来进行说明的。Do 方法执行完毕后将 init 值设置成 1 ,然后其他 goruntine 可以通过 IsConnetion 来获取连接是否建立,然后做后续的操作。

type?RedisConn?struct?{ once?sync.Onceinit?uint}func?(this?*RedisConn)?Init()?{ this.once.Do(func()?{ ?//?do?redis?connection?atomic.StoreUint(&this.init,?1)})}func?(this?*RedisConn)?IsConnect()?bool?{ ?//?另外一个goroutinereturn?atomic.LoadUint(&this.init)?!=?0}

Go并发编程:goroutine,channel和sync详解

       ä¼˜é›…的并发编程范式,完善的并发支持,出色的并发性能是Go语言区别于其他语言的一大特色。

       åœ¨å½“今这个多核时代,并发编程的意义不言而喻。使用Go开发并发程序,操作起来非常简单,语言级别提供关键字go用于启动协程,并且在同一台机器上可以启动成千上万个协程。

       ä¸‹é¢å°±æ¥è¯¦ç»†ä»‹ç»ã€‚

goroutine

       Go语言的并发执行体称为goroutine,使用关键词go来启动一个goroutine。

       go关键词后面必须跟一个函数,可以是有名函数,也可以是无名函数,函数的返回值会被忽略。

       go的执行是非阻塞的。

       å…ˆæ¥çœ‹ä¸€ä¸ªä¾‹å­ï¼š

packagemainimport("fmt""time")funcmain(){ gospinner(*time.Millisecond)constn=fibN:=fib(n)fmt.Printf("\rFibonacci(%d)=%d\n",n,fibN)//Fibonacci()=}funcspinner(delaytime.Duration){ for{ for_,r:=range`-\|/`{ fmt.Printf("\r%c",r)time.Sleep(delay)}}}funcfib(xint)int{ ifx<2{ returnx}returnfib(x-1)+fib(x-2)}

       ä»Žæ‰§è¡Œç»“果来看,成功计算出了斐波那契数列的值,说明程序在spinner处并没有阻塞,而且spinner函数还一直在屏幕上打印提示字符,说明程序正在执行。

       å½“计算完斐波那契数列的值,main函数打印结果并退出,spinner也跟着退出。

       å†æ¥çœ‹ä¸€ä¸ªä¾‹å­ï¼Œå¾ªçŽ¯æ‰§è¡Œæ¬¡ï¼Œæ‰“印两个数的和:

packagemainimport"fmt"funcAdd(x,yint){ z:=x+yfmt.Println(z)}funcmain(){ fori:=0;i<;i++{ goAdd(i,i)}}

       æœ‰é—®é¢˜äº†ï¼Œå±å¹•ä¸Šä»€ä¹ˆéƒ½æ²¡æœ‰ï¼Œä¸ºä»€ä¹ˆå‘¢ï¼Ÿ

       è¿™å°±è¦çœ‹Go程序的执行机制了。当一个程序启动时,只有一个goroutine来调用main函数,称为主goroutine。新的goroutine通过go关键词创建,然后并发执行。当main函数返回时,不会等待其他goroutine执行完,而是直接暴力结束所有goroutine。

       é‚£æœ‰æ²¡æœ‰åŠžæ³•è§£å†³å‘¢ï¼Ÿå½“然是有的,请往下看。

channel

       ä¸€èˆ¬å†™å¤šè¿›ç¨‹ç¨‹åºæ—¶ï¼Œéƒ½ä¼šé‡åˆ°ä¸€ä¸ªé—®é¢˜ï¼šè¿›ç¨‹é—´é€šä¿¡ã€‚常见的通信方式有信号,共享内存等。goroutine之间的通信机制是通道channel。

       ä½¿ç”¨make创建通道:

ch:=make(chanint)//ch的类型是chanint

       é€šé“支持三个主要操作:send,receive和close。

ch<-x//发送x=<-ch//接收<-ch//接收,丢弃结果close(ch)//关闭无缓冲channel

       make函数接受两个参数,第二个参数是可选参数,表示通道容量。不传或者传0表示创建了一个无缓冲通道。

       æ— ç¼“冲通道上的发送操作将会阻塞,直到另一个goroutine在对应的通道上执行接收操作。相反,如果接收先执行,那么接收goroutine将会阻塞,直到另一个goroutine在对应通道上执行发送。

       æ‰€ä»¥ï¼Œæ— ç¼“冲通道是一种同步通道。

       ä¸‹é¢æˆ‘们使用无缓冲通道把上面例子中出现的问题解决一下。

packagemainimport"fmt"funcAdd(x,yint,chchanint){ z:=x+ych<-z}funcmain(){ ch:=make(chanint)fori:=0;i<;i++{ goAdd(i,i,ch)}fori:=0;i<;i++{ fmt.Println(<-ch)}}

       å¯ä»¥æ­£å¸¸è¾“出结果。

       ä¸»goroutine会阻塞,直到读取到通道中的值,程序继续执行,最后退出。

缓冲channel

       åˆ›å»ºä¸€ä¸ªå®¹é‡æ˜¯5的缓冲通道:

ch:=make(chanint,5)

       ç¼“冲通道的发送操作在通道尾部插入一个元素,接收操作从通道的头部移除一个元素。如果通道满了,发送会阻塞,直到另一个goroutine执行接收。相反,如果通道是空的,接收会阻塞,直到另一个goroutine执行发送。

       æœ‰æ²¡æœ‰æ„Ÿè§‰ï¼Œå…¶å®žç¼“冲通道和队列一样,把操作都解耦了。

单向channel

       ç±»åž‹chan<-int是一个只能发送的通道,类型<-chanint是一个只能接收的通道。

       ä»»ä½•åŒå‘通道都可以用作单向通道,但反过来不行。

       è¿˜æœ‰ä¸€ç‚¹éœ€è¦æ³¨æ„ï¼Œclose只能用在发送通道上,如果用在接收通道会报错。

       çœ‹ä¸€ä¸ªå•å‘通道的例子:

packagemainimport"fmt"funccounter(outchan<-int){ forx:=0;x<;x++{ out<-x}close(out)}funcsquarer(outchan<-int,in<-chanint){ forv:=rangein{ out<-v*v}close(out)}funcprinter(in<-chanint){ forv:=rangein{ fmt.Println(v)}}funcmain(){ n:=make(chanint)s:=make(chanint)gocounter(n)gosquarer(s,n)printer(s)}sync

       sync包提供了两种锁类型:sync.Mutex和sync.RWMutex,前者是互斥锁,后者是读写锁。

       å½“一个goroutine获取了Mutex后,其他goroutine不管读写,只能等待,直到锁被释放。

packagemainimport("fmt""sync""time")funcmain(){ varmutexsync.Mutexwg:=sync.WaitGroup{ }//主goroutine先获取锁fmt.Println("Locking(G0)")mutex.Lock()fmt.Println("locked(G0)")wg.Add(3)fori:=1;i<4;i++{ gofunc(iint){ //由于主goroutine先获取锁,程序开始5秒会阻塞在这里fmt.Printf("Locking(G%d)\n",i)mutex.Lock()fmt.Printf("locked(G%d)\n",i)time.Sleep(time.Second*2)mutex.Unlock()fmt.Printf("unlocked(G%d)\n",i)wg.Done()}(i)}//主goroutine5秒后释放锁time.Sleep(time.Second*5)fmt.Println("readyunlock(G0)")mutex.Unlock()fmt.Println("unlocked(G0)")wg.Wait()}

       RWMutex属于经典的单写多读模型,当读锁被占用时,会阻止写,但不阻止读。而写锁会阻止写和读。

packagemainimport("fmt""sync""time")funcmain(){ varrwMutexsync.RWMutexwg:=sync.WaitGroup{ }Data:=0wg.Add()fori:=0;i<;i++{ gofunc(tint){ //第一次运行后,写解锁。//循环到第二次时,读锁定后,goroutine没有阻塞,同时读成功。fmt.Println("Locking")rwMutex.RLock()deferrwMutex.RUnlock()fmt.Printf("Readdata:%v\n",Data)wg.Done()time.Sleep(2*time.Second)}(i)gofunc(tint){ //写锁定下是需要解锁后才能写的rwMutex.Lock()deferrwMutex.Unlock()Data+=tfmt.Printf("WriteData:%v%d\n",Data,t)wg.Done()time.Sleep(2*time.Second)}(i)}wg.Wait()}总结

       å¹¶å‘编程算是Go的特色,也是核心功能之一了,涉及的知识点其实是非常多的,本文也只是起到一个抛砖引玉的作用而已。

       æœ¬æ–‡å¼€å§‹ä»‹ç»äº†goroutine的简单用法,然后引出了通道的概念。

       é€šé“有三种:

       æ— ç¼“冲通道

       ç¼“冲通道

       å•å‘通道

       æœ€åŽä»‹ç»äº†Go中的锁机制,分别是sync包提供的sync.Mutex(互斥锁)和sync.RWMutex(读写锁)。

       goroutine博大精深,后面的坑还是要慢慢踩的。

       æ–‡ç« ä¸­çš„脑图和源码都上传到了GitHub,有需要的同学可自行下载。

       åœ°å€ï¼šgithub.com/yongxinz/gopher/tree/main/sc

       ä½œè€…:yongxinz

一文完全掌握 Go math/rand

       本文旨在全面解析 Go 中的 math/rand 包,帮助开发者轻松掌握随机数生成。在开始之前,你可能会疑惑 rand 是否会引发 panic。实际上,rand 的源码简单明了,仅包含两个核心函数。让我们一起深入探讨。

       源码剖析

       math/rand 的源码主要由两个函数构成:一个用于设置 seed,另一个用于生成随机数。seed 的设置是通过调整 rng.vec 的值来实现的,而 rng.vec 的大小为 。无论调用 Intn()、Intn() 等函数,最终都会经过这个种子设置的过程。

       每次调用时,rand 通过 rng.feed 和 rng.tap 从 rng.vec 中获取两个值进行相加,得到结果并返回。同时,该结果会被重新放入 rng.vec。当多个 goroutine 同时调用时,需要考虑数据竞争问题,为此,math/rand 采用在调用 rngSource 时加锁 sync.Mutex 来解决。

       关于使用 rand.Seed() 和 rand.Intn() 等函数,它们之所以可行,是因为 math/rand 初始化了一个全局的 globalRand 变量。然而,在高并发场景下,直接使用 rand.Int() 可能会导致全局的 goroutine 锁竞争。因此,在性能瓶颈时,考虑使用 New(&lockedSource{ src: NewSource(1).(*rngSource)}) 为不同的模块生成单独的 rand。

       种子(seed)的作用

       设置相同的种子可确保每次运行结果一致,这是由 seed 实际计算结果决定的。seedrand 函数计算出的值并不随机,而是根据 seed 的实际值计算得到。因此,相同的 seed 会导致 rng.vec 中的值相同,进而 Intn 函数返回的值也相同。

       遇到的坑

       在项目开发中,使用第三方库时,可能出现 panic 情况,这是因为底层库中的 rrRand 不具备并发安全性。在并发调用 rrRand.Perm 时,锁竞争可能导致 panic。解决这一问题的方法是使用 globalRand,它在高并发场景下表现良好,减少了全局锁的负面影响。

       流量不均问题同样出现在底层封装的 RPC 库中。使用随机方式负载均衡时,流量集中到一台机器,导致服务宕机。问题在于每次请求都重新初始化 rand,而相同的 seed 导致了相同的随机数生成,进而获取相同的 ip 和 port。解决方法是采用全局 rand,如 globalRand。

       未来期望

       在使用 math/rand 时,为避免全局锁竞争,自定义 rand 时可能遇到问题。math/rand 需要加锁以保证在并发获取随机数时的随机性。考虑到 rand.Intn() 存在全局锁竞争问题,未来的优化方向可能是减少锁的使用或采用更高效的并发机制。欢迎讨论如何进一步优化 math/rand 包。

从项目的一个 panic 说起:Go 中 Sync 包的分析应用

       在项目开发过程中,遇到一个常见的错误——"fatal error: concurrent map read and map write",这是由于Golang内建的map在并发环境下不安全导致的。解决这个问题的方法并不复杂,就是转向使用sync包提供的并发安全的map。

       sync包在Golang 1.9之后被官方支持,其中包含了丰富的同步原语,是并发编程的关键部分。在Golang 1.9之前,解决map并发问题通常会借助sync包中的sync.RWMutex或其他锁机制。Golang作为支持用户态进程的编程语言,对并发编程的处理自然离不开锁,这是一种确保多个Goroutine在同一片内存中协同工作的同步机制。

       sync包的源码目录结构清晰,包含Mutex、RWmutex、WaitGroup、Map、Once、Cond、Pool等组件。接下来,我们将逐个分析这些同步原语的用途和使用注意事项,重点讨论在项目中常见的sync.Map。

       sync.Map是sync包中的一种高效并发安全的map实现,与内建map相比,它提供了Load、Store、LoadOrStore、Delete和Range等方法,并且具有更高的并发性能。虽然sync.Map没有len方法,但其内部机制使得在并发环境中的操作更加稳健。

       通过结合实际项目案例和面试题中的陷阱,本文简要探讨了sync包中Mutex、RWMutex、WaitGroup、Once以及Map的使用技巧和注意事项。在实际编程中,正确使用这些同步原语对于避免并发问题至关重要。

【本文网址:http://abssuliao.net/html/35e486095104.html 欢迎转载】

copyright © 2016 powered by 皮皮网   sitemap