用Golang自动获取最快GitHub IP,一键生成hosts文件

前言

作为开发者,访问 GitHub 慢或打不开是日常痛点。常见方案是手动查找 IP 然后修改 hosts 文件,但 IP 会不定期变更,手动维护非常麻烦。

于是我用 Go 写了一个小工具 GitHubHosts,自动完成以下流程:

复制代码
DNS 解析 → IP 可达性检测 → 选择最快 IP → 生成 hosts 文件

全程多线程并发,34 个域名几秒内完成解析和筛选。


核心功能

功能 说明
🌐 自定义 DNS 服务器 通过 -dns 参数指定,默认 1.1.1.1
📄 外部域名文件 读取 source.txt,不存在则使用内置域名列表
🔍 域名自动去重 支持外部文件传入时有重复域名,自动去重
⚡ 多线程并发 所有域名同时解析,同域名多 IP 同时探测
🏆 最快 IP 选取 TCP 443 探测延迟,选响应最快的 IP
📝 自动生成 hosts 输出 hosts.txt

快速使用

编译

bash 复制代码
go build -o GitHubHosts.exe main.go

运行

bash 复制代码
# 使用默认 DNS (1.1.1.1)
./GitHubHosts.exe

# 指定 DNS 服务器
./GitHubHosts.exe -dns 8.8.8.8

自定义域名

在同级目录创建 source.txt,每行一个域名,支持 # 注释:

text 复制代码
# GitHub 域名列表
github.com
api.github.com
raw.githubusercontent.com
gist.github.com

文件不存在或为空时,自动使用内置的 34 个 GitHub 相关域名。


输出效果

控制台输出:

复制代码
####################
github.com -> 20.205.243.166 (delay: 12.3ms)
api.github.com -> 20.205.243.168 (delay: 15.7ms)
raw.githubusercontent.com -> 185.199.109.133 (delay: 8.1ms)
...
# Last Update Time :  2026-06-29 10:30:45
####################

生成的 hosts.txt

复制代码
# ####################Github Start####################
20.205.243.166                         github.com
20.205.243.168                         api.github.com
185.199.109.133                        raw.githubusercontent.com
...
# Last Update Time : 2026-06-29 10:30:45
# Blog: https://blog.csdn.net/ccboy2009
# ####################Github End####################

设计思路

1. DNS 解析:自定义服务器

通过 nslookup 命令解析域名,支持指定 DNS 服务器:

go 复制代码
dnsServer := flag.String("dns", "1.1.1.1", "DNS server IP address")
flag.Parse()

不传入时默认使用 Cloudflare 的 1.1.1.1,也可用 8.8.8.8(Google)或其他。

2. 解析输出处理:兼容 GBK 编码

Windows 下 nslookup 的输出可能是 GBK 编码,中文关键词匹配会失败。策略是:直接用正则提取所有 IP 地址,去掉 DNS 服务器自身地址,剩余即为解析结果

go 复制代码
func parse_nslookup_output(output string, dnsServer string) []string {
    ipRegex := regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`)
    matches := ipRegex.FindAllStringSubmatch(output, -1)

    var ips []string
    for _, m := range matches {
        ip := m[1]
        if ip == dnsServer {  // 跳过 DNS 服务器自身
            continue
        }
        ips = append(ips, ip)
    }
    return ips
}

3. IP 可达性检测:TCP 443 探测

为什么用 TCP 443 而不是 ICMP ping?

  • 这些域名写入 hosts 后,通过浏览器 HTTPS 访问,走的就是 443 端口
  • 很多 CDN 节点禁用了 ICMP,ping 不通但 443 端口实际可用
  • TCP 探测能真实反映服务可用性
go 复制代码
func ping_ip(ip string, timeout time.Duration) ipPingResult {
    start := time.Now()
    conn, err := net.DialTimeout("tcp", ip+":443", timeout)
    elapsed := time.Since(start)
    if err != nil {
        return ipPingResult{ip: ip, ok: false}
    }
    conn.Close()
    return ipPingResult{ip: ip, delay: elapsed, ok: true}
}

4. 最快 IP 选取

一个域名通常解析出多个 IP,并发探测后按延迟排序,取最快的一个:

go 复制代码
sort.Slice(reachable, func(i, j int) bool {
    return reachable[i].delay < reachable[j].delay
})
best := reachable[0]

降级策略:如果所有 IP 都不可达,仍使用 DNS 解析到的第一个 IP,避免丢失域名。

5. 两级并发

复制代码
┌─────────────────────────────────────────────────┐
│  域名级并发:所有域名同时 DNS 解析 + IP 探测     │
│  ┌───────────┐  ┌───────────┐  ┌───────────┐   │
│  │ github.com │  │ api.github│  │ raw.github │  │
│  │  goroutine │  │  goroutine│  │  goroutine │  │
│  └─────┬─────┘  └─────┬─────┘  └─────┬─────┘  │
│        │               │               │        │
│  IP级并发:同域名多IP同时探测                      │
│  ┌─────┴─────┐  ┌─────┴─────┐  ┌─────┴─────┐  │
│  │IP1│IP2│IP3│  │IP1│IP2│IP3│  │IP1│IP2│IP3│  │
│  └───────────┘  └───────────┘  └───────────┘   │
└─────────────────────────────────────────────────┘
  • 域名级 :使用 sync.WaitGroup + goroutine,所有域名同时开始解析
  • IP 级:每个域名下的多个 IP 同时 TCP 探测

串行处理 34 个域名需要逐个等待,并发后总耗时 ≈ 最慢的单个域名耗时

6. 域名去重

外部文件可能存在重复域名,使用 map 去重并保持原有顺序:

go 复制代码
func deduplicateDomains(domains []string) []string {
    seen := make(map[string]bool)
    var result []string
    for _, d := range domains {
        d = strings.TrimSpace(d)
        if d != "" && !seen[d] {
            seen[d] = true
            result = append(result, d)
        }
    }
    return result
}

7. 输出顺序保持

并发结果通过 channel 收集到 map,再按原始域名顺序遍历输出,保证 hosts 文件顺序稳定:

go 复制代码
ipMap := make(map[string]string)
for r := range results {
    ipMap[r.domain] = r.ip
}
for _, v := range url_list {
    if ip, ok := ipMap[v]; ok {
        result += fmt.Sprintf("%-39s%s\n", ip, v)
    }
}

Go 时间格式小贴士

Go 的时间格式化与其他语言不同,不是用 YYYY-MM-DD 这种符号,而是用参考时间 Mon Jan 2 15:04:05 MST 2006 的各组成部分来指代:

格式 含义 记忆
2006 6
01 1
02 2
15 时(24h) 3
04 4
05 5

记住 1-2-3-4-5-6 就能写出任何格式:

go 复制代码
time.Now().Format("2006-01-02 15:04:05")
// 输出: 2026-06-29 10:30:45

完整源码

资源下载:https://download.csdn.net/download/ccboy2009/93040806

go 复制代码
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"net"
	"os"
	"os/exec"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"
)

func main() {
	dnsServer := flag.String("dns", "1.1.1.1", "DNS server IP address")
	flag.Parse()

	defaultDomains := []string{
		"alive.github.com",
		"live.github.com",
		"github.githubassets.com",
		"central.github.com",
		"desktop.githubusercontent.com",
		"assets-cdn.github.com",
		"camo.githubusercontent.com",
		"github.map.fastly.net",
		"github.global.ssl.fastly.net",
		"gist.github.com",
		"github.io",
		"github.com",
		"github.blog",
		"api.github.com",
		"raw.githubusercontent.com",
		"user-images.githubusercontent.com",
		"favicons.githubusercontent.com",
		"avatars5.githubusercontent.com",
		"avatars4.githubusercontent.com",
		"avatars3.githubusercontent.com",
		"avatars2.githubusercontent.com",
		"avatars1.githubusercontent.com",
		"avatars0.githubusercontent.com",
		"avatars.githubusercontent.com",
		"codeload.github.com",
		"github-cloud.s3.amazonaws.com",
		"github-com.s3.amazonaws.com",
		"github-production-release-asset-2e65be.s3.amazonaws.com",
		"github-production-user-asset-6210df.s3.amazonaws.com",
		"github-production-repository-file-5c1aeb.s3.amazonaws.com",
		"githubstatus.com",
		"github.community",
		"github.dev",
		"media.githubusercontent.com",
	}

	url_list := deduplicateDomains(loadDomains("source.txt", defaultDomains))

	result := "# ####################Github Start####################\n"
	fmt.Printf("####################\n")

	// 多线程并发解析所有域名
	type domainIP struct {
		domain string
		ip     string
	}

	var wg sync.WaitGroup
	results := make(chan domainIP, len(url_list))

	for _, v := range url_list {
		wg.Add(1)
		go func(domain string) {
			defer wg.Done()
			ip := get_fastest_ip(domain, *dnsServer)
			if ip != "" {
				results <- domainIP{domain: domain, ip: ip}
			}
		}(v)
	}

	// 等待所有任务完成后关闭 channel
	go func() {
		wg.Wait()
		close(results)
	}()

	// 收集结果并按原始域名顺序输出
	ipMap := make(map[string]string)
	for r := range results {
		ipMap[r.domain] = r.ip
	}

	for _, v := range url_list {
		if ip, ok := ipMap[v]; ok {
			result += fmt.Sprintf("%-39s%s\n", ip, v)
			fmt.Printf("%-39s%s\n", ip, v)
		}
	}

	result += fmt.Sprintf("# Last Update Time : %s \n", time.Now().Format("2006-01-02 15:04:05"))
	result += "# Blog: https://blog.csdn.net/ccboy2009 \n"

	fmt.Printf("# Last Update Time :  %s \n", time.Now().Format("2006-01-02 15:04:05"))

	result += "# ####################Github End####################\n"
	fmt.Printf("####################")
	//保存文件
	ioutil.WriteFile(`hosts.txt`, []byte(result), 0666)

	content := read_tmp()
	content = strings.Replace(content, "12345", result, 1)
	write_file(content)

}

func loadDomains(filePath string, defaultList []string) []string {
	data, err := ioutil.ReadFile(filePath)
	if err != nil {
		fmt.Printf("source.txt not found, using default domains\n")
		return defaultList
	}

	var domains []string
	for _, line := range strings.Split(string(data), "\n") {
		line = strings.TrimSpace(line)
		if line != "" && !strings.HasPrefix(line, "#") {
			domains = append(domains, line)
		}
	}

	if len(domains) == 0 {
		fmt.Printf("source.txt is empty, using default domains\n")
		return defaultList
	}

	fmt.Printf("loaded %d domains from %s\n", len(domains), filePath)
	return domains
}

// deduplicateDomains 对域名列表去重,保持原有顺序
func deduplicateDomains(domains []string) []string {
	seen := make(map[string]bool)
	var result []string
	for _, d := range domains {
		d = strings.TrimSpace(d)
		if d != "" && !seen[d] {
			seen[d] = true
			result = append(result, d)
		}
	}
	if len(result) < len(domains) {
		fmt.Printf("dedup: %d -> %d domains\n", len(domains), len(result))
	}
	return result
}

func read_tmp() string {
	file, err := os.Open("README_TEMP.md")
	if err != nil {
		panic(err)
	}
	defer file.Close()
	content, err := ioutil.ReadAll(file)
	return string(content)
}

func write_file(content string) {
	err := ioutil.WriteFile("README.md", []byte(content), 0644)
	if err != nil {
		panic(err)
	}
}

func get_ip_list(domain string, dnsServer string) []string {
	cmd := exec.Command("nslookup", domain, dnsServer)
	output, err := cmd.CombinedOutput()
	if err != nil {
		fmt.Printf("nslookup failed for %s: %v\n", domain, err)
		return nil
	}

	return parse_nslookup_output(string(output), dnsServer)
}

// ipPingResult 记录 IP 的连通性测试结果
type ipPingResult struct {
	ip    string
	delay time.Duration
	ok    bool
}

// ping_ip 测试 IP 是否可达,返回延迟时间
func ping_ip(ip string, timeout time.Duration) ipPingResult {
	start := time.Now()
	conn, err := net.DialTimeout("tcp", ip+":443", timeout)
	elapsed := time.Since(start)
	if err != nil {
		return ipPingResult{ip: ip, ok: false}
	}
	conn.Close()
	return ipPingResult{ip: ip, delay: elapsed, ok: true}
}

// get_fastest_ip 获取域名对应的所有 IP,测试连通性,返回响应最快的一个
func get_fastest_ip(domain string, dnsServer string) string {
	ips := get_ip_list(domain, dnsServer)
	if len(ips) == 0 {
		fmt.Printf("no IP found for %s\n", domain)
		return ""
	}

	// 并发测试所有 IP
	results := make(chan ipPingResult, len(ips))
	for _, ip := range ips {
		go func(addr string) {
			results <- ping_ip(addr, 3*time.Second)
		}(ip)
	}

	// 收集结果
	var reachable []ipPingResult
	for range ips {
		r := <-results
		if r.ok {
			reachable = append(reachable, r)
		}
	}

	if len(reachable) == 0 {
		fmt.Printf("all IPs unreachable for %s, using first: %s\n", domain, ips[0])
		return ips[0]
	}

	// 按延迟排序,选最快的
	sort.Slice(reachable, func(i, j int) bool {
		return reachable[i].delay < reachable[j].delay
	})

	best := reachable[0]
	fmt.Printf("%s -> %s (delay: %v)\n", domain, best.ip, best.delay)
	return best.ip
}

func parse_nslookup_output(output string, dnsServer string) []string {
	// 不依赖中文关键词,直接从输出中提取所有 IP 地址
	// Windows nslookup 输出可能是 GBK 编码,中文匹配会失败
	// 策略:提取所有 IP,去掉 DNS 服务器自身地址,剩余即为解析结果
	ipRegex := regexp.MustCompile(`(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})`)
	matches := ipRegex.FindAllStringSubmatch(output, -1)

	var ips []string
	for _, m := range matches {
		ip := m[1]
		// 跳过 DNS 服务器自身地址
		if ip == dnsServer {
			continue
		}
		ips = append(ips, ip)
	}

	return ips
}

总结

这个工具的核心思路是:让每个域名都选到真正最快、可达的 IP。关键设计决策:

  1. TCP 443 探测 > ICMP ping --- 更真实反映 HTTPS 服务可用性
  2. 两级并发 --- 域名级 + IP 级,大幅缩短总耗时
  3. 降级策略 --- 不可达时仍保留首个 IP,不丢失域名
  4. 外部文件 + 内置默认 --- 灵活配置,开箱即用

欢迎在评论区交流改进建议!