Below is my client code which stream all the customer URLs from a golang grpc server. It takes Request
input parameter and streams customer URLs based on matching a particular clientId
. In my this code, I am streaming all customer URLs for ClientId 12345 and it works fine.
I am also creating an XML file with all the URLs in it for particular clientId
as shown below. I need to split an XML file into multiple XML files for same clientId
to ensure it conforms to these requirements:
- A single XML file should not be more than 50MB max. It can be approximate, doesn't have to be accurate.
- A single XML file should not have more than 50K URLs max.
I know it's weird that 50k URL limit will be reached sooner than 50MB limit but this is the requirement I have. Based on above logic, I need to make multiple XML files for particular clientId
. All those multiple files can be like 12345_abc_1.xml
, 12345_abc_2.xml
or any other better naming format.
Below is the code I got which works fine but I am opting for code review to see if anything can be improved or optimized so that it can be run efficiently.
func main() {
// this "clientId" will be configurable in future
clientId := 12345
timeout := time.Duration(1000) * time.Millisecond
ctx, _ := context.WithTimeout(context.Background(), timeout)
conn, err := grpc.DialContext(ctx, "localhost:50005", grpc.WithInsecure())
if err != nil {
log.Fatalf("can not connect with server %v", err)
}
// create stream
client := pb.NewCustomerServiceClient(conn)
req := &pb.Request{ClientId: clientId}
stream, err := client.FetchResponse(context.Background(), req)
if err != nil {
log.Fatalf("open stream error %v", err)
}
// create new object to populate all URL data in memory
urlHolder := NewClient()
t := time.Unix(0, 0).UTC()
done := make(chan bool)
go func() {
// create new object to populate all URL data in memory
urlHolder := NewClient()
urlCounter := 0
byteCounter := 0
fileCounter := 0
for {
resp, err := stream.Recv()
if err == io.EOF {
done <- true
file, _ := os.Create(fmt.Sprintf("%d_abc_%d.xml", clientId, fileCounter))
urlHolder.WriteTo(file)
return
}
if err != nil {
log.Fatalf("can not receive %v", err)
}
log.Printf("Resp received: %s", resp.GetCustomerUrl())
// I add the bytes of the URL here as a return
urlBytes := urlHolder.Add(&URL{
Loc: resp.GetCustomerUrl(),
LastMod: &t,
ChangeFreq: Daily,
Priority: 10.2,
})
byteCounter += urlBytes
urlCounter += 1
if byteCounter > 49000000 || urlCounter >= 50000 {
file, _ := os.Create(fmt.Sprintf("%d_abc_%d.xml", clientId, fileCounter))
urlHolder.WriteTo(file)
urlHolder = NewClient() // create a new object for next loop
fileCounter += 1 // prepare fileCounter for next loop
byteCounter = 0 // restart count variables
urlCounter = 0
}
}
}()
<-done
log.Printf("finished")
}
Here is my urlholder.go
file:
type URL struct {
Loc string `xml:"loc"`
LastMod *time.Time `xml:"lastmod"`
ChangeFreq ChangeFreq `xml:"changefreq"`
Priority float32 `xml:"priority"`
}
type UrlMap struct {
XMLName xml.Name `xml:"urlset"`
Xmlns string `xml:"xmlns,attr"`
URLs []*URL `xml:"url"`
Minify bool `xml:"-"`
}
func NewClient() *UrlMap {
return &UrlMap{
Xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9",
URLs: make([]*URL, 0),
}
}
func (s *UrlMap) Add(u *URL) (int) {
s.URLs = append(s.URLs, u)
var urlBytes []byte
var err error
urlBytes, err = xml.Marshal(u) // Transform to bytes using xml.Marshal or xml.MarshalIndent
if err != nil {
panic(err) // or return the error
}
return len(urlBytes)
}
// WriteTo writes XML encoded urlMap to given io.Writer.
func (s *UrlMap) WriteTo(w io.Writer) (n int64, err error) {
cw := NewCounterWriter(w)
_, err = cw.Write([]byte(xml.Header))
if err != nil {
return cw.Count(), err
}
en := xml.NewEncoder(cw)
if !s.Minify {
en.Indent("", " ")
}
err = en.Encode(s)
cw.Write([]byte{'\n'})
return cw.Count(), err
}
Here is my CounterWriter
class -
type CounterWriter struct {
writer io.Writer
count int64
}
var _ io.Writer = (*CounterWriter)(nil)
func NewCounterWriter(w io.Writer) (cw *CounterWriter) {
return &CounterWriter{
writer: w,
}
}
func (cw *CounterWriter) Write(p []byte) (n int, err error) {
n, err = cw.writer.Write(p)
cw.count = cw.count + int64(n)
return n, err
}
// Count returns the number of bytes written to the Writer.
func (cw *CounterWriter) Count() (n int64) {
return cw.count
}
Problem Statement
I am looking for code review on above code:
- Is there anything that can be improved in terms of multiple XML file generation logic? I modified my
Add
method inurlholder.go
file to return bytes (which isint
) but I also haveWriteTo
method in same go file which also returns bytes (which isint64
). Can we simplify my above multipleXML
file generation logic? - Also do we need to use interface to split out above class and make it more clean?
I am just trying to learn on how to write good go code as this code will be running in production.